fix(agent-sleep): keep active dispatch workers awake

This commit is contained in:
Brennan Benson
2026-07-31 13:03:29 -07:00
parent 129d8b32bb
commit 2ee4bd25db
8 changed files with 108 additions and 1 deletions
+4 -1
View File
@@ -36036,6 +36036,7 @@ describe('OrcaRuntimeService', () => {
expect(result.agentOrchestrationByPaneKey?.[workerPaneKey]).toMatchObject({
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus: 'dispatched',
taskTitle: 'Dispatch prompt work',
displayName: 'Review dispatch prompts and make worker labels distinct',
parentPaneKey: coordinatorPaneKey,
@@ -36113,6 +36114,7 @@ describe('OrcaRuntimeService', () => {
expect(result.agentOrchestrationByPaneKey?.[workerPaneKey]).toMatchObject({
taskId: 'task-done',
dispatchId: 'ctx-done',
dispatchStatus: 'completed',
parentPaneKey: coordinatorPaneKey,
parentTerminalHandle: coordinatorHandle
})
@@ -36170,7 +36172,8 @@ describe('OrcaRuntimeService', () => {
expect(result.agentOrchestrationByPaneKey?.[workerPaneKey]).toEqual({
taskId: 'task-done',
dispatchId: 'ctx-done'
dispatchId: 'ctx-done',
dispatchStatus: 'completed'
})
})
+1
View File
@@ -29198,6 +29198,7 @@ export class OrcaRuntimeService {
return {
taskId: dispatch.task_id,
dispatchId: dispatch.id,
dispatchStatus: dispatch.status,
...(display.taskTitle ? { taskTitle: display.taskTitle } : {}),
...(display.displayName ? { displayName: display.displayName } : {}),
...(parentTerminalHandle ? { parentTerminalHandle } : {}),
@@ -356,6 +356,35 @@ describe('agent sleep coordinator', () => {
expect(shutdown).not.toHaveBeenCalled()
})
it('rechecks dispatch settlement before shutdown', async () => {
vi.useFakeTimers()
const completed = {
...entry(),
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus: 'completed' as const
}
}
const shutdown = installEligibleState(vi.fn().mockResolvedValue(undefined), {
agentStatusByPaneKey: { [completed.paneKey]: completed }
})
startAgentHibernationCoordinator({ intervalMs: 1000, now: () => NOW })
await vi.advanceTimersByTimeAsync(1000)
useAppStore.setState({
agentStatusByPaneKey: {
[completed.paneKey]: {
...completed,
orchestration: { ...completed.orchestration, dispatchStatus: 'dispatched' }
}
}
})
await vi.advanceTimersByTimeAsync(1000)
expect(shutdown).not.toHaveBeenCalled()
})
it('restarts confirmation when a foreground terminal visit refreshes idle state', async () => {
vi.useFakeTimers()
vi.setSystemTime(NOW)
@@ -134,6 +134,39 @@ describe('agent sleep planner', () => {
).toEqual([])
})
it.each([undefined, 'pending', 'dispatched'] as const)(
'blocks orchestration panes while dispatch status is %s',
(dispatchStatus) => {
const orchestrated = entry({
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
...(dispatchStatus ? { dispatchStatus } : {})
}
})
expect(
plannedPaneKeys(
snapshot({ agentStatusByPaneKey: { [orchestrated.paneKey]: orchestrated } })
)
).toEqual([])
}
)
it('allows orchestration panes after authoritative dispatch settlement', () => {
const orchestrated = entry({
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus: 'completed'
}
})
expect(
plannedPaneKeys(snapshot({ agentStatusByPaneKey: { [orchestrated.paneKey]: orchestrated } }))
).toEqual([orchestrated.paneKey])
})
it('requires the idle threshold and blocks input after done', () => {
const fresh = entry({ updatedAt: NOW - 1_000 })
expect(
@@ -110,6 +110,13 @@ function getEntryTabId(entry: AgentStatusEntry): string | null {
return parsePaneKey(entry.paneKey)?.tabId ?? null
}
function hasUnsettledOrUnknownDispatch({ orchestration }: AgentStatusEntry): boolean {
// Why: provider done hooks can fire mid-Dispatch; only runtime-confirmed settlement makes sleep safe.
return orchestration
? !['completed', 'failed', 'circuit_broken'].includes(orchestration.dispatchStatus ?? '')
: false
}
function getEligiblePane(args: {
entry: AgentStatusEntry
tab: TerminalTab
@@ -140,6 +147,7 @@ function getEligiblePane(args: {
if (
entry.state !== 'done' ||
entry.interrupted === true ||
hasUnsettledOrUnknownDispatch(entry) ||
(sleepingRecord && !hasOnlyLivePiCompatibleRecoveryIdentity)
) {
return null
@@ -265,6 +265,36 @@ describe('agent status runtime orchestration metadata', () => {
})
})
it('updates runtime status for the same dispatch', () => {
vi.useFakeTimers()
const store = createTestStore()
const childPaneKey = 'tab-child:11111111-1111-4111-8111-111111111111'
store.getState().setAgentStatus(childPaneKey, {
state: 'done',
prompt: 'child agent',
agentType: 'claude',
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus: 'dispatched'
}
})
store.getState().setRuntimeAgentOrchestrationByPaneKey({
[childPaneKey]: {
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus: 'completed'
}
})
expect(store.getState().agentStatusByPaneKey[childPaneKey].orchestration).toMatchObject({
taskId: 'task-1',
dispatchId: 'ctx-1',
dispatchStatus: 'completed'
})
})
it('keeps current payload orchestration ahead of a stale runtime map entry', () => {
vi.useFakeTimers()
const store = createTestStore()
@@ -1090,6 +1090,7 @@ function orchestrationContextsEqual(
return (
a.taskId === b.taskId &&
a.dispatchId === b.dispatchId &&
a.dispatchStatus === b.dispatchStatus &&
a.taskTitle === b.taskTitle &&
a.displayName === b.displayName &&
a.parentTerminalHandle === b.parentTerminalHandle &&
+2
View File
@@ -60,6 +60,8 @@ export const AGENT_STATE_HISTORY_MAX = 20
export type AgentStatusOrchestrationContext = {
taskId: string
dispatchId: string
/** Runtime-authoritative lifecycle state. Hook-only contexts may omit it. */
dispatchStatus?: 'pending' | 'dispatched' | 'completed' | 'failed' | 'circuit_broken'
taskTitle?: string
displayName?: string
parentTerminalHandle?: string