mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(hibernation): restart idle window after restored reap
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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([])
|
||||
|
||||
Reference in New Issue
Block a user