Files
orca/src/main/codex/codex-default-mode-subagents.test.ts
T
Brennan Benson 5b228ea71d One shared subagent tracker for Claude, Codex and Grok chat rows (#27228)
* test(native-chat): pin each agent's subagent row writes before unifying the rosters

* refactor(native-chat): one shared subagent tracker owns every agent's subagent rows

Claude, Codex and ACP (Grok) each kept their own subagent roster with drifted
rules. They now only report what they observe; one tracker decides grouping,
runs, names, state, sweeps, retention, row writes and restart inheritance.
The golden row-write tests from the previous commit show each rule change.

* fix(native-chat): one entry per subagent, derived lookups, ACP end stamped in its session-end write

Review round 1: a resumed child keeps its one entry and reopens it where it is
listed (the chat draws one section per child, anchored at the first row naming
it), every lookup is read from the rows, the no-turn row is never released,
and ACP's session end stamps its live children inside the session-end write
instead of writing rows ahead of it.

* fix(native-chat): derive the no-turn group, end a foreground run with the turn that started it

Review round 2: the row no turn owns is recognised from its id (a stored flag
was lost when an earlier run's row was first reached through one of its
children), and a turn's end settles the foreground runs it started wherever
their child is listed.

* fix(native-chat): count an inherited subagent row as settled history when a lookup loads it

A read-only lookup could load an earlier run's settled row that retention never saw, so it was never
released and a fresh settled row was released in its place.
2026-10-11 13:04:04 -07:00

709 lines
26 KiB
TypeScript

// Codex's default multi-agent mode announces a helper only by the `collabAgentToolCall` items that
// spawn, message, wait on or close it. These frames, through the real adapter, must register that
// helper the same way a `subAgentActivity` does: one child in the strip, the host's records and
// the roster row.
import { describe, expect, it } from 'vitest'
import type { AgentJournalItemBody } from '../../shared/agent-session-journal-types'
import { createAgentChildWorkAdmission } from '../../shared/agent-status-child-work-admission'
import type { AgentChildWorkRecord } from '../../shared/agent-status-child-work'
import { reconcileAgentChildWorkEvidence } from '../../shared/agent-status-child-work-reconciliation'
import { createAgentStatusStore } from '../../shared/agent-status-store'
import { makeStructuredAgentStatusSubject } from '../../shared/agent-status-subject'
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import { fakeCodex, identityFor, THREAD_ID } from './codex-structured-session-adapter-fixture'
import { CodexStructuredSessionAdapter } from './codex-structured-session-adapter'
import { CodexBackgroundTaskTracker } from './codex-background-task-tracker'
import { createCodexJournalTranslator } from './codex-structured-journal-translation'
import { CodexSubagentExecutions } from './codex-subagent-executions'
import { readCodexSubagentAnnouncements } from './codex-subagent-activity'
import type { CodexThreadItem } from './codex-thread-item-identity'
const parent = makeStructuredAgentStatusSubject(
{
executionHostId: 'local',
wslDistro: null,
workspaceId: 'workspace-1',
workspaceKind: 'folder'
},
'session-1'
)
// The spawn and wait items below are verbatim from a live default-mode session (codex-cli
// 0.155.0-alpha.9.2, `multi_agent` on, `multi_agent_v2` off); only the sender is the fixture's
// thread. That session sent no `subAgentActivity` at all.
const HELPER = '01a0d114-2b8e-73e2-a69b-8e86df067ae9'
const PARENT_TURN = '01a0d112-6f86-7092-8c87-b8618f259efa'
const HELPER_TURN = 'helper-turn-1'
const PROMPT =
'Run the shell command exactly: `sleep 45; echo CHILD_DONE`. After it completes, reply with the single word `CHILD_REPLY` and nothing else.'
/** The prompt's head, as the helper's row names it. */
const LABEL = 'Run the shell command exactly: `sleep 45; echo CHILD_DONE`. After it completes,…'
const COMMAND = "/bin/zsh -lc 'sleep 45; echo CHILD_DONE'"
type Frame = { method: string; params: Record<string, unknown> }
const turn = (
method: 'turn/started' | 'turn/completed',
threadId: string,
id: string,
status = 'completed'
): Frame => ({ method, params: { threadId, turn: { id, status } } })
const item = (
method: 'item/started' | 'item/completed',
threadId: string,
turnId: string,
fields: Record<string, unknown>
): Frame => ({ method, params: { threadId, turnId, item: fields } })
const collab = (
method: 'item/started' | 'item/completed',
fields: Record<string, unknown>
): Frame =>
item(method, THREAD_ID, PARENT_TURN, {
type: 'collabAgentToolCall',
senderThreadId: THREAD_ID,
prompt: null,
model: null,
reasoningEffort: null,
agentsStates: {},
...fields
})
const spawnStarted = collab('item/started', {
id: 'call_dxoSnQY1cHVswlN8MGFJBxHt',
tool: 'spawnAgent',
status: 'inProgress',
receiverThreadIds: [],
prompt: PROMPT,
model: '',
reasoningEffort: 'medium'
})
const spawnCompleted = collab('item/completed', {
id: 'call_dxoSnQY1cHVswlN8MGFJBxHt',
tool: 'spawnAgent',
status: 'completed',
receiverThreadIds: [HELPER],
prompt: PROMPT,
model: 'gpt-5.5',
reasoningEffort: 'medium',
agentsStates: { [HELPER]: { status: 'pendingInit', message: null } }
})
const waitStarted = collab('item/started', {
id: 'call_rMn9MIAPavyhOD6ifjH38A8E',
tool: 'wait',
status: 'inProgress',
receiverThreadIds: [HELPER]
})
const waitCompleted = collab('item/completed', {
id: 'call_rMn9MIAPavyhOD6ifjH38A8E',
tool: 'wait',
status: 'completed',
receiverThreadIds: [HELPER],
agentsStates: { [HELPER]: { status: 'completed', message: 'CHILD_REPLY' } }
})
const closeCompleted = collab('item/completed', {
id: 'call-close',
tool: 'closeAgent',
status: 'completed',
receiverThreadIds: [HELPER],
agentsStates: { [HELPER]: { status: 'shutdown', message: null } }
})
const helperShell = (method: 'item/started' | 'item/completed'): Frame =>
item(method, HELPER, HELPER_TURN, {
type: 'commandExecution',
id: 'call_3iUPkSEtwkUYstngZm5CNJ52',
command: COMMAND,
cwd: '/work/repo',
// As the live session reported it; a command is tracked the same whatever its source.
source: 'unifiedExecStartup',
status: method === 'item/started' ? 'inProgress' : 'completed',
...(method === 'item/completed' ? { exitCode: 0, aggregatedOutput: 'CHILD_DONE\n' } : {})
})
const helperReply = item('item/completed', HELPER, HELPER_TURN, {
type: 'agentMessage',
id: 'msg-child',
text: 'CHILD_REPLY'
})
const activityStarted: Frame = item('item/started', THREAD_ID, PARENT_TURN, {
type: 'subAgentActivity',
id: 'activity-helper',
kind: 'started',
agentThreadId: HELPER,
agentPath: '/root/sleeper'
})
async function session() {
const codex = fakeCodex()
const store = createAgentStatusStore({ epoch: 'epoch-1', mode: 'authority' })
expect(store.applyMutation({ parent: { subject: parent } })).not.toBeNull()
let minted = 0
const admission = createAgentChildWorkAdmission(store, {
mintChildWorkId: () => `child-${++minted}`
})
const adapter = new CodexStructuredSessionAdapter({
resolveLaunch: async () => ({
command: 'codex',
args: ['app-server'],
cwd: '/work/repo',
codexHome: null,
resumeThreadId: null
}),
openConnection: codex.openConnection,
readProcessStartTime: async () => 1_700_000_000_000,
now: () => 1_700_000_000_500,
onChildWorkEvidence: (_sessionId, evidence) =>
reconcileAgentChildWorkEvidence({ store, admission, parent, provider: 'codex', evidence })
})
// The latest revision of each journal row, in first-written order.
const rows = new Map<string, AgentJournalItemBody>()
const journal: StructuredAgentSessionEventSink = {
appendItem: (identity, body) => rows.set(JSON.stringify(identity), body),
appendTombstone: () => {},
publish: () => {}
}
await adapter.acquire({
identity: identityFor('session-1'),
fence: 7,
spawnToken: 'spawn-9',
events: journal
})
const send = (...frames: Frame[]): void => {
for (const frame of frames) {
codex.connections[0]!.handlers.onNotification?.(frame.method, frame.params)
}
}
const agents = (): AgentChildWorkRecord[] =>
store.getChildren(parent).filter((record) => record.kind === 'agent')
const commands = (): AgentChildWorkRecord[] =>
store.getChildren(parent).filter((record) => record.kind === 'command')
const strip = () => adapter.backgroundTaskState('session-1')?.tasks ?? []
const toolRow = (name: string) =>
[...rows.values()].find((body) => body.kind === 'tool-call' && body.name === name)
const rosterRows = () =>
[...rows.values()].flatMap((body) =>
body.kind === 'message'
? body.blocks.flatMap((block) => (block.type === 'subagent-group' ? [block] : []))
: []
)
const rawCollabRows = () =>
[...rows.values()].filter(
(body) => body.kind === 'status' && body.providerFrame?.kind === 'item:collabAgentToolCall'
)
return { send, agents, commands, strip, toolRow, rosterRows, rawCollabRows }
}
describe('Codex default-mode helpers', () => {
it('registers a helper announced only by its spawn call, in the strip, the records and the roster row', async () => {
const run = await session()
run.send(turn('turn/started', THREAD_ID, PARENT_TURN), spawnStarted)
// The call has not said which thread the helper is yet.
expect(run.agents()).toEqual([])
expect(run.strip()).toEqual([])
run.send(spawnCompleted, turn('turn/started', HELPER, HELPER_TURN), waitStarted)
expect(run.agents()).toEqual([
expect.objectContaining({
membership: 'live',
state: 'working',
description: LABEL,
invocation: { invocationId: HELPER_TURN, generation: 1 }
})
])
expect(run.strip()).toEqual([
{ id: `codex-agent:${HELPER}`, kind: 'agent', description: LABEL }
])
expect(run.rosterRows()).toEqual([
expect.objectContaining({
agents: [expect.objectContaining({ id: HELPER, label: LABEL, state: 'working' })]
})
])
// While the helper runs, its command is the helper's work, not the session's own.
run.send(helperShell('item/started'))
expect(run.strip()).toEqual([
{ id: `codex-agent:${HELPER}`, kind: 'agent', description: LABEL }
])
expect(run.commands()).toEqual([
expect.objectContaining({
membership: 'live',
description: COMMAND,
parentChildWorkId: run.agents()[0]?.childWorkId
})
])
expect(run.agents()[0]?.operation).toMatchObject({ toolName: 'Bash', input: COMMAND })
run.send(helperShell('item/completed'))
// A finished command leaves no record behind.
expect(run.commands()).toEqual([])
run.send(helperReply, turn('turn/completed', HELPER, HELPER_TURN))
expect(run.agents()).toEqual([
expect.objectContaining({
membership: 'settled',
outcome: 'succeeded',
lastMessage: 'CHILD_REPLY'
})
])
expect(run.strip()).toEqual([])
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, state: 'completed' })
])
})
it('labels a command the helper leaves running with the helper, once its turn is over', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
helperShell('item/started'),
turn('turn/completed', HELPER, HELPER_TURN)
)
expect(run.strip()).toEqual([
expect.objectContaining({ kind: 'command', description: `${LABEL} — ${COMMAND}` })
])
})
it('renders each collab call as a tool row naming its helper, never as a raw provider row', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnStarted,
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
waitStarted
)
expect(run.toolRow('wait_agent')).toMatchObject({
state: 'running',
input: { description: LABEL, agents: [HELPER] }
})
run.send(turn('turn/completed', HELPER, HELPER_TURN), waitCompleted)
expect(run.toolRow('spawn_agent')).toMatchObject({
state: 'completed',
input: { description: LABEL, prompt: PROMPT, model: 'gpt-5.5', agents: [HELPER] }
})
expect(run.toolRow('wait_agent')).toMatchObject({
state: 'completed',
input: { description: LABEL },
output: expect.objectContaining({ head: 'CHILD_REPLY' })
})
expect(run.rawCollabRows()).toEqual([])
})
it('keeps naming the helper when the turn ends with its wait still open', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
waitStarted,
turn('turn/completed', THREAD_ID, PARENT_TURN, 'interrupted')
)
expect(run.toolRow('wait_agent')).toMatchObject({
state: 'failed',
input: { description: LABEL }
})
})
it('registers the helper whichever order its first turn and its spawn arrive in', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
turn('turn/started', HELPER, HELPER_TURN),
spawnCompleted
)
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'live', state: 'working', description: LABEL })
])
expect(run.strip()).toHaveLength(1)
expect(run.rosterRows()).toEqual([
expect.objectContaining({
agents: [expect.objectContaining({ id: HELPER, label: LABEL, state: 'working' })]
})
])
// The roster row does not stand in for the call's own row.
expect(run.toolRow('spawn_agent')).toMatchObject({
state: 'completed',
input: { description: LABEL }
})
})
it('registers a helper whose spawn call failed but created its thread, as Codex reports an errored one', async () => {
const run = await session()
// The shape Codex builds for a helper that errored at birth: the call fails, yet names the
// thread it created, and that thread can still run.
const spawnFailedWithThread = collab('item/completed', {
id: 'call-failed-spawn',
tool: 'spawnAgent',
status: 'failed',
receiverThreadIds: [HELPER],
prompt: PROMPT,
model: 'gpt-5.5',
reasoningEffort: 'medium',
agentsStates: { [HELPER]: { status: 'errored', message: null } }
})
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnFailedWithThread,
turn('turn/started', HELPER, HELPER_TURN),
helperShell('item/started')
)
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'live', state: 'working', description: LABEL })
])
// Its command is the helper's, not a bare command of the session's own.
expect(run.strip()).toEqual([
{ id: `codex-agent:${HELPER}`, kind: 'agent', description: LABEL }
])
expect(run.rosterRows()).toEqual([
expect.objectContaining({
agents: [expect.objectContaining({ id: HELPER, state: 'working' })]
})
])
})
it('registers nothing for a spawn that created no thread', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
collab('item/completed', {
id: 'call-refused-spawn',
tool: 'spawnAgent',
status: 'failed',
receiverThreadIds: [],
prompt: PROMPT
})
)
expect(run.agents()).toEqual([])
expect(run.strip()).toEqual([])
expect(run.rosterRows()).toEqual([])
})
it('leaves a helper running when its caller failed to close it', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
collab('item/completed', {
id: 'call-close-failed',
tool: 'closeAgent',
status: 'failed',
receiverThreadIds: [HELPER],
agentsStates: { [HELPER]: { status: 'notFound', message: null } }
})
)
expect(run.agents()).toEqual([expect.objectContaining({ membership: 'live' })])
expect(run.strip()).toHaveLength(1)
})
it("ends a closed helper only on its own interrupted turn, never on the caller's close item", async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
// What Codex reports for a close that failed on a running helper: the status is the
// helper's, not the close's, so `completed` says nothing about whether it stopped.
collab('item/completed', {
id: 'call-close-errored',
tool: 'closeAgent',
status: 'completed',
receiverThreadIds: [HELPER],
agentsStates: { [HELPER]: { status: 'running', message: null } }
})
)
expect(run.agents()).toEqual([expect.objectContaining({ membership: 'live' })])
expect(run.strip()).toHaveLength(1)
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, state: 'working' })
])
// A close that works aborts the helper's turn, which Codex reports on the helper's thread.
run.send(closeCompleted, turn('turn/completed', HELPER, HELPER_TURN, 'interrupted'))
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'settled', outcome: 'cancelled' })
])
expect(run.strip()).toEqual([])
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, state: 'stopped' })
])
expect(run.toolRow('close_agent')).toMatchObject({
state: 'completed',
input: { description: LABEL }
})
})
it("keeps a closed helper's running command until Codex reports the killed process's exit", async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
helperShell('item/started'),
turn('turn/completed', HELPER, HELPER_TURN),
closeCompleted
)
// The close shuts the helper's thread down and kills its processes; each still reports its
// exit on the helper's thread, and that exit is what ends the command.
expect(run.commands()).toEqual([expect.objectContaining({ membership: 'live' })])
run.send(helperShell('item/completed'))
expect(run.commands()).toEqual([])
expect(run.strip()).toEqual([])
})
it('leaves a finished helper finished when its caller closes it', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
turn('turn/completed', HELPER, HELPER_TURN),
closeCompleted
)
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'settled', outcome: 'succeeded' })
])
})
it('keeps one child for a session that announces the helper both ways', async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
activityStarted,
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
{ ...activityStarted, method: 'item/completed' }
)
// The activity landed first, so its task name labels the one child.
expect(run.agents()).toEqual([expect.objectContaining({ description: 'sleeper' })])
expect(run.strip()).toEqual([
{ id: `codex-agent:${HELPER}`, kind: 'agent', description: 'sleeper' }
])
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, label: 'sleeper' })
])
})
})
describe('a helper whose spawn was never seen', () => {
const sendInput = collab('item/completed', {
id: 'call-send-input',
tool: 'sendInput',
status: 'completed',
receiverThreadIds: [HELPER],
prompt: 'Now run it again.',
agentsStates: { [HELPER]: { status: 'running', message: null } }
})
it("registers from any call that names it, so its shell is no longer the session's own", async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
turn('turn/started', HELPER, HELPER_TURN),
helperShell('item/started')
)
expect(run.strip()).toEqual([])
expect(run.commands()).toEqual([
expect.objectContaining({ membership: 'live', residency: 'foreground' })
])
run.send(sendInput)
// No spawn named it, so it reads as any unnamed subagent does.
expect(run.strip()).toEqual([{ id: `codex-agent:${HELPER}`, kind: 'agent' }])
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'live', state: 'working' })
])
expect(run.commands()).toEqual([
expect.objectContaining({
membership: 'live',
parentChildWorkId: run.agents()[0]?.childWorkId
})
])
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, label: 'subagent', state: 'working' })
])
expect(run.toolRow('send_input')).toMatchObject({ input: { description: HELPER } })
})
it('registers each helper a call on several names', async () => {
const second = 'helper-two'
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
turn('turn/started', HELPER, HELPER_TURN),
turn('turn/started', second, 'helper-two-turn'),
collab('item/started', {
id: 'call-wait-both',
tool: 'wait',
status: 'inProgress',
receiverThreadIds: [HELPER, second]
})
)
expect(run.strip()).toEqual([
{ id: `codex-agent:${HELPER}`, kind: 'agent' },
{ id: `codex-agent:${second}`, kind: 'agent' }
])
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'live' }),
expect.objectContaining({ membership: 'live' })
])
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, label: 'subagent' }),
expect.objectContaining({ id: second, label: 'subagent 2' })
])
})
it("reopens a helper's one entry when a later turn sends it work, even after that turn ended", async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
spawnCompleted,
turn('turn/started', HELPER, HELPER_TURN),
turn('turn/completed', HELPER, HELPER_TURN),
turn('turn/completed', THREAD_ID, PARENT_TURN),
turn('turn/started', THREAD_ID, 'parent-turn-2'),
{ ...sendInput, params: { ...sendInput.params, turnId: 'parent-turn-2' } },
turn('turn/completed', THREAD_ID, 'parent-turn-2'),
turn('turn/started', HELPER, 'helper-turn-2')
)
// One entry per helper: a second row naming it would show it without its own section.
const groups = new Set(run.rosterRows().map((row) => row.groupId))
expect(groups.size).toBe(1)
expect(run.rosterRows().at(-1)?.agents).toEqual([
expect.objectContaining({ id: HELPER, state: 'working' })
])
})
it('registers every helper a call names, and none it reports not found', () => {
const [second, missing] = ['helper-two', 'helper-missing']
const waited: CodexThreadItem = {
type: 'collabAgentToolCall',
id: 'call-wait-many',
tool: 'wait',
status: 'completed',
receiverThreadIds: [HELPER, second, missing],
agentsStates: {
[HELPER]: { status: 'completed', message: 'done' },
[second]: { status: 'running', message: null },
[missing]: { status: 'notFound', message: null }
}
}
expect(readCodexSubagentAnnouncements(waited, THREAD_ID)).toEqual([
{ agentThreadId: HELPER, label: null, namesParentTurn: false, spawned: false },
{ agentThreadId: second, label: null, namesParentTurn: false, spawned: false }
])
})
})
describe('the roster row follows a helper whose turn ends', () => {
const lastRow = (run: Awaited<ReturnType<typeof session>>) => run.rosterRows().at(-1)?.agents
const running = async () => {
const run = await session()
run.send(
turn('turn/started', THREAD_ID, PARENT_TURN),
activityStarted,
turn('turn/started', HELPER, HELPER_TURN)
)
expect(lastRow(run)).toEqual([expect.objectContaining({ id: HELPER, state: 'working' })])
return run
}
it('settles the row with the strip and the record on the failed completion that follows a fatal error', async () => {
const run = await running()
// Codex sends the fatal error, then the helper's failed `turn/completed` 0-32 ms later.
run.send({
method: 'error',
params: {
threadId: HELPER,
turnId: HELPER_TURN,
willRetry: false,
error: { message: 'Selected model is at capacity.', codexErrorInfo: 'serverOverloaded' }
}
})
expect(run.strip()).toEqual([expect.objectContaining({ id: `codex-agent:${HELPER}` })])
expect(run.agents()).toEqual([expect.objectContaining({ membership: 'live' })])
expect(lastRow(run)).toEqual([expect.objectContaining({ id: HELPER, state: 'working' })])
run.send(turn('turn/completed', HELPER, HELPER_TURN, 'failed'))
expect(run.strip()).toEqual([])
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'settled', outcome: 'failed' })
])
expect(lastRow(run)).toEqual([expect.objectContaining({ id: HELPER, state: 'failed' })])
})
it('settles the row with the strip and the record when its thread closes with no turn/completed', async () => {
const run = await running()
run.send({ method: 'thread/closed', params: { threadId: HELPER } })
expect(run.strip()).toEqual([])
expect(run.agents()).toEqual([
expect.objectContaining({ membership: 'settled', outcome: 'unknown' })
])
expect(lastRow(run)).toEqual([expect.objectContaining({ id: HELPER, state: 'unverifiable' })])
})
})
describe('a restored thread', () => {
function restored(replayed = [spawnCompleted, waitCompleted]) {
const executions = new CodexSubagentExecutions()
const rows = new Map<string, AgentJournalItemBody>()
const translator = createCodexJournalTranslator({
sink: {
appendItem: (identity, body) => rows.set(JSON.stringify(identity), body),
appendTombstone: () => {},
publish: () => {}
},
primaryThreadId: () => THREAD_ID,
subagentExecutions: executions,
schedule: (run: () => void) => {
run()
return () => {}
}
})
const tracker = new CodexBackgroundTaskTracker(THREAD_ID, executions)
const admission = translator.restoreThread(THREAD_ID, {
turns: [{ id: PARENT_TURN, items: replayed.map((frame) => frame.params.item) }]
})
expect(admission).toEqual({ accepted: true })
const bodies = () => [...rows.values()]
return { executions, tracker, bodies }
}
it('names the helper on a replayed call row, as the live row did', () => {
const { bodies } = restored()
expect(bodies()).toContainEqual(
expect.objectContaining({
kind: 'tool-call',
name: 'wait_agent',
input: expect.objectContaining({ description: LABEL })
})
)
})
it('claims no running helper for a name it learned from history', () => {
const { executions, tracker, bodies } = restored()
expect(executions.workingChildren()).toEqual([])
// A live frame republishes the strip and drains evidence: neither holds the helper.
tracker.observe({
method: 'turn/started',
threadId: THREAD_ID,
params: { threadId: THREAD_ID, turn: { id: 'next-turn', status: 'inProgress' } }
})
expect(tracker.state).toBeNull()
expect(tracker.drainChildWorkEvidence(1)).toEqual([])
expect(
bodies().filter(
(body) =>
body.kind === 'message' && body.blocks.some((block) => block.type === 'subagent-group')
)
).toEqual([])
})
it('knows a helper whose spawn history compacted away, from a call on it', () => {
const { tracker } = restored([waitCompleted])
tracker.observe({ ...turn('turn/started', HELPER, HELPER_TURN), threadId: HELPER })
expect(tracker.state?.tasks).toEqual([{ id: `codex-agent:${HELPER}`, kind: 'agent' }])
})
})