fix(hibernation): restart idle window after restored reap

This commit is contained in:
Brennan Benson
2026-07-28 16:53:34 -07:00
parent 47ccd53614
commit a37dce3954
3 changed files with 48 additions and 6 deletions
@@ -29,6 +29,7 @@ beforeEach(() => {
})
afterEach(() => {
vi.restoreAllMocks()
rmSync(dir, { recursive: true, force: true })
})
@@ -37,6 +38,7 @@ afterEach(() => {
* behind when the child's SubagentStop is lost while Orca is down. */
async function restartWithInFlightSubagent(options?: {
connectionId?: string
state?: 'working' | 'waiting'
subagents?: AgentSubagentSnapshot[]
additionalPaneKey?: string
}): Promise<AgentHookServer> {
@@ -48,7 +50,7 @@ async function restartWithInFlightSubagent(options?: {
worktreeId: 'wt-1',
connectionId: options?.connectionId ?? null,
payload: {
state: 'working',
state: options?.state ?? 'working',
prompt: 'review the PR',
agentType: 'claude',
subagents: options?.subagents ?? [WORKING_CHILD]
@@ -61,7 +63,7 @@ async function restartWithInFlightSubagent(options?: {
worktreeId: 'wt-1',
connectionId: options.connectionId ?? null,
payload: {
state: 'working',
state: options.state ?? 'working',
prompt: 'review the PR',
agentType: 'claude',
subagents: options.subagents ?? [WORKING_CHILD]
@@ -112,10 +114,19 @@ describe('restored subagent liveness sweep', () => {
const server = await restartWithInFlightSubagent()
try {
expect(paneStatus(server)).toEqual({ state: 'working', subagents: [WORKING_CHILD] })
const previous = server.getStatusSnapshotForPane(PANE)[0]
const previousReceivedAt = previous?.receivedAt ?? 0
const reconciledAt = previousReceivedAt + 1
const now = vi.spyOn(Date, 'now').mockReturnValue(previousReceivedAt)
expect(await sweepWith(server, { persistedPtyIdByPaneKey: { [PANE]: PTY } })).toBe(1)
expect(paneStatus(server)).toEqual({ state: 'done', subagents: undefined })
expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({
receivedAt: reconciledAt,
stateStartedAt: reconciledAt
})
now.mockRestore()
} finally {
server.stop()
}
@@ -137,6 +148,24 @@ describe('restored subagent liveness sweep', () => {
}
})
it('preserves timing when reaping rows does not change the lead state', async () => {
const server = await restartWithInFlightSubagent({ state: 'waiting' })
try {
const previous = server.getStatusSnapshotForPane(PANE)[0]
expect(await sweepWith(server, { persistedPtyIdByPaneKey: { [PANE]: PTY } })).toBe(1)
expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({
state: 'waiting',
subagents: undefined,
receivedAt: previous?.receivedAt,
stateStartedAt: previous?.stateStartedAt
})
} finally {
server.stop()
}
})
it('uses targeted liveness for a PTY bound in this runtime', async () => {
const server = await restartWithInFlightSubagent()
try {
+6
View File
@@ -1986,8 +1986,14 @@ export class AgentHookServer {
enriched.payload.state === 'working' && !claudeRosterHasWorkingSubagent(roster)
? 'done'
: enriched.payload.state
const stateChanged = state !== enriched.payload.state
const reconciledAt = stateChanged
? Math.max(Date.now(), enriched.receivedAt + 1)
: enriched.receivedAt
this.state.lastStatusByPaneKey.set(paneKey, {
...enriched,
receivedAt: reconciledAt,
stateStartedAt: stateChanged ? reconciledAt : enriched.stateStartedAt,
payload: { ...enriched.payload, state, subagents }
})
}
@@ -554,7 +554,7 @@ describe('agent sleep planner', () => {
).toEqual([`tab-1:${LEAF}`, `tab-1:${OTHER_LEAF}`])
})
it('becomes eligible once a phantom subagent stops gating the pane working', () => {
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({
@@ -565,9 +565,14 @@ describe('agent sleep planner', () => {
[]
)
const reaped = entry({ state: 'done' })
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
@@ -575,8 +580,10 @@ describe('agent sleep planner', () => {
expect(
plannedPaneKeys(
snapshot({
agentStatusByPaneKey: { [reaped.paneKey]: reaped },
lastTerminalInputAtByPaneKey: { [reaped.paneKey]: reaped.stateStartedAt + 1 }
agentStatusByPaneKey: { [idleReaped.paneKey]: idleReaped },
lastTerminalInputAtByPaneKey: {
[idleReaped.paneKey]: idleReaped.stateStartedAt + 1
}
})
)
).toEqual([])