mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(agent-status): reap only unbacked claims from an empty stop inventory
Review finding round 11: the complete-empty fold cleared live-tracked rows, so a delayed stale stop's empty list could retire a newer live start — the uncorrelated-retirement race again. The empty inventory now reaps only restored/inventory-derived claims not backed by live lifecycle events.
This commit is contained in:
@@ -3350,6 +3350,28 @@ describe('shared agent-hook-listener', () => {
|
||||
expect(stopped?.payload.subagents).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps a newer live start alive through a delayed stale stop with an empty inventory', () => {
|
||||
claudeEvent({
|
||||
hook_event_name: 'SubagentStart',
|
||||
agent_id: 'a-old',
|
||||
agent_type: 'general-purpose'
|
||||
})
|
||||
claudeEvent({
|
||||
hook_event_name: 'SubagentStart',
|
||||
agent_id: 'a-new',
|
||||
agent_type: 'general-purpose'
|
||||
})
|
||||
// Why: a-old's delayed stop reports the empty inventory captured before
|
||||
// a-new existed; clearing a-new from it would retire newer live work.
|
||||
const staleStop = claudeEvent({
|
||||
hook_event_name: 'SubagentStop',
|
||||
agent_id: 'a-old',
|
||||
background_tasks: []
|
||||
})
|
||||
expect(staleStop?.payload.state).toBe('working')
|
||||
expect(staleStop?.payload.subagents).toEqual([expect.objectContaining({ id: 'a-new' })])
|
||||
})
|
||||
|
||||
it('clears restored phantom rows when a no-lead stop carries a complete empty inventory', () => {
|
||||
seedClaudeSubagentRosterFromSnapshots(state, PANE_KEY, [
|
||||
{ id: 'a1', state: 'working', startedAt: 1, agentType: 'general-purpose' },
|
||||
|
||||
@@ -2471,7 +2471,16 @@ function normalizeClaudeSubagentLifecycleEvent(
|
||||
// Additive only — a complete-inventory reap here would kill a just-parked teammate row
|
||||
// before its TeammateIdle confirmation arrives.
|
||||
const inventory = readClaudeBackgroundAgentTasks(hookPayload)
|
||||
if (
|
||||
if (inventory.present && inventory.tasks.length === 0 && roster) {
|
||||
// Why: an empty complete inventory reaps only claims not backed by live lifecycle events
|
||||
// (restored/inventory-derived phantoms). A delayed stale stop's empty list must not clear
|
||||
// a newer live start's row — that is the uncorrelated-retirement race again.
|
||||
for (const [id, tracked] of roster) {
|
||||
if (tracked.restoredFromSnapshot || tracked.backgroundTasksAuthoritative) {
|
||||
roster.delete(id)
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
inventory.present &&
|
||||
(roster ||
|
||||
inventory.tasks.some(
|
||||
@@ -2484,11 +2493,9 @@ function normalizeClaudeSubagentLifecycleEvent(
|
||||
getOrCreateClaudeSubagentRoster(state, paneKey),
|
||||
inventory.tasks,
|
||||
Date.now(),
|
||||
// Why: an empty inventory is complete by construction and proves nothing is left
|
||||
// alive (lead-path rule), clearing restored phantoms; a non-empty one folds
|
||||
// additively — its reap would kill a just-parked teammate row before its
|
||||
// TeammateIdle confirmation arrives.
|
||||
{ inventoryComplete: inventory.tasks.length === 0 }
|
||||
// Why: additive only — a complete-inventory reap here would kill a just-parked
|
||||
// teammate row before its TeammateIdle confirmation arrives.
|
||||
{ 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.
|
||||
|
||||
Reference in New Issue
Block a user