fix(runtime): retract Orca's own Codex ready title when the next turn starts (#35547)

* fix(runtime): retract Orca's own Codex ready title when the next turn starts

A Codex that paints no working title of its own (its activity title off) kept the
"Codex ready" frame Orca writes on its Stop hook for the whole next turn, so
tui-idle waits, mailbox delivery and the tab all read it as at rest. The working
hook now replaces Orca's own rest label with one working frame, as #29429 did
for agents whose profiles synthesize working titles.

Tracked in STA-12718.

* test: cover titleless turn wait and mailbox lifecycle

* test: type title recovery write spy

* test: isolate CLI lifecycle exit state across runtimes

---------

Co-authored-by: Codex QA <codex-qa@users.noreply.github.com>
This commit is contained in:
Brennan Benson
2026-10-29 10:06:42 -07:00
committed by GitHub
co-authored by Codex QA
parent e41105b5fc
commit b42fcd8a00
4 changed files with 311 additions and 10 deletions
@@ -1,20 +1,90 @@
import { rmSync } from 'node:fs'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentStatusIpcPayload } from '../../shared/agent-status-types'
import type { RuntimeMobileSessionTabsSnapshot } from '../../shared/runtime-types'
import { extractAllOscTitleEntries } from '../../shared/osc-title-extraction'
import { settledWriteStub } from '../providers/settled-pty-write-stub'
import { paneKeyPtyId, ptyPaneKey, rememberPaneKeyForPty } from '../ipc/pty/pane/key-state'
import { mainProcessState } from '../startup/main-process-state'
import { driveSyntheticTitleFromHookStatus } from '../startup/synthetic-title-runtime'
import { createLongPollServer } from './runtime-rpc-long-poll-server.test-fixture'
import {
createBoundRun,
createDatabase,
createRuntime,
insertDirectRunMessage,
LEAF_ID,
PANE_KEY,
PTY_ID,
TAB_ID,
TERMINAL_HANDLE,
temporaryDirectories
temporaryDirectories,
WORKTREE_ID
} from './orchestration-mailbox-notification-test-harness'
describe('stale-working title clear and completion recovery', () => {
function producerFixture(agentType: 'codex' | 'pi' = 'codex') {
const db = createDatabase('orca-hook-title-contract-')
const hook: AgentStatusIpcPayload = {
paneKey: PANE_KEY,
terminalHandle: TERMINAL_HANDLE,
agentType,
state: 'done',
prompt: '',
connectionId: null,
receivedAt: Date.now(),
stateStartedAt: Date.now()
}
const { runtime } = createRuntime(db, {
launchAgent: agentType,
getAgentStatusSnapshot: () => [hook]
})
const write = vi.fn((_ptyId: string, _data: string) => true)
runtime.setPtyController({
write,
writeWithSettlement: settledWriteStub(write),
kill: vi.fn(),
getForegroundProcess: async () => agentType
})
const previousRuntime = mainProcessState.runtime
const previousPaneKey = ptyPaneKey.get(PTY_ID)
const previousPtyId = paneKeyPtyId.get(PANE_KEY)
const previousOtherPtyId = previousPaneKey ? paneKeyPtyId.get(previousPaneKey) : undefined
mainProcessState.runtime = runtime
rememberPaneKeyForPty(PTY_ID, PANE_KEY)
const status = (state: 'done' | 'working') => {
Object.assign(hook, { state, receivedAt: Date.now(), stateStartedAt: Date.now() })
driveSyntheticTitleFromHookStatus({ paneKey: PANE_KEY, payload: hook })
}
const dispose = async () => {
try {
await runtime.onPtyExit(PTY_ID, 0)
} finally {
db.close()
mainProcessState.runtime = previousRuntime
if (previousPaneKey === undefined) {
ptyPaneKey.delete(PTY_ID)
} else {
ptyPaneKey.set(PTY_ID, previousPaneKey)
}
if (previousPtyId === undefined) {
paneKeyPtyId.delete(PANE_KEY)
} else {
paneKeyPtyId.set(PANE_KEY, previousPtyId)
}
if (previousPaneKey && previousPaneKey !== PANE_KEY && previousOtherPtyId !== undefined) {
paneKeyPtyId.set(previousPaneKey, previousOtherPtyId)
}
}
}
return { db, runtime, write, status, dispose }
}
describe('hook title evidence and completion recovery', () => {
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllEnvs()
vi.useRealTimers()
for (const directory of temporaryDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
@@ -59,4 +129,184 @@ describe('stale-working title clear and completion recovery', () => {
])
db.close()
})
it('CLI waits through a silent titleless turn and delivers mail only after Stop', async () => {
const { db, runtime, write, status, dispose } = producerFixture()
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-title-wait-cli-'))
temporaryDirectories.push(userDataPath)
const server = createLongPollServer({ runtime, userDataPath, enableWebSocket: false })
const exitCode = process.exitCode
for (const name of ['ORCA_AGENT_SESSION_ID', 'ORCA_ENVIRONMENT', 'ORCA_PAIRING_CODE']) {
vi.stubEnv(name, '')
}
vi.stubEnv('ORCA_USER_DATA_PATH', userDataPath)
vi.stubEnv('ORCA_TERMINAL_HANDLE', TERMINAL_HANDLE)
const output = vi.spyOn(console, 'log').mockImplementation(() => {})
try {
// The source CLI has its own TS project; keep the real client import at the runtime boundary.
const cliPath = '../../cli/index'
const { main }: { main(argv: string[], cwd: string): Promise<void> } = await import(cliPath)
const wait = async () => {
output.mockClear()
process.exitCode = 0
await main(
[
'terminal',
'wait',
'--terminal',
TERMINAL_HANDLE,
'--for',
'tui-idle',
'--timeout-ms',
'300',
'--json'
],
userDataPath
)
return JSON.parse(String(output.mock.calls.at(-1)?.[0]))
}
await server.start()
const run = createBoundRun(db, 'Titleless Run')
await runtime.listTerminals()
status('done')
await expect(
runtime.sendTerminal(
TERMINAL_HANDLE,
{ text: 'next prompt', enter: true },
{ inputKind: 'driving' }
)
).resolves.toMatchObject({ accepted: true })
write.mockClear()
status('working')
insertDirectRunMessage(db, run.id, 'Worker progress')
runtime.notifyMessageArrived(`run:${run.id}`, 'status')
runtime.onPtyData(PTY_ID, 'working without native titles\r\n', 1)
// Real silence lets the production display timer fire; no output resets it.
await new Promise((resolve) => setTimeout(resolve, 3_200))
expect((await runtime.listTerminals()).terminals[0]?.title).toBe('Codex')
expect(runtime.getTrackedPtyTitle(PTY_ID)).toBe('⠋ Codex')
expect(await wait()).toMatchObject({ ok: false, error: { code: 'timeout' } })
expect(process.exitCode).toBe(1)
expect(write).not.toHaveBeenCalled()
status('done')
expect(await wait()).toMatchObject({ ok: true, result: { wait: { satisfied: true } } })
expect(process.exitCode).toBe(0)
await expect
.poll(() => write.mock.calls.map(([, data]) => data))
.toEqual([expect.stringContaining('You have 1 orchestration message'), '\r'])
runtime.notifyMessageArrived(`run:${run.id}`, 'status')
await new Promise((resolve) => setTimeout(resolve, 500))
expect(write).toHaveBeenCalledTimes(2)
} finally {
process.exitCode = exitCode ?? 0
try {
await server.stop()
} finally {
await dispose()
}
}
}, 15_000)
it('Pi controlled hooks retract rest and preserve a subsequently recorded native title', async () => {
vi.useFakeTimers()
const { runtime, status, dispose } = producerFixture('pi')
try {
// Pi 0.74.0/Linux capture, 2026-10-16; adjacent .meta.json records provenance.
const capture = readFileSync(
new URL('./__fixtures__/pi-0-74-late-kitty-esc-cr.txt', import.meta.url),
'utf8'
)
const nativeTitle = extractAllOscTitleEntries(capture).find(({ title }) =>
title.startsWith('π')
)
if (!nativeTitle) {
throw new Error('Pi capture must contain its native OSC title')
}
const nativeFrame = capture.slice(
capture.lastIndexOf('\x1b]', nativeTitle.end - 1),
nativeTitle.end
)
await runtime.listTerminals()
status('done')
expect(runtime.getTrackedPtyTitle(PTY_ID)).toBe('Pi ready')
status('working')
expect(runtime.getTrackedPtyTitle(PTY_ID)).toBe('⠋ Pi')
await expect(runtime.getTerminalAgentStatus(TERMINAL_HANDLE)).resolves.toMatchObject({
status: 'working'
})
runtime.onPtyData(PTY_ID, nativeFrame, 1)
expect(runtime.getTrackedPtyTitle(PTY_ID)).toBe('π - work')
status('working')
expect(runtime.getTrackedPtyTitle(PTY_ID)).toBe('π - work')
} finally {
await dispose()
}
})
it.each([20, 200])(
'bounds semantic ingestion and snapshot tab visits at %i tabs',
async (population) => {
vi.useFakeTimers()
const { runtime, status, dispose } = producerFixture()
try {
await runtime.listTerminals()
status('done')
await runtime.getTerminalAgentStatus(TERMINAL_HANDLE)
await vi.advanceTimersByTimeAsync(1_000)
runtime.mobileSessionTabsByWorktree.clear()
let visits = 0
for (const [group, worktree] of [WORKTREE_ID, 'unrelated-worktree'].entries()) {
const tabs: RuntimeMobileSessionTabsSnapshot['tabs'] = Array.from(
{ length: population / 2 },
(_, index) => {
const target = group === 0 && index === population / 2 - 1
return {
type: 'terminal',
id: target ? `${TAB_ID}::${LEAF_ID}` : `tab-${group}-${index}`,
parentTabId: target ? TAB_ID : `parent-${group}-${index}`,
leafId: target ? LEAF_ID : `leaf-${index}`,
title: 'Codex',
ptyId: target ? PTY_ID : `other-${group}-${index}`,
isActive: false
}
}
)
const some = tabs.some.bind(tabs)
vi.spyOn(tabs, 'some').mockImplementation((predicate, thisArg) =>
some((tab, index, array) => {
visits += 1
return predicate.call(thisArg, tab, index, array)
})
)
runtime.mobileSessionTabsByWorktree.set(worktree, {
worktree,
publicationEpoch: 'title-contract',
snapshotVersion: 1,
activeGroupId: null,
activeTabId: null,
activeTabType: null,
tabs
})
}
const ingestion = vi.spyOn(runtime, 'ingestSyntheticTitleFrame')
status('working')
await vi.advanceTimersByTimeAsync(1_000)
expect(ingestion).toHaveBeenCalledTimes(1)
expect(ingestion).toHaveBeenCalledWith(PTY_ID, '\x1b]0;⠋ Codex\x07')
expect(visits).toBeGreaterThanOrEqual(population)
expect(visits).toBeLessThanOrEqual(4 * population)
ingestion.mockClear()
visits = 0
for (let event = 0; event < 100; event += 1) {
status('working')
}
await vi.advanceTimersByTimeAsync(1_000)
expect(ingestion).not.toHaveBeenCalled()
expect(visits).toBe(0)
} finally {
await dispose()
}
}
)
})
@@ -190,6 +190,11 @@ export abstract class OrcaRuntimeWithEmitDaemonPtyTransientFact extends OrcaRunt
return null
}
/** The same title for main's synthetic title producer, which must see its own frames. */
getTrackedPtyTitle(ptyId: string): string | null {
return this.getTrackedRawTitleForPty(ptyId)
}
protected isLiveCursorNativeTitle(rawTitle: string, meta?: TerminalTitleFactMeta): boolean {
return isCursorNativeAgentTitle(rawTitle) && meta?.staleWorkingTitleClear !== true
}
@@ -6,7 +6,7 @@ type EnrichedListener = (event: Parameters<typeof driveSyntheticTitleFromHookSta
const hooks = vi.hoisted(() => {
const enriched: EnrichedListener[] = []
return { enriched, ingest: vi.fn() }
return { enriched, ingest: vi.fn(), trackedTitle: vi.fn((): string | null => null) }
})
vi.mock('../ipc/pty/pane/key-state', () => ({
@@ -37,11 +37,13 @@ function hermesStatus(state: 'working' | 'done'): Parameters<EnrichedListener>[0
beforeEach(() => {
hooks.enriched.length = 0
hooks.ingest.mockReset()
hooks.trackedTitle.mockReset()
mainProcessState.mainWindow = null
mainProcessState.store = null
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: sendSyntheticTitle reads only ingestSyntheticTitleFrame from the runtime.
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the producer reads only these two members of the runtime.
mainProcessState.runtime = {
ingestSyntheticTitleFrame: hooks.ingest
ingestSyntheticTitleFrame: hooks.ingest,
getTrackedPtyTitle: hooks.trackedTitle
} as unknown as typeof mainProcessState.runtime
})
@@ -73,4 +75,24 @@ describe('synthetic agent titles without a window', () => {
expect(hooks.ingest).not.toHaveBeenCalled()
})
// Codex leaves working titles to itself, so a Codex that paints none kept "ready" all turn.
it.each([
{ title: 'Codex ready', retracted: true },
{ title: 'Codex - action required', retracted: true },
{ title: '⠋ Fix the build | orca', retracted: false }
])(
'retracts only its own rest label when Codex starts a turn ($title)',
({ title, retracted }) => {
hooks.trackedTitle.mockReturnValue(title)
driveSyntheticTitleFromHookStatus({
paneKey: 'pane-1',
payload: { state: 'working', agentType: 'codex' }
})
expect(hooks.ingest.mock.calls.map(([, frame]) => frame)).toEqual(
retracted ? ['\x1b]0;⠋ Codex\x07'] : []
)
}
)
})
+28 -4
View File
@@ -1,6 +1,7 @@
import { registerPaneKeyTeardownListener, getPtyIdForPaneKey } from '../ipc/pty/pane/key-state'
import { agentHookServer } from '../agent-hooks/server'
import type { AgentStatusState } from '../../shared/agent-status-types'
import { normalizeTerminalTitle } from '../../shared/agent-title-status'
import {
getSyntheticAgentTitleProfile,
shouldDriveSyntheticAgentTitleFromHook,
@@ -148,6 +149,27 @@ export function driveSyntheticTitleFromHook(
sendSyntheticTitle(ptyId, `\x1b]0;${label}\x07${needsUserInput ? '\x07' : ''}`, { force: true })
}
// Why: a profile that leaves working titles to the agent never overwrites the rest label Orca
// wrote, so an agent that paints no working title (Codex with its activity title off) would read
// as at rest for the whole next turn, to tui-idle, mailbox delivery and the tab alike.
function retractSyntheticRestTitle(paneKey: string, profile: SyntheticAgentTitleProfile): void {
const ptyId = getPtyIdForPaneKey(paneKey)
const title = ptyId ? state.runtime?.getTrackedPtyTitle(ptyId) : null
if (
!ptyId ||
!title ||
![profile.idleLabel, profile.permissionLabel].some(
(label) => normalizeTerminalTitle(label) === title
)
) {
return
}
// One frame and no spinner timer, so the agent's own titles still win whenever it paints them.
sendSyntheticTitle(ptyId, `\x1b]0;${SPINNER_FRAMES[0]} ${profile.workingLabel}\x07`, {
semantic: true
})
}
export function initializeSyntheticTitleRuntime(): void {
// Drop torn-down PTYs explicitly or the shared timer loops forever on no-op sends.
registerPaneKeyTeardownListener((paneKey) => stopSyntheticTitleSpinner(paneKey))
@@ -175,10 +197,12 @@ export function driveSyntheticTitleFromHookStatus(event: {
}
// Inject hook-derived idle/permission frames that native OSC titles can omit.
const profile = getSyntheticAgentTitleProfile(event.payload.agentType)
if (
profile &&
shouldDriveSyntheticAgentTitleFromHook(event.payload.agentType, event.payload.state)
) {
if (!profile) {
return
}
if (shouldDriveSyntheticAgentTitleFromHook(event.payload.agentType, event.payload.state)) {
driveSyntheticTitleFromHook(event.paneKey, event.payload.state, profile)
} else if (event.payload.state === 'working' && profile.synthesizeTerminalTitle !== false) {
retractSyntheticRestTitle(event.paneKey, profile)
}
}