mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
* feat(agent-status): combine Codex child work through the shared main-agent status fold * docs(agent-status): correct two comments the waiting child-work arm made stale A child failure reported in place as `blocked` now pins the row `waiting`, not `working`; and no relay ever sent an unfolded `working` beside a waiting child. * fix(agent-status): only a waiting child asks for a human A child's `blocked` state means its task failed (the only producer maps a failed background task to it, and the background-task view labels it "failed"), not that a human must act. Folding it into the waiting arm would surface a failed child as needs-you. It stays live work, as before this series. * docs(agent-status): say a waiting child, not a blocked one, makes the row wait A child's blocked state means it failed; only its waiting state feeds the waiting arm. Two fold comments, a test describe and two parity story names still called the waiting child blocked. * docs(agent-status): name where a child's wait is still lost, and pin the structured lane's real input The doc said the Claude hook lane's rows match Codex and that every lane feeds a child's wait into the fold. Neither holds: Claude keeps the wait in one slot the next main agent event overwrites, the structured lane turns a child's prompt into the main agent's own attention, and Codex drops its roster on a root Stop when it tracks no child transcripts. The parity story now drives the structured lane with the input it actually receives.
131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
AGENT_MODEL_MAX_LENGTH,
|
|
AGENT_STATUS_MAX_SUBAGENTS,
|
|
AGENT_TYPE_MAX_LENGTH
|
|
} from './agent-status-types'
|
|
import {
|
|
codexRosterChildWorkLiveness,
|
|
codexRosterToSnapshots,
|
|
finishCodexSubagent,
|
|
setCodexSubagentModel,
|
|
upsertCodexSubagent,
|
|
type CodexSubagentRoster
|
|
} from './codex-subagent-roster'
|
|
|
|
describe('Codex subagent roster', () => {
|
|
it('normalizes retained identity fields before storing them', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
|
|
upsertCodexSubagent(
|
|
roster,
|
|
' child-1 ',
|
|
{
|
|
agentType: `reviewer\n${'x'.repeat(AGENT_TYPE_MAX_LENGTH * 2)}`,
|
|
description: 'Review the sidebar lifecycle',
|
|
model: `gpt-model-${'x'.repeat(AGENT_MODEL_MAX_LENGTH * 2)}`,
|
|
state: 'working'
|
|
},
|
|
10
|
|
)
|
|
|
|
const snapshot = codexRosterToSnapshots(roster)?.[0]
|
|
expect([...roster.keys()]).toEqual(['child-1'])
|
|
expect(snapshot?.agentType).toHaveLength(AGENT_TYPE_MAX_LENGTH)
|
|
expect(snapshot?.agentType).not.toContain('\n')
|
|
expect(snapshot?.description).toBe('Review the sidebar lifecycle')
|
|
expect(snapshot?.model).toHaveLength(AGENT_MODEL_MAX_LENGTH)
|
|
|
|
finishCodexSubagent(roster, ' child-1 ')
|
|
expect(roster.size).toBe(0)
|
|
})
|
|
|
|
it('rejects an id that would normalize to an invisible child', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
|
|
upsertCodexSubagent(roster, ' ', { state: 'waiting' }, 10)
|
|
|
|
expect(roster.size).toBe(0)
|
|
})
|
|
|
|
it('bounds live storage while admitting a replacement after one child stops', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
for (let index = 0; index <= AGENT_STATUS_MAX_SUBAGENTS; index += 1) {
|
|
upsertCodexSubagent(roster, `child-${index}`, { state: 'working' }, index)
|
|
}
|
|
|
|
expect(roster.size).toBe(AGENT_STATUS_MAX_SUBAGENTS)
|
|
expect(roster.has(`child-${AGENT_STATUS_MAX_SUBAGENTS}`)).toBe(false)
|
|
|
|
finishCodexSubagent(roster, 'child-0')
|
|
upsertCodexSubagent(roster, 'replacement', { state: 'working' }, 100)
|
|
|
|
expect(roster.size).toBe(AGENT_STATUS_MAX_SUBAGENTS)
|
|
expect(roster.has('replacement')).toBe(true)
|
|
})
|
|
|
|
describe('codexRosterChildWorkLiveness', () => {
|
|
it('reads every child as agent work, a waiting one above the rest, and nothing as a watch loop', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
expect(codexRosterChildWorkLiveness(undefined)).toBeNull()
|
|
expect(codexRosterChildWorkLiveness(roster)).toBeNull()
|
|
upsertCodexSubagent(roster, 'a', { state: 'working' }, 1)
|
|
expect(codexRosterChildWorkLiveness(roster)).toBe('working')
|
|
upsertCodexSubagent(roster, 'b', { state: 'waiting' }, 2)
|
|
expect(codexRosterChildWorkLiveness(roster)).toBe('waiting')
|
|
finishCodexSubagent(roster, 'b')
|
|
expect(codexRosterChildWorkLiveness(roster)).toBe('working')
|
|
})
|
|
})
|
|
|
|
describe('setCodexSubagentModel', () => {
|
|
it('records the model without disturbing the child lifecycle or label', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
upsertCodexSubagent(roster, 'child-1', { description: '/root/audit', state: 'waiting' }, 10)
|
|
|
|
setCodexSubagentModel(roster, 'child-1', ' gpt-5.6-terra ')
|
|
|
|
expect(codexRosterToSnapshots(roster)).toEqual([
|
|
{
|
|
id: 'child-1',
|
|
agentType: undefined,
|
|
description: '/root/audit',
|
|
model: 'gpt-5.6-terra',
|
|
state: 'waiting',
|
|
startedAt: 10
|
|
}
|
|
])
|
|
})
|
|
|
|
it('never creates a row for a child that is no longer tracked', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
upsertCodexSubagent(roster, 'child-1', { state: 'working' }, 10)
|
|
finishCodexSubagent(roster, 'child-1')
|
|
|
|
// A model read racing a completed child must not resurrect its row.
|
|
setCodexSubagentModel(roster, 'child-1', 'gpt-5.6-terra')
|
|
|
|
expect(roster.size).toBe(0)
|
|
})
|
|
|
|
it('keeps a known model when the new value is empty', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
upsertCodexSubagent(roster, 'child-1', { model: 'gpt-5.6-sol', state: 'working' }, 10)
|
|
|
|
setCodexSubagentModel(roster, 'child-1', ' ')
|
|
setCodexSubagentModel(roster, 'child-1', undefined)
|
|
|
|
expect(roster.get('child-1')?.model).toBe('gpt-5.6-sol')
|
|
})
|
|
|
|
it('bounds an oversized model to the shared cap', () => {
|
|
const roster: CodexSubagentRoster = new Map()
|
|
upsertCodexSubagent(roster, 'child-1', { state: 'working' }, 10)
|
|
|
|
setCodexSubagentModel(roster, 'child-1', 'x'.repeat(AGENT_MODEL_MAX_LENGTH + 50))
|
|
|
|
expect(roster.get('child-1')?.model).toHaveLength(AGENT_MODEL_MAX_LENGTH)
|
|
})
|
|
})
|
|
})
|