mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
fix(hibernation): reap restored subagent rows with no live agent process (#11219)
* fix(hibernation): reap restored subagent rows with no live agent process A pane whose Claude session had a subagent in flight can be locked out of agent hibernation for good. A PTY that dies while Orca is down never runs the teardown that clears pane state, so hydrate rebuilds a subagent roster that nothing can retire: the existing reap needs the parent to emit a complete `background_tasks` inventory, and a parent that went idle before the restart never emits one. The restored row keeps gating the pane 'working', and hibernation only accepts 'done'. Observed locally: six panes parked at SubagentStop in state 'working' for 17 to 145 hours, each still holding a working child row. Adds a second reap path. Hydrate seeds are marked `restoredFromSnapshot`, cleared by any live lifecycle event or an id-exact running inventory entry. One post-restore sweep drops the rows still unconfirmed when the pane's PTY is absent from the live local inventory, then re-derives the child-gated 'working' to 'done'. The scan is local-only by construction: panes with a relay connection id are skipped and SSH-scoped PTY ids resolve as live, since a remote agent runs on the far host and could never appear in a local listing. An unreadable inventory is not evidence that anything exited, so it is a no-op. Panes that have reported to this runtime are left alone. `stateStartedAt` and `stateHistory` are untouched, so a draft typed while the pane was working still blocks hibernation. * fix(hibernation): prove local ownership before restored reap * fix(hibernation): require authoritative restored PTY absence * fix(hibernation): probe restored PTY liveness authoritatively * fix(hibernation): restart idle window after restored reap * fix(hibernation): type restored reconciliation timing * fix(hibernation): respect worktree host ownership * fix(hibernation): preserve same-id restored PTY rebinds * fix(hibernation): fence batched restored PTY probes
This commit is contained in:
@@ -554,6 +554,41 @@ describe('agent sleep planner', () => {
|
||||
).toEqual([`tab-1:${LEAF}`, `tab-1:${OTHER_LEAF}`])
|
||||
})
|
||||
|
||||
it('restarts the idle window once a phantom subagent stops gating the pane working', () => {
|
||||
// Why: a restored subagent row holds a finished lead at 'working', which is
|
||||
// the one state hibernation never accepts — reaping it is what unlocks it.
|
||||
const gated = entry({
|
||||
state: 'working',
|
||||
subagents: [{ id: 'areview-loop-c237a4c577493352', state: 'working', startedAt: 1 }]
|
||||
})
|
||||
expect(plannedPaneKeys(snapshot({ agentStatusByPaneKey: { [gated.paneKey]: gated } }))).toEqual(
|
||||
[]
|
||||
)
|
||||
|
||||
const reaped = entry({ state: 'done', updatedAt: NOW, stateStartedAt: NOW })
|
||||
expect(
|
||||
plannedPaneKeys(snapshot({ agentStatusByPaneKey: { [reaped.paneKey]: reaped } }))
|
||||
).toEqual([])
|
||||
|
||||
const idleReaped = entry({ state: 'done' })
|
||||
expect(
|
||||
plannedPaneKeys(snapshot({ agentStatusByPaneKey: { [idleReaped.paneKey]: idleReaped } }))
|
||||
).toEqual([`tab-1:${LEAF}`])
|
||||
|
||||
// Why: reaping only clears the child gate — a draft typed into the composer
|
||||
// while that segment was open still dies with the PTY, so it keeps blocking.
|
||||
expect(
|
||||
plannedPaneKeys(
|
||||
snapshot({
|
||||
agentStatusByPaneKey: { [idleReaped.paneKey]: idleReaped },
|
||||
lastTerminalInputAtByPaneKey: {
|
||||
[idleReaped.paneKey]: idleReaped.stateStartedAt + 1
|
||||
}
|
||||
})
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('clamps corrupt or out-of-range idle durations to the default', () => {
|
||||
expect(getEffectiveAgentHibernationIdleMs(0)).toBe(DEFAULT_AGENT_HIBERNATION_IDLE_MS)
|
||||
expect(getEffectiveAgentHibernationIdleMs(Number.NaN)).toBe(DEFAULT_AGENT_HIBERNATION_IDLE_MS)
|
||||
|
||||
Reference in New Issue
Block a user