fix(automations): wait for Codex prompt lifecycle

This commit is contained in:
Brennan Benson
2026-08-27 07:47:47 -07:00
parent 026389a3bc
commit 0ce1def8d4
3 changed files with 25 additions and 11 deletions
@@ -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())
@@ -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,
@@ -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(