Files
orca/src/shared/program-status/agent-status-projector.test.ts
T
Neil d5ee28d716 fix(program-status): keep a Pi row working while a pi-subagents child outlives the parent's done (#33738)
* fix(program-status): keep a Pi row working while a pi-subagents child outlives the parent's done

OSC 7501 projected the root record's done straight onto the row even when
live child records remained, so a Pi parent showed Done/IDLE while an async
pi-subagents worker kept running. Fold child liveness with the existing
foldMainAgentWithRowChildWork; mainAgent keeps the lead's own verdict so the
row settles to done once the last child ends.

Fixes #33657

* fix(program-status): announce a held turn's done on time and keep hook verdicts authoritative

- Stamp turnCompletedAt on the child-held row and its all-clear so the lead's
  done is announced once, when it happens, as hook rows do.
- Hold only a finished (non-boundary) turn, only for working/waiting children;
  in-turn rows and failed/idle children keep the root's state.
- A settled hook row is never reopened by a program's child-held working row,
  so the 2-minute silence fallback cannot flip done -> working -> done.

* fix(program-status): lapse an unrefreshed child hold to the lead's done

Children report only changes, so a pi-subagents that dies or hangs inside a
live Pi would hold the row working until decay. The lease timer now settles a
child-held row no report refreshed within AGENT_STATUS_STALE_AFTER_MS minus one
renewal interval (20-30 min) to the lead's done, keeping turnCompletedAt so it
is not announced again.
2026-10-25 20:24:34 -07:00

