fix(runtime): refuse to silently wake a deliberately slept pane

`activateMobileSessionTab` decided whether to materialize a pane from
`publicTab.status !== 'ready'` alone. A pane the user put to sleep publishes
`pending-handle` indefinitely, so it was indistinguishable there from a pane
whose PTY was lost and is awaiting reconnect — and activating it respawned it
with a freshly re-resolved agent launch the user never asked for.

Gate materialization on the durable sleeping record's `origin`. Only a
worktree-owned capture ('worktree-sleep', written by workspace sleep and by
completed-agent hibernation) records a deliberate takedown; 'live' and 'quit'
captures describe a pane that was still running, so reconnect-driven
materialization (#11542) is unchanged for them.
This commit is contained in:
Jinwoo-H
2026-08-05 03:39:02 -04:00
parent c4d5a535f2
commit 917c71fdde
2 changed files with 175 additions and 0 deletions
+152
View File
@@ -79,6 +79,7 @@ import {
} from '../../shared/runtime-types'
import type { TerminalSideEffectBatch } from '../../shared/terminal-side-effect-facts'
import type { RuntimeClientEvent } from '../../shared/runtime-client-events'
import type { SleepingAgentSessionRecord } from '../../shared/agent-session-resume'
import {
TERMINAL_INPUT_CHUNK_MAX_BYTES,
TERMINAL_INPUT_MAX_BYTES,
@@ -29145,6 +29146,157 @@ describe('OrcaRuntimeService', () => {
})
})
describe('deliberately parked pane activation (STA-3465)', () => {
function makeParkedSessionStore(
origin: SleepingAgentSessionRecord['origin'] | undefined,
overrides: Partial<SleepingAgentSessionRecord> = {}
) {
return makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
sleepingAgentSessionsByPaneKey: {
[`host-tab:${HEADLESS_LEAF_ID}`]: {
paneKey: `host-tab:${HEADLESS_LEAF_ID}`,
tabId: 'host-tab',
worktreeId: TEST_WORKTREE_ID,
agent: 'claude',
providerSession: { key: 'session_id', id: 'provider-session-1' },
prompt: 'do the thing',
state: 'done',
capturedAt: 1,
updatedAt: 1,
...(origin ? { origin } : {}),
...overrides
} as SleepingAgentSessionRecord
}
})
)
}
function makeParkedRuntime(runtimeStore: unknown): {
runtime: OrcaRuntimeService
spawn: ReturnType<typeof vi.fn>
} {
const spawn = vi.fn().mockResolvedValue({ id: 'persisted-pty' })
const runtime = new OrcaRuntimeService(runtimeStore as never)
runtime.setPtyController({
spawn,
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => []
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
return { runtime, spawn }
}
it('refuses to respawn a deliberately slept pane on a reconnect-driven activate', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep')
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ notifyClients: false, navigation: 'caller' }
)
expect(spawn).not.toHaveBeenCalled()
expect(activated.tabs[0]).toMatchObject({
type: 'terminal',
parentTabId: 'host-tab',
leafId: HEADLESS_LEAF_ID,
status: 'pending-handle',
terminal: null
})
// Negative safety: refusing to wake must not retire the surface either.
expect((await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs[0]).toMatchObject(
{ parentTabId: 'host-tab', status: 'pending-handle' }
)
})
it('refuses to respawn a deliberately slept pane on a headless host activate', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep')
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
const activated = await runtime.activateMobileSessionTab(`id:${TEST_WORKTREE_ID}`, 'host-tab')
expect(spawn).not.toHaveBeenCalled()
expect(activated.tabs[0]).toMatchObject({ status: 'pending-handle', terminal: null })
})
// Why: #11542's reconnect fix depends on activate materializing a genuinely
// awaiting pane. These three prove the park guard did not break it.
it('still materializes a pane awaiting reconnect with no sleeping record', async () => {
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
)
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ notifyClients: false, navigation: 'caller' }
)
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({
tabId: 'host-tab',
leafId: HEADLESS_LEAF_ID,
sessionId: 'persisted-pty'
})
)
expect(activated.tabs[0]).toMatchObject({ status: 'ready' })
})
it('still materializes a pane whose record was captured while it was live', async () => {
const { runtimeStore } = makeParkedSessionStore('live')
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ notifyClients: false, navigation: 'caller' }
)
expect(spawn).toHaveBeenCalledOnce()
expect(activated.tabs[0]).toMatchObject({ status: 'ready' })
})
it('still materializes a pane whose record was captured at app quit', async () => {
const { runtimeStore } = makeParkedSessionStore('quit')
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ notifyClients: false, navigation: 'caller' }
)
expect(spawn).toHaveBeenCalledOnce()
expect(activated.tabs[0]).toMatchObject({ status: 'ready' })
})
it('ignores a park record that belongs to a different worktree', async () => {
const { runtimeStore } = makeParkedSessionStore('worktree-sleep', {
worktreeId: 'other-repo::/other'
})
const { runtime, spawn } = makeParkedRuntime(runtimeStore)
const activated = await runtime.activateMobileSessionTab(
`id:${TEST_WORKTREE_ID}`,
'host-tab',
undefined,
{ notifyClients: false, navigation: 'caller' }
)
expect(spawn).toHaveBeenCalledOnce()
expect(activated.tabs[0]).toMatchObject({ status: 'ready' })
})
})
it('reattaches hydrated SSH headless terminals with the persisted relay identity', async () => {
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal({
+23
View File
@@ -7321,6 +7321,7 @@ export class OrcaRuntimeService {
const shouldMaterializePendingTerminal =
publicTab?.type === 'terminal' &&
publicTab.status !== 'ready' &&
!this.isDeliberatelyParkedPane(worktreeId, tab) &&
(!targetsHost ||
!this.notifier?.focusTerminal ||
this.shouldMaterializeHeadlessMobileSessionTab(snapshot!, tab))
@@ -7474,6 +7475,28 @@ export class OrcaRuntimeService {
return snapshot
}
/**
* Whether persistence proves this pane's PTY was deliberately taken down and parked
* (workspace sleep or completed-agent hibernation) rather than lost and awaiting reconnect.
* Why: `pending-handle` alone cannot tell those apart — a parked pane publishes it
* indefinitely — and respawning a parked pane re-launches its agent behind the user.
*/
private isDeliberatelyParkedPane(
worktreeId: string,
tab: RuntimeMobileSessionTerminalTab
): boolean {
const record =
this.getWorkspaceSessionForWorktree(worktreeId)?.sleepingAgentSessionsByPaneKey?.[
makePaneKey(tab.parentTabId, tab.leafId)
]
// Why: 'live'/'quit' captures describe a pane that was still running, so a reconnect
// must still mint its replacement PTY (#11542). Only a worktree-owned capture records
// a deliberate takedown the user did not ask to undo.
return (
record?.origin === 'worktree-sleep' && runtimeWorktreeIdsEqual(record.worktreeId, worktreeId)
)
}
private shouldMaterializeHeadlessMobileSessionTab(
snapshot: RuntimeMobileSessionTabsSnapshot,
tab: RuntimeMobileSessionTerminalTab