From 0ce1def8d4d346dcf7e9aa2bbc7c77dfc6a2d35c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 07:47:47 -0700 Subject: [PATCH] fix(automations): wait for Codex prompt lifecycle --- .../hooks/useAutomationDispatchEvents.test.ts | 7 +++++-- .../src/hooks/useAutomationDispatchEvents.ts | 16 +++++++++++++--- .../lib/launch-agent-background-session.test.ts | 13 +++++++------ 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts index 81fc23a5728..19f220a479e 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.test.ts @@ -537,7 +537,7 @@ describe('useAutomationDispatchEvents setup launch', () => { ) }) - it('finalizes a fresh non-reuse terminal only after completed result persistence', async () => { + it('finalizes fresh Codex only after working and completed result persistence', async () => { const order: string[] = [] let launchArgs: { onAgentStatus?: (payload: { state: string }) => void } = {} mockMarkDispatchResult.mockImplementation( @@ -569,7 +569,10 @@ describe('useAutomationDispatchEvents setup launch', () => { } }) - await registerAndDispatch() + await registerAndDispatch(makeAutomation({ agentId: 'codex' })) + launchArgs.onAgentStatus?.({ state: 'done' }) + expect(mockFinalizeTerminalOwnership).not.toHaveBeenCalled() + launchArgs.onAgentStatus?.({ state: 'working' }) launchArgs.onAgentStatus?.({ state: 'done' }) await vi.waitFor(() => expect(mockFinalizeTerminalOwnership).toHaveBeenCalledOnce()) diff --git a/src/renderer/src/hooks/useAutomationDispatchEvents.ts b/src/renderer/src/hooks/useAutomationDispatchEvents.ts index 1e46b315f28..98d8ec952cb 100644 --- a/src/renderer/src/hooks/useAutomationDispatchEvents.ts +++ b/src/renderer/src/hooks/useAutomationDispatchEvents.ts @@ -595,6 +595,7 @@ export function useAutomationDispatchEvents(): void { } } } + let freshLaunchSawWorking = false const result = await launchAgentBackgroundSession({ agent: automation.agentId, worktreeId: worktree.id, @@ -607,8 +608,15 @@ export function useAutomationDispatchEvents(): void { onAgentStatus: (payload) => { latestAssistantMessage = payload.lastAssistantMessage?.trim() || latestAssistantMessage - // Why: session-boundary done = launch connect, not run completion (see observeAgentStatus). - if (payload.state !== 'done' || payload.sessionBoundary === true) { + if (payload.state === 'working') { + freshLaunchSawWorking = true + } + // Why: Claude marks launch boundaries done; Codex reports initial idle before its prompt turn works. + if ( + payload.state !== 'done' || + payload.sessionBoundary === true || + (automation.agentId === 'codex' && !freshLaunchSawWorking) + ) { return } handleAgentDone() @@ -634,7 +642,9 @@ export function useAutomationDispatchEvents(): void { releaseTerminalOwnership() } const launchedTabId = result.tabId - observeAgentStatus(result.paneKey, dispatchStartedAt) + observeAgentStatus(result.paneKey, dispatchStartedAt, { + requireWorkingAfterStart: automation.agentId === 'codex' + }) try { await markDispatchResult({ runId: run.id, diff --git a/src/renderer/src/lib/launch-agent-background-session.test.ts b/src/renderer/src/lib/launch-agent-background-session.test.ts index eedd1e9fb32..33b29523a10 100644 --- a/src/renderer/src/lib/launch-agent-background-session.test.ts +++ b/src/renderer/src/lib/launch-agent-background-session.test.ts @@ -90,11 +90,11 @@ describe('launchAgentBackgroundSession', () => { }) }) - it('spawns a PTY first and creates the inactive tab already bound to it', async () => { + it('spawns the Codex PTY first and publishes the inactive tab already bound to it', async () => { const { launchAgentBackgroundSession } = await import('./launch-agent-background-session') const result = await launchAgentBackgroundSession({ - agent: 'claude', + agent: 'codex', worktreeId: 'wt-1', prompt: 'run the automation', title: 'Nightly audit' @@ -123,7 +123,8 @@ describe('launchAgentBackgroundSession', () => { expect(mockSpawn).toHaveBeenCalledWith( expect.objectContaining({ cwd: '/repo/worktree', - command: "claude '--dangerously-skip-permissions' 'run the automation'", + command: "codex '--dangerously-bypass-approvals-and-sandbox' 'run the automation'", + startupCommandDelivery: 'shell-ready', env: expect.objectContaining({ ORCA_TAB_ID: tabId, ORCA_WORKTREE_ID: 'wt-1' @@ -146,11 +147,11 @@ describe('launchAgentBackgroundSession', () => { expect(mockSetTabLayout.mock.calls.at(-1)?.[1]).not.toHaveProperty('titlesByLeafId') expect(mockSpawn.mock.calls[0]?.[0]).toMatchObject({ launchConfig: { - agentCommand: "claude '--dangerously-skip-permissions'", - agentArgs: '--dangerously-skip-permissions', + agentCommand: "codex '--dangerously-bypass-approvals-and-sandbox'", + agentArgs: '--dangerously-bypass-approvals-and-sandbox', agentEnv: {} }, - launchAgent: 'claude', + launchAgent: 'codex', launchToken: expect.stringMatching(UUID_RE) }) expect(mockSpawn.mock.calls[0]?.[0].launchToken).toBe(