mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
* fix(agent-status): preserve Claude background work * fix(agent-status): harden background task lifecycle * fix(agent-status): narrow interruption retention * fix(agent-status): scope background task authority * fix(agent-status): isolate lifecycle inventories * fix(agent-status): harden background evidence recovery * fix(agent-status): reject ambiguous child authority * perf(agent-status): skip lifecycle inventory scans * refactor(agent-status): isolate task inventory parsing * fix(agent-status): clear stale background evidence * fix(agent-status): gate accepted remote evidence * test(agent-status): pin session cron interrupts * fix: harden Claude inventory tracking * test: pin Claude cron drain authority * refactor(agent-status): unify Claude turn-boundary predicate Collapse the five inline copies of the Stop/StopFailure test into a single isTurnBoundary constant and drop the reportedStateName/stateName alias, so a future edit can't move one copy and leave the others behind. Pin the two behaviors that unification now depends on: a non-interrupted StopFailure keeps gating on live background work, and interrupted state does not survive a mid-turn lead event that has no prompt submit. Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): gate local Claude background evidence --------- Co-authored-by: Orca <help@stably.ai>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { AGENT_STATUS_MAX_SUBAGENTS } from './agent-status-types'
|
|
|
|
const CLAUDE_TERMINAL_BACKGROUND_TASK_STATUSES = new Set([
|
|
'idle',
|
|
'done',
|
|
'success',
|
|
'succeeded',
|
|
'complete',
|
|
'completed',
|
|
'finished',
|
|
'failed',
|
|
'error',
|
|
'terminated',
|
|
'exited',
|
|
'aborted',
|
|
'expired',
|
|
'skipped',
|
|
'crashed',
|
|
'killed',
|
|
'cancelled',
|
|
'canceled',
|
|
'timed_out'
|
|
])
|
|
|
|
/** One agent entry from the `background_tasks` array Claude attaches to Stop
|
|
* (and SubagentStop) hook payloads. Non-agent tasks do not become rows. */
|
|
export type ClaudeBackgroundAgentTask = {
|
|
id: string
|
|
agentType?: string
|
|
description?: string
|
|
running: boolean
|
|
/** True for `type: "teammate"` entries. Their ids never match lifecycle
|
|
* agent_ids and they report "running" permanently — even after the named
|
|
* agent finished — so they carry no per-agent state at all. */
|
|
teammate: boolean
|
|
}
|
|
|
|
/** Read the agent-typed entries of a hook payload's `background_tasks` field.
|
|
* `present: false` means the field was absent/malformed (older Claude builds),
|
|
* so callers must keep their tracked roster instead of clearing it. */
|
|
export function readClaudeBackgroundAgentTasks(hookPayload: Record<string, unknown>): {
|
|
present: boolean
|
|
tasks: ClaudeBackgroundAgentTask[]
|
|
truncated: boolean
|
|
hasRunningNonAgentTask: boolean
|
|
} {
|
|
const raw = hookPayload['background_tasks']
|
|
if (!Array.isArray(raw)) {
|
|
return { present: false, tasks: [], truncated: false, hasRunningNonAgentTask: false }
|
|
}
|
|
const tasks: ClaudeBackgroundAgentTask[] = []
|
|
let truncated = false
|
|
let hasRunningNonAgentTask = false
|
|
for (const item of raw) {
|
|
if (typeof item !== 'object' || item === null) {
|
|
truncated = true
|
|
hasRunningNonAgentTask = true
|
|
continue
|
|
}
|
|
const obj = item as Record<string, unknown>
|
|
const taskType = typeof obj.type === 'string' ? obj.type.trim().toLowerCase() : ''
|
|
const taskStatus = typeof obj.status === 'string' ? obj.status.trim().toLowerCase() : ''
|
|
const isTerminal =
|
|
taskStatus.length > 0 && CLAUDE_TERMINAL_BACKGROUND_TASK_STATUSES.has(taskStatus)
|
|
if (taskType.length === 0) {
|
|
truncated = true
|
|
hasRunningNonAgentTask ||= !isTerminal
|
|
continue
|
|
}
|
|
const isAgentTask = taskType === 'subagent' || taskType === 'teammate'
|
|
// Why: future non-agent types and nonterminal labels must fail active; only typed agent rows or explicit terminal states can safely retire work.
|
|
if (!isAgentTask && !isTerminal) {
|
|
hasRunningNonAgentTask = true
|
|
}
|
|
if (!isAgentTask) {
|
|
continue
|
|
}
|
|
if (typeof obj.id !== 'string' || obj.id.trim().length === 0) {
|
|
truncated = true
|
|
continue
|
|
}
|
|
if (tasks.length >= AGENT_STATUS_MAX_SUBAGENTS) {
|
|
// Why: a capped inventory cannot prove a tracked id is absent; callers
|
|
// must retain unlisted rows rather than deleting live overflow tasks.
|
|
truncated = true
|
|
continue
|
|
}
|
|
tasks.push({
|
|
id: obj.id.trim(),
|
|
agentType: typeof obj.agent_type === 'string' ? obj.agent_type : undefined,
|
|
description: typeof obj.description === 'string' ? obj.description : undefined,
|
|
running: !isTerminal,
|
|
teammate: taskType === 'teammate'
|
|
})
|
|
}
|
|
return { present: true, tasks, truncated, hasRunningNonAgentTask }
|
|
}
|