mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-23 05:31:23 +00:00
fix(tui): handle timeout/error subagent statuses in /agents (#26687)
Accept delegation timeout/error statuses in the TUI subagent model, normalize unknown status strings defensively, and harden /agents overlay rendering/sorting so unknown statuses cannot crash glyph/color lookup. Add regression tests for live event normalization and disk snapshot replay.
This commit is contained in:
parent
566d8f0d75
commit
006937f7d0
8 changed files with 173 additions and 14 deletions
|
|
@ -13,7 +13,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
|
|||
import { topLevelSubagents } from '../lib/subagentTree.js'
|
||||
import { formatToolCall, stripAnsi } from '../lib/text.js'
|
||||
import { fromSkin } from '../theme.js'
|
||||
import type { Msg, SubagentProgress } from '../types.js'
|
||||
import type { Msg, SubagentProgress, SubagentStatus } from '../types.js'
|
||||
|
||||
import { applyDelegationStatus, getDelegationState } from './delegationStore.js'
|
||||
import type { GatewayEventHandlerContext } from './interfaces.js'
|
||||
|
|
@ -54,6 +54,26 @@ const pushThinking = pushUnique(6)
|
|||
const pushNote = pushUnique(6)
|
||||
const pushTool = pushUnique(8)
|
||||
|
||||
const KNOWN_SUBAGENT_STATUSES = new Set<SubagentStatus>([
|
||||
'completed',
|
||||
'error',
|
||||
'failed',
|
||||
'interrupted',
|
||||
'queued',
|
||||
'running',
|
||||
'timeout'
|
||||
])
|
||||
|
||||
const normalizeSubagentStatus = (status: unknown, fallback: SubagentStatus): SubagentStatus => {
|
||||
if (typeof status !== 'string') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const normalized = status.toLowerCase() as SubagentStatus
|
||||
|
||||
return KNOWN_SUBAGENT_STATUSES.has(normalized) ? normalized : fallback
|
||||
}
|
||||
|
||||
export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: GatewayEvent) => void {
|
||||
const { rpc } = ctx.gateway
|
||||
const { STARTUP_RESUME_ID, newSession, resumeById, setCatalog } = ctx.session
|
||||
|
|
@ -180,8 +200,9 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
|
||||
// Terminal statuses are never overwritten by late-arriving live events —
|
||||
// otherwise a stale `subagent.start` / `spawn_requested` can clobber a
|
||||
// `failed` or `interrupted` terminal state (Copilot review #14045).
|
||||
const isTerminalStatus = (s: SubagentProgress['status']) => s === 'completed' || s === 'failed' || s === 'interrupted'
|
||||
// terminal state from complete (failed/interrupted/timeout/error).
|
||||
const isTerminalStatus = (s: SubagentProgress['status']) =>
|
||||
s === 'completed' || s === 'error' || s === 'failed' || s === 'interrupted' || s === 'timeout'
|
||||
|
||||
const keepTerminalElseRunning = (s: SubagentProgress['status']) => (isTerminalStatus(s) ? s : 'running')
|
||||
|
||||
|
|
@ -648,7 +669,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
ev.payload,
|
||||
c => ({
|
||||
durationSeconds: ev.payload.duration_seconds ?? c.durationSeconds,
|
||||
status: ev.payload.status ?? 'completed',
|
||||
status: normalizeSubagentStatus(ev.payload.status, 'completed'),
|
||||
summary: ev.payload.summary || ev.payload.text || c.summary
|
||||
}),
|
||||
{ createIfMissing: false }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { atom } from 'nanostores'
|
||||
|
||||
import type { SpawnTreeLoadResponse } from '../gatewayTypes.js'
|
||||
import type { SubagentProgress } from '../types.js'
|
||||
import type { SubagentProgress, SubagentStatus } from '../types.js'
|
||||
|
||||
export interface SpawnSnapshot {
|
||||
finishedAt: number
|
||||
|
|
@ -21,6 +21,26 @@ export interface SpawnDiffPair {
|
|||
|
||||
const HISTORY_LIMIT = 10
|
||||
|
||||
const KNOWN_SUBAGENT_STATUSES = new Set<SubagentStatus>([
|
||||
'completed',
|
||||
'error',
|
||||
'failed',
|
||||
'interrupted',
|
||||
'queued',
|
||||
'running',
|
||||
'timeout'
|
||||
])
|
||||
|
||||
const normalizeSubagentStatus = (status: unknown, fallback: SubagentStatus): SubagentStatus => {
|
||||
if (typeof status !== 'string') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const normalized = status.toLowerCase() as SubagentStatus
|
||||
|
||||
return KNOWN_SUBAGENT_STATUSES.has(normalized) ? normalized : fallback
|
||||
}
|
||||
|
||||
export const $spawnHistory = atom<SpawnSnapshot[]>([])
|
||||
export const $spawnDiff = atom<null | SpawnDiffPair>(null)
|
||||
|
||||
|
|
@ -128,7 +148,7 @@ function normaliseSubagent(raw: unknown): SubagentProgress {
|
|||
parentId: s(o.parentId) ?? null,
|
||||
reasoningTokens: n(o.reasoningTokens),
|
||||
startedAt: n(o.startedAt),
|
||||
status: (s(o.status) as SubagentProgress['status']) ?? 'completed',
|
||||
status: normalizeSubagentStatus(o.status, 'completed'),
|
||||
summary: s(o.summary),
|
||||
taskCount: typeof o.taskCount === 'number' ? o.taskCount : 1,
|
||||
thinking: (arr<string>(o.thinking) ?? []).filter(x => typeof x === 'string'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue