fix(codex): stabilize goal status updates (#651)

This commit is contained in:
SmallSpider
2026-05-20 11:31:28 +08:00
committed by GitHub
parent 2aaae25d0a
commit 25631d971c
4 changed files with 257 additions and 18 deletions
+82 -13
View File
@@ -1855,6 +1855,39 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
};
const forwardedGoalSignaturesByThreadId = new Map<string, string>();
const forwardedGoalClearsByThreadId = new Set<string>();
const adminInterruptedTurnIds = new Set<string>();
const adminInterruptedTurnTimers = new Map<string, ReturnType<typeof setTimeout>>();
const suppressReadyForInterruptedTurn = (turnId: string | null) => {
if (!turnId) {
return;
}
adminInterruptedTurnIds.add(turnId);
const previousTimer = adminInterruptedTurnTimers.get(turnId);
if (previousTimer) {
clearTimeout(previousTimer);
}
const timer = setTimeout(() => {
adminInterruptedTurnIds.delete(turnId);
adminInterruptedTurnTimers.delete(turnId);
}, 30_000);
timer.unref?.();
adminInterruptedTurnTimers.set(turnId, timer);
};
const consumeInterruptedTurnReadySuppression = (turnId: string | null): boolean => {
if (!turnId || !adminInterruptedTurnIds.has(turnId)) {
return false;
}
adminInterruptedTurnIds.delete(turnId);
const timer = adminInterruptedTurnTimers.get(turnId);
if (timer) {
clearTimeout(timer);
adminInterruptedTurnTimers.delete(turnId);
}
return true;
};
const shouldForwardGoalUpdate = (msg: Record<string, unknown>, threadId: string | null): boolean => {
const goal = asRecord(msg.goal);
@@ -1871,14 +1904,22 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
return false;
}
forwardedGoalClearsByThreadId.delete(scopedThreadId);
forwardedGoalSignaturesByThreadId.set(scopedThreadId, signature);
return true;
};
const noteGoalCleared = (threadId: string | null) => {
if (threadId) {
forwardedGoalSignaturesByThreadId.delete(threadId);
const shouldForwardGoalClear = (threadId: string | null): boolean => {
if (!threadId) {
return true;
}
if (forwardedGoalClearsByThreadId.has(threadId)) {
logger.debug(`[Codex] Suppressing duplicate thread goal clear; threadId=${threadId}`);
return false;
}
forwardedGoalClearsByThreadId.add(threadId);
forwardedGoalSignaturesByThreadId.delete(threadId);
return true;
};
const handleCodexEvent = (msg: Record<string, unknown>) => {
@@ -1887,6 +1928,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
const eventTurnId = asString(msg.turn_id ?? msg.turnId);
const eventThreadId = asString(msg.thread_id ?? msg.threadId);
const isTerminalEvent = msgType === 'task_complete' || msgType === 'turn_aborted' || msgType === 'task_failed';
const suppressReadyForThisTerminalEvent = isTerminalEvent
? consumeInterruptedTurnReadySuppression(eventTurnId)
: false;
if (msgType === 'thread_started') {
const threadId = asString(msg.thread_id ?? msg.threadId);
@@ -1947,7 +1991,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
}
if (msgType === 'thread_goal_cleared') {
noteGoalCleared(eventThreadId ?? this.currentThreadId);
if (!shouldForwardGoalClear(eventThreadId ?? this.currentThreadId)) {
return;
}
session.sendAgentMessage({
...addCodexEventScope(msg, 'parent', eventThreadId ?? this.currentThreadId),
id: randomUUID()
@@ -2091,9 +2137,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
wakeLoop();
}
if (isTerminalEvent && !turnInFlight) {
if (isTerminalEvent && !turnInFlight && !suppressReadyForThisTerminalEvent) {
scheduleReadyAfterTurn?.();
} else if (readyAfterTurnTimer && msgType !== 'task_started') {
} else if (readyAfterTurnTimer && msgType !== 'task_started' && !suppressReadyForThisTerminalEvent) {
scheduleReadyAfterTurn?.();
}
@@ -2496,6 +2542,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
let hasThread = false;
let pending: QueuedMessage | null = null;
let suppressReadyForAdminCommand = false;
clearReadyAfterTurnTimer = () => {
if (!readyAfterTurnTimer) {
@@ -2507,8 +2554,14 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
scheduleReadyAfterTurn = () => {
clearReadyAfterTurnTimer?.();
if (suppressReadyForAdminCommand) {
return;
}
readyAfterTurnTimer = setTimeout(() => {
readyAfterTurnTimer = null;
if (suppressReadyForAdminCommand) {
return;
}
emitReadyIfIdle({
pending: pending ?? (recoveryInFlight ? activeMessage : null),
queueSize: () => session.queue.size(),
@@ -2527,7 +2580,9 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
const sendGoalEvent = (event: Record<string, unknown>) => {
const threadId = asString(event.thread_id ?? event.threadId) ?? this.currentThreadId;
if (event.type === 'thread_goal_cleared') {
noteGoalCleared(threadId);
if (!shouldForwardGoalClear(threadId)) {
return;
}
} else if (event.type === 'thread_goal_updated' && !shouldForwardGoalUpdate(event, threadId)) {
return;
}
@@ -2549,6 +2604,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
};
const interruptActiveTurn = async () => {
suppressReadyForInterruptedTurn(this.currentTurnId);
await this.interruptActiveTurns('slash command');
};
@@ -2751,6 +2807,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
} else {
sendVisibleStatus('No goal to clear');
}
sendGoalEvent({ type: 'thread_goal_cleared', thread_id: threadId });
return true;
}
@@ -2764,6 +2821,7 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
});
const goal = normalizeGoal(response.goal);
sendVisibleStatus(formatGoalUsage(goal));
sendGoalEvent({ type: 'thread_goal_updated', thread_id: threadId, goal });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
if (/goals feature is disabled|unsupported remote app-server request|method not found/i.test(detail)) {
@@ -2860,6 +2918,12 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
messageBuffer.addMessage(message.message, 'user');
}
activeMessage = message;
const isGoalCommand = parseGoalCommand(message.message) !== null;
let suppressReadyAfterMessage = isGoalCommand;
if (isGoalCommand) {
suppressReadyForAdminCommand = true;
clearReadyAfterTurnTimer?.();
}
try {
if (await handleGoalCommand(message)) {
@@ -3021,12 +3085,17 @@ class CodexRemoteLauncher extends RemoteLauncherBase {
childAgentRuntimeById.clear();
session.onThinkingChange(false);
clearReadyAfterTurnTimer?.();
emitReadyIfIdle({
pending: pending ?? (recoveryInFlight ? activeMessage : null),
queueSize: () => session.queue.size(),
shouldExit: this.shouldExit,
sendReady
});
if (!suppressReadyAfterMessage) {
emitReadyIfIdle({
pending: pending ?? (recoveryInFlight ? activeMessage : null),
queueSize: () => session.queue.size(),
shouldExit: this.shouldExit,
sendReady
});
}
}
if (suppressReadyAfterMessage) {
suppressReadyForAdminCommand = false;
}
logActiveHandles('after-turn');
}