mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
fix(agent-status): track inventory children additively and fail closed on truncated inventories
Review findings round 6: (1) veto-only left the vetoing child untracked, so its own later drain was start-less-suppressed and the pane stuck on working — fold running non-teammate inventory entries additively (inventoryComplete: false; no reap, so a just-parked unconfirmed teammate row survives). (2) The agent-task parser caps at 32 entries; a running child past the cap is invisible, so a truncated inventory now claims neither done nor working. The raw-array shell/cron veto stays uncapped.
This commit is contained in:
@@ -3192,9 +3192,36 @@ describe('shared agent-hook-listener', () => {
|
||||
{ id: 'a2', type: 'subagent', status: 'running', agent_type: 'general-purpose' }
|
||||
]
|
||||
})
|
||||
// Why: veto only — a2's row materializes from its own events or the next
|
||||
// lead Stop's fold; the stop must merely not retire the pane.
|
||||
expect(stopped?.payload.state).toBe('working')
|
||||
// Why: the additive fold must TRACK a2 — otherwise its own later stop is
|
||||
// start-less-suppressed and the pane sticks on working forever.
|
||||
expect(stopped?.payload.subagents).toEqual([expect.objectContaining({ id: 'a2' })])
|
||||
|
||||
const drained = claudeEvent({ hook_event_name: 'SubagentStop', agent_id: 'a2' })
|
||||
expect(drained?.payload.state).toBe('done')
|
||||
expect(drained?.payload.subagents).toBeUndefined()
|
||||
})
|
||||
|
||||
it('claims nothing from a no-lead removal whose agent inventory is truncated', () => {
|
||||
claudeEvent({
|
||||
hook_event_name: 'SubagentStart',
|
||||
agent_id: 'a1',
|
||||
agent_type: 'general-purpose'
|
||||
})
|
||||
// Why: 32 teammate rows fill the parser cap; the running subagent behind
|
||||
// them is invisible, so the inventory can prove neither done nor working.
|
||||
const cappedTasks = Array.from({ length: 32 }, (_, i) => ({
|
||||
id: `t${i}`,
|
||||
type: 'teammate',
|
||||
status: 'running'
|
||||
}))
|
||||
cappedTasks.push({ id: 'hidden-live', type: 'subagent', status: 'running' })
|
||||
const stopped = claudeEvent({
|
||||
hook_event_name: 'SubagentStop',
|
||||
agent_id: 'a1',
|
||||
background_tasks: cappedTasks
|
||||
})
|
||||
expect(stopped).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps working when a no-lead drain leaves a live shell in the inventory', () => {
|
||||
|
||||
@@ -2465,6 +2465,22 @@ function normalizeClaudeSubagentLifecycleEvent(
|
||||
stopClaudeSubagent(roster, agentId)
|
||||
removedRow = hadRow && !roster.has(agentId)
|
||||
}
|
||||
// Why: the stop's inventory can list children this listener never saw start (post-restart);
|
||||
// tracking them keeps their own later drains resolvable instead of start-less-suppressed.
|
||||
// Additive only — a complete-inventory reap here would kill a just-parked teammate row
|
||||
// before its TeammateIdle confirmation arrives.
|
||||
const inventory = readClaudeBackgroundAgentTasks(hookPayload)
|
||||
if (
|
||||
inventory.present &&
|
||||
(roster || inventory.tasks.some((task) => task.running && !task.teammate))
|
||||
) {
|
||||
foldClaudeBackgroundTasksIntoRoster(
|
||||
getOrCreateClaudeSubagentRoster(state, paneKey),
|
||||
inventory.tasks,
|
||||
Date.now(),
|
||||
{ inventoryComplete: false }
|
||||
)
|
||||
}
|
||||
// Why: a blocked child that dies without another tool event would pin its permission/question wait on the pane forever — nothing else references that agent again.
|
||||
clearClaudePendingWaitForAgent(state, paneKey, (waitingAgentId) => waitingAgentId === agentId)
|
||||
}
|
||||
@@ -2594,16 +2610,22 @@ function buildClaudeChildDrivenStatusPayload(
|
||||
})
|
||||
}
|
||||
if (evidence.removedRow) {
|
||||
// Why: the stop's own inventory can list work this listener never saw start (post-restart);
|
||||
// a running non-teammate task or live shell/cron vetoes done. Teammate-typed entries prove
|
||||
// nothing ("running" even while idle), and full folding here would reap a just-parked
|
||||
// teammate row before its TeammateIdle confirmation arrives — veto only, no reconcile.
|
||||
// Why: live shells/crons veto done (the raw-array check is uncapped). A truncated agent-task
|
||||
// inventory is incomplete evidence — a running child past the parser cap would be invisible —
|
||||
// so it claims neither done nor working. Running non-teammate tasks were already folded into
|
||||
// the roster above and surface through the working branch.
|
||||
if (hasActiveClaudeNonAgentBackgroundWork(hookPayload)) {
|
||||
return buildClaudeStatusPayload(state, eventName, '', paneKey, hookPayload, {
|
||||
stateName: 'working',
|
||||
updateToolSnapshot: false
|
||||
})
|
||||
}
|
||||
const inventory = readClaudeBackgroundAgentTasks(hookPayload)
|
||||
const inventoryShowsLiveWork =
|
||||
inventory.tasks.some((task) => task.running && !task.teammate) ||
|
||||
hasActiveClaudeNonAgentBackgroundWork(hookPayload)
|
||||
if (inventory.present && inventory.truncated) {
|
||||
return null
|
||||
}
|
||||
return buildClaudeStatusPayload(state, eventName, '', paneKey, hookPayload, {
|
||||
stateName: inventoryShowsLiveWork ? 'working' : 'done',
|
||||
stateName: 'done',
|
||||
updateToolSnapshot: false
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user