mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
fix(agent-status): keep reused-id parks from clearing waits and drop untrackable ids everywhere
Review findings round 9: (1) a delayed prior-turn stop for a reused teammate id could park the newer start's working row and ride the wait- clear into done — the clearing done now fires only when the owner has no roster row left; a parked owner stays silent (sweeper-bounded card). (2) Over-length ids no longer consume inventory parser cap slots, so junk cannot fake truncation and veto a legitimate drain. (3) A waiting- inducing child event with an untrackable id mints no card at all.
This commit is contained in:
@@ -3269,9 +3269,11 @@ describe('shared agent-hook-listener', () => {
|
||||
expect(drained?.payload.state).toBe('done')
|
||||
})
|
||||
|
||||
it('clears a fresh-pane teammate wait card with done when its owner parks', () => {
|
||||
// Why: the wait is identity-owned; its owner parking must clear the amber
|
||||
// card instead of leaving it fresh for the stale sweeper.
|
||||
it('stays silent when a fresh-pane teammate wait owner parks with its row still tracked', () => {
|
||||
// Why: a parked owner is indistinguishable from a delayed prior-turn stop
|
||||
// against a newer start of the same reused id — claiming done could retire
|
||||
// newer real work (the #11353 race). The stale amber card is bounded by
|
||||
// the stale sweeper instead.
|
||||
claudeEvent({
|
||||
hook_event_name: 'PermissionRequest',
|
||||
agent_id: 'alane-6d3cb5b52120b7bf',
|
||||
@@ -3282,7 +3284,56 @@ describe('shared agent-hook-listener', () => {
|
||||
hook_event_name: 'SubagentStop',
|
||||
agent_id: 'alane-6d3cb5b52120b7bf'
|
||||
})
|
||||
expect(parked?.payload.state).toBe('done')
|
||||
expect(parked).toBeNull()
|
||||
})
|
||||
|
||||
it('never retires a newer same-id start via a delayed prior-turn stop clearing its wait', () => {
|
||||
claudeEvent({
|
||||
hook_event_name: 'PermissionRequest',
|
||||
agent_id: 'alane-6d3cb5b52120b7bf',
|
||||
agent_type: 'lane',
|
||||
tool_name: 'Bash'
|
||||
})
|
||||
claudeEvent({
|
||||
hook_event_name: 'SubagentStart',
|
||||
agent_id: 'alane-6d3cb5b52120b7bf',
|
||||
agent_type: 'lane'
|
||||
})
|
||||
const staleStop = claudeEvent({
|
||||
hook_event_name: 'SubagentStop',
|
||||
agent_id: 'alane-6d3cb5b52120b7bf'
|
||||
})
|
||||
expect(staleStop?.payload.state ?? null).not.toBe('done')
|
||||
})
|
||||
|
||||
it('resolves a drain to done when over-length ids fill the inventory', () => {
|
||||
// Why: untrackable ids must not consume parser cap slots — 33 of them is
|
||||
// not a truncated inventory, just noise.
|
||||
claudeEvent({
|
||||
hook_event_name: 'SubagentStart',
|
||||
agent_id: 'a1',
|
||||
agent_type: 'general-purpose'
|
||||
})
|
||||
const junk = Array.from({ length: 33 }, (_, i) => ({
|
||||
id: 'x'.repeat(65) + i,
|
||||
type: 'subagent',
|
||||
status: 'running'
|
||||
}))
|
||||
const drained = claudeEvent({
|
||||
hook_event_name: 'SubagentStop',
|
||||
agent_id: 'a1',
|
||||
background_tasks: junk
|
||||
})
|
||||
expect(drained?.payload.state).toBe('done')
|
||||
})
|
||||
|
||||
it('does not mint a waiting card for an untrackable child id', () => {
|
||||
const waiting = claudeEvent({
|
||||
hook_event_name: 'PermissionRequest',
|
||||
agent_id: 'w'.repeat(65),
|
||||
tool_name: 'Bash'
|
||||
})
|
||||
expect(waiting).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves a fresh-pane child permission wait whose child dies to done, not phantom working', () => {
|
||||
|
||||
@@ -2499,9 +2499,19 @@ function normalizeClaudeSubagentLifecycleEvent(
|
||||
)
|
||||
}
|
||||
}
|
||||
// Why: a wait owner that still has a roster row was PARKED, not finished — a delayed stop from
|
||||
// a prior turn can park a newly working reused id, so claiming done there would retire newer
|
||||
// real work (the #11353 race). The clearing done fires only for an owner with no row left;
|
||||
// a parked owner's stale amber card is bounded by the stale sweeper instead.
|
||||
const waitOwnerStillTracked =
|
||||
clearedOwnWait === 'deleted' &&
|
||||
roster !== undefined &&
|
||||
(eventName === 'TeammateIdle'
|
||||
? [...roster.keys()].some((id) => claudeTeammateIdMatchesName(id, lifecycleId))
|
||||
: roster.has(lifecycleId))
|
||||
return buildClaudeChildDrivenStatusPayload(state, eventName, paneKey, hookPayload, {
|
||||
removedRow,
|
||||
clearedOwnWait: clearedOwnWait === 'deleted'
|
||||
clearedOwnWait: clearedOwnWait === 'deleted' && !waitOwnerStillTracked
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2731,6 +2741,11 @@ function normalizeClaudeEvent(
|
||||
// Why: subagent/teammate events carry `agent_id` (lead's don't); child tool activity keeps its row live but must not become the lead's state or overwrite its tool/prompt caches (a live card would vanish).
|
||||
// Two exceptions take the full path below: waiting-inducing events (a child needs human attention on this pane) and the blocked child's own next tool event (approval granted — clear the wait as for the lead).
|
||||
const isWaitingInducing = stateName === 'waiting'
|
||||
// Why: an untrackable child id can neither appear in the roster nor reliably clear its wait —
|
||||
// recording it would mint an unresolvable amber card (STA-2915 review).
|
||||
if (isWaitingInducing && eventAgentId && !isValidClaudeSubagentId(eventAgentId)) {
|
||||
return null
|
||||
}
|
||||
const subagentOriginId =
|
||||
!isWaitingInducing &&
|
||||
(eventName === 'PreToolUse' ||
|
||||
|
||||
@@ -191,7 +191,12 @@ export function readClaudeBackgroundAgentTasks(hookPayload: Record<string, unkno
|
||||
if (obj.type !== 'subagent' && obj.type !== 'teammate') {
|
||||
continue
|
||||
}
|
||||
if (typeof obj.id !== 'string' || obj.id.trim().length === 0) {
|
||||
// Why: an id the upsert rejects can never be tracked — it must not consume a cap slot either.
|
||||
if (
|
||||
typeof obj.id !== 'string' ||
|
||||
obj.id.trim().length === 0 ||
|
||||
!isValidClaudeSubagentId(obj.id)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (tasks.length >= AGENT_STATUS_MAX_SUBAGENTS) {
|
||||
|
||||
Reference in New Issue
Block a user