From 2aaae25d0ae5ae573c0bef5e2a4c243329298361 Mon Sep 17 00:00:00 2001 From: SmallSpider <568442079@qq.com> Date: Wed, 20 May 2026 10:00:51 +0800 Subject: [PATCH] fix(codex): dedupe repeated goal updates (#649) --- cli/src/codex/codexRemoteLauncher.test.ts | 88 +++++++++++++++++++++++ cli/src/codex/codexRemoteLauncher.ts | 78 ++++++++++++++++++-- 2 files changed, 162 insertions(+), 4 deletions(-) diff --git a/cli/src/codex/codexRemoteLauncher.test.ts b/cli/src/codex/codexRemoteLauncher.test.ts index 21b4f64f..a3b834f4 100644 --- a/cli/src/codex/codexRemoteLauncher.test.ts +++ b/cli/src/codex/codexRemoteLauncher.test.ts @@ -41,6 +41,7 @@ const harness = vi.hoisted(() => ({ emitSecondChildMessage: false, emitLateChildCommandAfterParentTool: false, emitParentUsageEvents: false, + emitParentGoalDuplicateEvents: false, emitChildNestedAgentTool: false, emitParentTitleChange: false, emitParentSpawnFailureWithoutAgentId: false, @@ -223,6 +224,41 @@ vi.mock('./codexAppServerClient', () => { } if (params?.threadId === 'thread-1') { + if (harness.emitParentGoalDuplicateEvents) { + const goalBase = { + threadId, + objective: 'keep benchmark work moving', + status: 'active', + tokenBudget: null, + tokensUsed: 0, + timeUsedSeconds: 0, + createdAt: 1 + }; + for (let index = 0; index < 4; index += 1) { + const notification = { + threadId, + goal: { + ...goalBase, + timeUsedSeconds: index, + updatedAt: 2 + index + } + }; + harness.notifications.push({ method: 'thread/goal/updated', params: notification }); + this.notificationHandler?.('thread/goal/updated', notification); + } + const pausedNotification = { + threadId, + goal: { + ...goalBase, + status: 'paused', + timeUsedSeconds: 4, + updatedAt: 6 + } + }; + harness.notifications.push({ method: 'thread/goal/updated', params: pausedNotification }); + this.notificationHandler?.('thread/goal/updated', pausedNotification); + } + if (harness.emitParentTitleChange) { const titleStart = { item: { @@ -928,6 +964,7 @@ describe('codexRemoteLauncher', () => { harness.emitSecondChildMessage = false; harness.emitLateChildCommandAfterParentTool = false; harness.emitParentUsageEvents = false; + harness.emitParentGoalDuplicateEvents = false; harness.emitChildNestedAgentTool = false; harness.emitParentTitleChange = false; harness.emitParentSpawnFailureWithoutAgentId = false; @@ -1700,6 +1737,57 @@ describe('codexRemoteLauncher', () => { })); }); + it('suppresses duplicate parent goal updates that only change runtime counters', async () => { + harness.emitParentGoalDuplicateEvents = true; + const { session, codexMessages } = createSessionStub(); + + await codexRemoteLauncher(session as never); + + const goalMessages = codexMessages.filter((message): message is Record => { + return Boolean(message && typeof message === 'object' && (message as Record).type === 'thread_goal_updated'); + }); + expect(goalMessages).toHaveLength(2); + expect(goalMessages).toEqual([ + expect.objectContaining({ + thread_id: 'thread-1', + goal: expect.objectContaining({ + status: 'active', + updatedAt: 2 + }) + }), + expect.objectContaining({ + thread_id: 'thread-1', + goal: expect.objectContaining({ + status: 'paused', + updatedAt: 6 + }) + }) + ]); + }); + + it('suppresses duplicate goal events from repeated show commands', async () => { + const { session, codexMessages } = createSessionStub([ + '/goal keep benchmark work moving', + '/goal' + ]); + + await codexRemoteLauncher(session as never); + + expect(harness.goalSetCalls).toHaveLength(1); + expect(harness.goalGetCalls).toEqual([{ threadId: 'thread-1' }]); + const goalMessages = codexMessages.filter((message): message is Record => { + return Boolean(message && typeof message === 'object' && (message as Record).type === 'thread_goal_updated'); + }); + expect(goalMessages).toHaveLength(1); + expect(goalMessages[0]).toEqual(expect.objectContaining({ + thread_id: 'thread-1', + goal: expect.objectContaining({ + objective: 'keep benchmark work moving', + status: 'active' + }) + })); + }); + it('marks parent usage and compact events with parent scope', async () => { harness.emitParentUsageEvents = true; const { session, codexMessages } = createSessionStub(); diff --git a/cli/src/codex/codexRemoteLauncher.ts b/cli/src/codex/codexRemoteLauncher.ts index 01c2a459..82000a78 100644 --- a/cli/src/codex/codexRemoteLauncher.ts +++ b/cli/src/codex/codexRemoteLauncher.ts @@ -65,6 +65,40 @@ const SAME_THREAD_COMPACT_TIMEOUT_MS = 10 * 60 * 1000; const CODEX_GOALS_UNSUPPORTED_MESSAGE = 'Codex goals are not supported by this Codex runtime. Upgrade Codex or enable features.goals.'; const MAX_CODEX_GOAL_OBJECTIVE_CHARS = 4_000; +type GoalForwardSignature = { + objective: string | null; + status: string | null; + tokenBudget: number | null; + tokenBucket: number | null; +}; + +function goalNumber(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? value : null; +} + +function goalString(value: unknown): string | null { + return typeof value === 'string' ? value : null; +} + +function buildGoalForwardSignature(goal: Record): GoalForwardSignature { + const tokenBudget = goalNumber(goal.tokenBudget ?? goal.token_budget); + const tokensUsed = goalNumber(goal.tokensUsed ?? goal.tokens_used) ?? 0; + const tokenBucket = tokenBudget !== null && tokenBudget > 0 + ? Math.floor(Math.min(tokensUsed, tokenBudget) / Math.max(1, tokenBudget * 0.05)) + : null; + + return { + objective: goalString(goal.objective), + status: goalString(goal.status), + tokenBudget, + tokenBucket + }; +} + +function goalForwardSignatureKey(signature: GoalForwardSignature): string { + return JSON.stringify(signature); +} + function isSameThreadRetryableCodexError(error: string | null): boolean { if (!error) { return false; @@ -1820,6 +1854,33 @@ class CodexRemoteLauncher extends RemoteLauncherBase { }); }; + const forwardedGoalSignaturesByThreadId = new Map(); + + const shouldForwardGoalUpdate = (msg: Record, threadId: string | null): boolean => { + const goal = asRecord(msg.goal); + const scopedThreadId = threadId + ?? asString(goal?.threadId ?? goal?.thread_id) + ?? this.currentThreadId; + if (!goal || !scopedThreadId) { + return true; + } + + const signature = goalForwardSignatureKey(buildGoalForwardSignature(goal)); + if (forwardedGoalSignaturesByThreadId.get(scopedThreadId) === signature) { + logger.debug(`[Codex] Suppressing duplicate thread goal update; threadId=${scopedThreadId}`); + return false; + } + + forwardedGoalSignaturesByThreadId.set(scopedThreadId, signature); + return true; + }; + + const noteGoalCleared = (threadId: string | null) => { + if (threadId) { + forwardedGoalSignaturesByThreadId.delete(threadId); + } + }; + const handleCodexEvent = (msg: Record) => { const msgType = asString(msg.type); if (!msgType) return; @@ -1876,14 +1937,17 @@ class CodexRemoteLauncher extends RemoteLauncherBase { } if (msgType === 'thread_goal_updated') { - session.sendAgentMessage({ - ...addCodexEventScope(msg, 'parent', eventThreadId ?? this.currentThreadId), - id: randomUUID() - }); + if (shouldForwardGoalUpdate(msg, eventThreadId)) { + session.sendAgentMessage({ + ...addCodexEventScope(msg, 'parent', eventThreadId ?? this.currentThreadId), + id: randomUUID() + }); + } return; } if (msgType === 'thread_goal_cleared') { + noteGoalCleared(eventThreadId ?? this.currentThreadId); session.sendAgentMessage({ ...addCodexEventScope(msg, 'parent', eventThreadId ?? this.currentThreadId), id: randomUUID() @@ -2461,6 +2525,12 @@ class CodexRemoteLauncher extends RemoteLauncherBase { }; const sendGoalEvent = (event: Record) => { + const threadId = asString(event.thread_id ?? event.threadId) ?? this.currentThreadId; + if (event.type === 'thread_goal_cleared') { + noteGoalCleared(threadId); + } else if (event.type === 'thread_goal_updated' && !shouldForwardGoalUpdate(event, threadId)) { + return; + } session.sendAgentMessage({ ...addCodexEventScope(event, 'parent', this.currentThreadId), id: randomUUID()