fix(codex): wait for manual compaction to finish (#1038)

* test: reproduce issue #982

* fix: wait for Codex manual compaction (closes #982)

* test: cover Codex compaction turn completion
This commit is contained in:
SSU-WEI HUANG
2026-07-16 12:30:04 +08:00
committed by GitHub
parent c87720ab4d
commit 553b3492f1
4 changed files with 259 additions and 2 deletions
+167
View File
@@ -1827,6 +1827,16 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
message: QueuedMessage;
timeout: ReturnType<typeof setTimeout> | null;
} | null = null;
let manualCompact: {
threadId: string;
turnId: string | null;
compacted: boolean;
terminal: { type: 'complete' | 'failed'; turnId: string; error?: string } | null;
timeout: ReturnType<typeof setTimeout> | null;
abortHandler: (() => void) | null;
resolve: () => void;
reject: (error: Error) => void;
} | null = null;
let loopWakeWaiter: (() => void) | null = null;
const wakeLoop = () => {
@@ -1930,6 +1940,132 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
});
};
const clearManualCompact = (compact: typeof manualCompact) => {
if (!compact) {
return;
}
if (compact.timeout) {
clearTimeout(compact.timeout);
compact.timeout = null;
}
if (compact.abortHandler) {
this.abortController.signal.removeEventListener('abort', compact.abortHandler);
compact.abortHandler = null;
}
if (manualCompact === compact) {
manualCompact = null;
}
};
const settleManualCompact = (
compact: typeof manualCompact,
error?: Error
) => {
if (!compact || manualCompact !== compact) {
return;
}
clearManualCompact(compact);
if (error) {
compact.reject(error);
} else {
compact.resolve();
}
};
const beginManualCompact = (threadId: string): Promise<void> => {
if (manualCompact) {
settleManualCompact(manualCompact, new Error('Compaction superseded'));
}
return new Promise<void>((resolve, reject) => {
const compact = {
threadId,
turnId: null as string | null,
compacted: false,
terminal: null as { type: 'complete' | 'failed'; turnId: string; error?: string } | null,
timeout: null as ReturnType<typeof setTimeout> | null,
abortHandler: null as (() => void) | null,
resolve,
reject
};
manualCompact = compact;
compact.timeout = setTimeout(() => {
settleManualCompact(compact, new Error('timed out waiting for Codex compaction to finish'));
}, SAME_THREAD_COMPACT_TIMEOUT_MS);
compact.timeout.unref?.();
compact.abortHandler = () => {
settleManualCompact(compact, new Error('compaction interrupted'));
};
this.abortController.signal.addEventListener('abort', compact.abortHandler, { once: true });
});
};
const recordManualCompactStarted = (threadId: string | null, turnId: string | null) => {
const compact = manualCompact;
if (!compact || !turnId || (threadId && threadId !== compact.threadId)) {
return;
}
compact.turnId ??= turnId;
};
const recordManualCompactCompleted = (
threadId: string | null,
turnId: string | null,
awaitTurnCompletion: boolean
) => {
const compact = manualCompact;
if (!compact || threadId !== compact.threadId) {
return;
}
if (!awaitTurnCompletion) {
settleManualCompact(compact);
return;
}
if (!turnId && !compact.turnId) {
settleManualCompact(compact);
return;
}
if (turnId && compact.turnId && turnId !== compact.turnId) {
return;
}
compact.turnId ??= turnId;
compact.compacted = true;
if (!compact.turnId) {
settleManualCompact(compact);
return;
}
if (compact.terminal?.turnId === compact.turnId) {
settleManualCompact(
compact,
compact.terminal.type === 'failed'
? new Error(compact.terminal.error ?? 'Codex compaction failed')
: undefined
);
}
};
const recordManualCompactTerminal = (
type: 'complete' | 'failed',
threadId: string | null,
turnId: string | null,
error?: string
) => {
const compact = manualCompact;
if (!compact || !turnId || (threadId && threadId !== compact.threadId)) {
return;
}
if (!compact.turnId || turnId !== compact.turnId) {
return;
}
compact.terminal = { type, turnId, ...(error ? { error } : {}) };
if (type === 'failed' || compact.compacted) {
settleManualCompact(
compact,
type === 'failed' ? new Error(error ?? 'Codex compaction failed') : undefined
);
}
};
const forwardedGoalSignaturesByThreadId = new Map<string, string>();
const forwardedGoalClearsByThreadId = new Set<string>();
const adminInterruptedTurnIds = new Set<string>();
@@ -2208,10 +2344,32 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}
if (msgType === 'thread_compacted') {
recordManualCompactCompleted(
eventThreadId,
eventTurnId,
msg.await_turn_completion === true
);
completeCompactRecovery(eventThreadId);
return;
}
if (msgType === 'task_started') {
recordManualCompactStarted(eventThreadId ?? this.currentThreadId, eventTurnId);
} else if (msgType === 'task_complete') {
recordManualCompactTerminal(
'complete',
eventThreadId ?? this.currentThreadId,
eventTurnId
);
} else if (msgType === 'task_failed' || msgType === 'turn_aborted') {
recordManualCompactTerminal(
'failed',
eventThreadId ?? this.currentThreadId,
eventTurnId,
asString(msg.error) ?? (msgType === 'turn_aborted' ? 'Codex compaction was aborted' : undefined)
);
}
if (eventThreadId && this.currentThreadId && eventThreadId !== this.currentThreadId) {
logger.debug(
`[Codex] Routing event from non-active thread into agent trace; ` +
@@ -3328,14 +3486,23 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}
sendVisibleStatus('Compaction started');
const compactCompletion = beginManualCompact(threadId);
void compactCompletion.catch(() => {});
try {
await appServerClient.compactThread({ threadId }, {
signal: this.abortController.signal
});
await compactCompletion;
sendVisibleStatus('Compaction completed');
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
sendVisibleStatus(`Compaction failed: ${detail}`);
} finally {
if (manualCompact?.threadId === threadId) {
const compact = manualCompact;
clearManualCompact(compact);
compact.resolve();
}
}
return true;
};