319 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import {
createProgramStatusProjector,
lapsedProgramStatusChildHold
} from './agent-status-projector'
const b64 = (text: string): string => Buffer.from(text, 'utf8').toString('base64')
const osc = (body: string): string => `\x1b]7501;${body}\x1b\\`
function projector() {
let clock = 1000
const instance = createProgramStatusProjector(() => clock)
return {
feed: (data: string) => instance.process(data).payloads.map(({ payload }) => payload),
tick: (ms: number) => {
clock += ms
}
}
}
describe('createProgramStatusProjector', () => {
it('maps a Claude Code turn onto Orca agent states', () => {
const { feed, tick } = projector()
expect(feed(osc('state=idle:app=claude-code'))).toEqual([
{
state: 'done',
prompt: '',
agentType: 'claude',
sessionBoundary: true,
mainAgent: { state: 'done', stateStartedAt: 1000 }
}
])
tick(5)
expect(feed(osc(`state=working:app=claude-code:msg=${b64('Fixing the parser')}`))).toEqual([
{
state: 'working',
prompt: '',
agentType: 'claude',
toolInput: 'Fixing the parser',
mainAgent: { state: 'working', stateStartedAt: 1005 }
}
])
tick(5)
expect(
feed(
osc(
`state=blocked:kind=permission:app=claude-code:msg=${b64('approve Bash: rm -rf build')}`
)
)
).toMatchObject([{ state: 'waiting', toolInput: 'approve Bash: rm -rf build' }])
tick(5)
expect(feed(osc(`state=done:app=claude-code:msg=${b64('Parser fixed')}`))).toEqual([
{
state: 'done',
prompt: '',
agentType: 'claude',
lastAssistantMessage: 'Parser fixed',
mainAgent: { state: 'done', outcome: 'success', stateStartedAt: 1015 }
}
])
})
it('reports auth and unnamed blocks as blocked, errors as failed turns', () => {
const { feed } = projector()
expect(feed(osc('state=blocked:kind=auth:app=pi'))).toMatchObject([
{ state: 'blocked', agentType: 'pi' }
])
expect(feed(osc(`state=error:app=pi:msg=${b64('rate limited')}`))).toMatchObject([
{ state: 'done', lastAssistantMessage: 'rate limited', mainAgent: { outcome: 'failure' } }
])
})
it('reads idle as at rest, never as a cancelled turn, and keeps an unseen done', () => {
const { feed } = projector()
// Codex boots with a short working blip before settling idle.
feed(osc('state=idle:app=codex'))
feed(osc('state=working:app=codex'))
const [settled] = feed(osc('state=idle:app=codex'))
expect(settled).toMatchObject({ state: 'done', sessionBoundary: true })
expect(settled.interrupted).toBeUndefined()
expect(settled.mainAgent?.outcome).toBeUndefined()
feed(osc('state=working:app=codex'))
feed(osc('state=done:app=codex'))
expect(feed(osc('state=idle:app=codex'))).toEqual([])
})
it('emits only changes, so redraws that resend a report are silent', () => {
const { feed } = projector()
expect(feed(osc('state=working:app=omp'))).toHaveLength(1)
expect(feed(osc('state=working:app=omp') + osc('state=working:app=omp'))).toEqual([])
})
it('ends a live program at the next shell prompt but keeps a finished one', () => {
const { feed } = projector()
feed(osc('state=working:app=pi'))
expect(feed('\x1b]133;A\x07')).toMatchObject([{ state: 'done', sessionBoundary: true }])
feed(osc('state=done:app=pi'))
expect(feed('\x1b]133;A\x07')).toEqual([])
})
it('turns child records into subagent rows with inherited app and sanitized titles', () => {
const { feed, tick } = projector()
feed(osc('state=working:app=claude-code'))
tick(10)
expect(feed(osc(`state=working:id=task_1:title=${b64('‮Explore the repo')}`))).toMatchObject([
{
state: 'working',
subagents: [
{
id: 'task_1',
state: 'working',
agentType: 'claude',
description: 'Explore the repo',
startedAt: 1010
}
]
}
])
expect(feed(osc('state=blocked:kind=question:id=task_1'))).toMatchObject([
{ subagents: [{ id: 'task_1', state: 'waiting', startedAt: 1010 }] }
])
const [cleared] = feed(osc('state=clear:id=task_1'))
expect(cleared.subagents).toBeUndefined()
})
it("keeps a finished turn working while a child runs, stamping the turn's own end for both rows", () => {
const { feed, tick } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=subagents/run1:app=pi-subagents'))
tick(10)
expect(feed(osc('state=done:app=pi'))).toMatchObject([
{
state: 'working',
turnCompletedAt: 1010,
mainAgent: { state: 'done', outcome: 'success', stateStartedAt: 1010 },
subagents: [{ id: 'subagents/run1', state: 'working' }]
}
])
tick(10)
const [settled] = feed(osc('state=done:id=subagents/run1:app=pi-subagents'))
expect(settled).toMatchObject({
state: 'done',
turnCompletedAt: 1010,
mainAgent: { state: 'done', outcome: 'success', stateStartedAt: 1010 }
})
expect(settled.subagents).toBeUndefined()
})
it("lapses a hold no child refreshed to the lead's done, keeping its completion stamp", () => {
const { feed } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=subagents/run1'))
const [held] = feed(osc('state=done:app=pi'))
expect(lapsedProgramStatusChildHold(held, 0, 999, 1000)).toBeNull()
const lapsed = lapsedProgramStatusChildHold(held, 0, 1000, 1000)
expect(lapsed).toMatchObject({
state: 'done',
turnCompletedAt: held.turnCompletedAt,
mainAgent: held.mainAgent
})
expect(lapsed?.subagents).toBeUndefined()
const [settled] = feed(osc('state=done:id=subagents/run1'))
expect(lapsedProgramStatusChildHold(settled, 0, 1000, 1000)).toBeNull()
})
it('leaves live turns, failed children and an idle root as their root reports them', () => {
const { feed } = projector()
const last = (data: string) => feed(data).at(-1)
expect(
last(osc('state=working:app=claude-code') + osc('state=blocked:kind=question:id=task'))
).toMatchObject({ state: 'working', subagents: [{ state: 'waiting' }] })
const failedChild = last(osc('state=blocked:id=task') + osc('state=done:app=claude-code'))
expect(failedChild).toMatchObject({ state: 'done', subagents: [{ state: 'blocked' }] })
expect(failedChild?.turnCompletedAt).toBeUndefined()
const resumed = projector()
expect(
resumed.feed(osc('state=idle:app=pi') + osc('state=working:id=subagents/run1')).at(-1)
).toMatchObject({ state: 'done', sessionBoundary: true })
})
it('reads a clear during live work as the program leaving, not a cancelled turn', () => {
const { feed } = projector()
feed(osc('state=working:app=claude-code'))
const [left] = feed(osc('state=clear'))
expect(left).toMatchObject({ state: 'done', sessionBoundary: true })
expect(left.interrupted).toBeUndefined()
expect(feed(osc('state=clear'))).toEqual([])
})
it("maps harness app ids onto Orca's agent ids so hook precedence can match them", () => {
const { feed } = projector()
expect(feed(osc('state=working:app=grok-build'))).toMatchObject([{ agentType: 'grok' }])
expect(feed(osc('state=done:app=terraform'))).toMatchObject([{ agentType: 'terraform' }])
})
it('emits the net facet per chunk, including progress the lifecycle payload ignores', () => {
let clock = 1
const { process } = createProgramStatusProjector(() => clock)
expect(process(osc('state=working:app=claude-code:progress=40')).facet).toEqual({
state: 'working',
progress: 40,
observedAt: 1
})
clock = 2
const tick = process(osc('state=working:app=claude-code:progress=55'))
expect(tick.payloads).toEqual([])
expect(tick.facet).toMatchObject({ progress: 55, observedAt: 2 })
expect(process(osc('state=working:app=claude-code:progress=55')).facet).toBeUndefined()
expect(process(osc('state=clear')).facet).toBeNull()
})
it('stays silent on children alone, then lists them once the root reports', () => {
const { feed } = projector()
expect(feed(osc('state=working:id=job'))).toEqual([])
expect(feed(osc('state=working:app=pi'))).toMatchObject([
{ state: 'working', subagents: [{ id: 'job', state: 'working', agentType: 'pi' }] }
])
})
it("keeps a failed turn's verdict when the program settles idle", () => {
const { feed } = projector()
feed(osc('state=working:app=pi'))
expect(feed(osc('state=error:app=pi'))).toMatchObject([{ mainAgent: { outcome: 'failure' } }])
expect(feed(osc('state=idle:app=pi'))).toEqual([])
})
it("reports a child's app change as news", () => {
const { feed } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=job'))
expect(feed(osc('state=working:id=job:app=codex'))).toMatchObject([
{ subagents: [{ id: 'job', agentType: 'codex' }] }
])
})
it('drops a finished row when the program clears it or the terminal resets', () => {
const { feed } = projector()
feed(osc('state=done:app=brew'))
expect(feed(osc('state=clear'))).toMatchObject([{ state: 'done', sessionBoundary: true }])
feed(osc('state=error:app=brew'))
expect(feed('\x1bc')).toMatchObject([{ state: 'done', sessionBoundary: true }])
})
it('reports an identical result from the next invocation as news', () => {
const { feed, tick } = projector()
expect(feed(osc('state=done:app=brew'))).toHaveLength(1)
feed('\x1b]133;A\x07')
tick(10)
expect(feed(osc('state=done:app=brew'))).toMatchObject([
{ state: 'done', mainAgent: { stateStartedAt: 1010 } }
])
})
it("starts the next invocation's idle as a boundary, not the kept result", () => {
const { feed } = projector()
feed(osc('state=done:app=brew'))
feed('\x1b]133;A\x07')
expect(feed(osc('state=idle:app=codex'))).toMatchObject([
{ state: 'done', sessionBoundary: true, agentType: 'codex' }
])
})
it('ends the live root at the prompt while keeping the finished record for display', () => {
const instance = createProgramStatusProjector(() => 0)
instance.process(osc('state=done:app=terraform'))
const atPrompt = instance.process('\x1b]133;A\x07')
expect(atPrompt.rootChanges).toEqual([{ offset: 8, root: undefined }])
expect(atPrompt.facet).toBeUndefined()
expect(instance.process('\x1b]133;A\x07').rootChanges).toEqual([])
})
it('never resolves an app name to an Object.prototype member', () => {
const { feed } = projector()
expect(feed(osc('state=working:app=constructor'))).toMatchObject([{ agentType: 'constructor' }])
expect(feed(osc('state=working:app=toString'))).toMatchObject([{ agentType: 'toString' }])
})
it('keeps long child ids distinct within the snapshot cap, leaf last', () => {
const { feed } = projector()
const path = (head: string) => [head, 'b'.repeat(30), 'c'.repeat(30)].join('/')
feed(osc('state=working:app=pi'))
const [payload] = feed(
`${osc(`state=working:id=${path('a')}`)}${osc(`state=working:id=${path('z')}`)}`
).slice(-1)
const ids = payload.subagents?.map((child) => child.id) ?? []
expect(new Set(ids).size).toBe(2)
expect(ids.every((id) => id.length <= 64 && id.endsWith('c'.repeat(30)))).toBe(true)
})
it('marks a projection whose program left no records as removing its row', () => {
const instance = createProgramStatusProjector(() => 0)
instance.process(osc('state=done:app=brew'))
expect(instance.process(osc('state=clear')).payloads).toMatchObject([{ removesRow: true }])
instance.process(osc('state=idle:app=codex'))
expect(instance.process(osc('state=working:app=codex')).payloads[0]?.removesRow).toBeUndefined()
})
it('keeps a result unchanged when the prompt drops a lingering child', () => {
const { feed, tick } = projector()
feed(osc('state=working:app=pi') + osc('state=working:id=child'))
feed(osc('state=done:app=pi'))
tick(5)
// The child drop at the prompt is not a new completion of the kept result.
expect(feed('\x1b]133;A\x07')).toMatchObject([
{ state: 'done', mainAgent: { stateStartedAt: 1000 } }
])
tick(5)
expect(feed(osc('state=done:app=pi'))).toMatchObject([
{ state: 'done', mainAgent: { stateStartedAt: 1010 } }
])
})
it('removes the row when an idle program clears or the terminal resets', () => {
for (const ending of [osc('state=clear'), '\x1bc']) {
const instance = createProgramStatusProjector(() => 0)
instance.process(osc('state=idle:app=brew'))
expect(instance.process(ending).payloads).toMatchObject([{ removesRow: true }])
expect(instance.process(ending).payloads).toEqual([])
}
})
})