mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 08:02:44 +00:00
* fix(agent-status): keep a Claude pane working until owed task wake-ups arrive Claude wakes the main agent for each background task that ends, after the task stops running. "Nothing running" was read as done, so automations closed the terminal before the final turn. Fixes #23942 * fix(agent-status): keep owed task wake-ups through failed turns, the cap and nested exits A failed turn's task list now marks a vanished shell owed like a normal turn end does. Past the cap only an already-announced sub-agent is forgotten. A process exit clears what is owed only where the server admits it from the pane's owner. * refactor(agent-status): move pane-scoped cache entry helpers out of listener-state listener-state.ts passed the 300-line limit once main's and this branch's additions met. * fix(agent-status): verify Claude task wake-up completion on its execution host * fix(agent-status): canonicalize Claude background task identifiers * Preserve out-of-order Claude task notification evidence * Verify retained released parser in cross-version checkout test * Reuse watcher directory-cache enumeration without losing fresh keys --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
import { AGENT_STATUS_MAX_SUBAGENTS } from './agent-status-types'
|
|
import { classifyClaudeBackgroundTaskKind } from './claude-background-task-kind'
|
|
import { isAgentChildWorkKind } from './agent-status-child-work-liveness'
|
|
|
|
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
|
|
/** Ids of the running shell-like entries; one without an id is counted but not listed. */
|
|
runningNonAgentTaskIds: string[]
|
|
} {
|
|
const raw = hookPayload['background_tasks']
|
|
if (!Array.isArray(raw)) {
|
|
return {
|
|
present: false,
|
|
tasks: [],
|
|
truncated: false,
|
|
hasRunningNonAgentTask: false,
|
|
runningNonAgentTaskIds: []
|
|
}
|
|
}
|
|
const tasks: ClaudeBackgroundAgentTask[] = []
|
|
let truncated = false
|
|
let hasRunningNonAgentTask = false
|
|
const runningNonAgentTaskIds: string[] = []
|
|
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 = isAgentChildWorkKind(classifyClaudeBackgroundTaskKind(taskType))
|
|
// 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
|
|
const id = typeof obj.id === 'string' ? obj.id.trim() : ''
|
|
if (id.length > 0) {
|
|
runningNonAgentTaskIds.push(id)
|
|
}
|
|
}
|
|
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, runningNonAgentTaskIds }
|
|
}
|