Main writes a workspace sleep's resume records after the statuses it accepted before the sleep (dark, layout-owner build only) (#30482)

* Main keeps an agent's resume record from the status its hook server accepts (dark, layout-owner build only)

In a layout-owner build, the window's recorder and quit capture kept only
records of tabs the window holds, so an agent in a workspace the window
never opened lost its resume record at quit. Main now applies the window's
rule (moved into src/shared) to every hook status for a pane it holds, and
captures its own panes' agents at quit; the window's quit capture is gone.

Co-Authored-By: Claude <noreply@anthropic.com>

* Main writes a workspace sleep's resume records after the hook statuses it accepted before the sleep (dark, layout-owner build only)

* A snapshot replay looks up a pane's tab only when its status can leave a resume record

Restores the lookup order the window had before the record builder moved to shared code, and
updates the two handler tests for the quit-records option.

* Note why the status write applies its command directly

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jinwoo Hong
2026-10-19 00:03:42 -04:00
committed by GitHub
co-authored by Claude
parent a282409d20
commit e37d5271cc
3 changed files with 207 additions and 17 deletions
@@ -0,0 +1,168 @@
// A layout-owner build: a hook status main accepted before a workspace sleep must not replace the
// sleep's record when its queued write lands after the sleep is saved.
import { describe, expect, it, vi } from 'vitest'
import type { AgentStatusIpcPayload } from '../../shared/agent-status-ipc-payload'
import type { ExecutionHostId } from '../../shared/execution-host'
import type { LayoutApplyOutcome } from '../../shared/workspace-layout/workspace-layout-command-types'
import { applyCommandToSession } from '../persistence/terminal-topology/terminal-pane-admission'
import type { WorktreeSleepResumeCapture } from './terminal/stop/worktree-sleep-resume-records'
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
// Fragments stay side-effect ordered: mocks, then lifecycle, then fixtures.
const { OrcaRuntimeService } = await import('./orca-runtime-test-mocks.spec')
await import('./orca-runtime-test-lifecycle.spec')
const {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal
} = await import('./orca-runtime-test-fixtures.spec')
const PANE_KEY = `host-tab:${HEADLESS_LEAF_ID}`
const PTY_ID = 'pty-slept'
const SESSION = { key: 'session_id' as const, id: 'codex-session-1' }
function codexRow(overrides: Partial<AgentStatusIpcPayload> = {}): AgentStatusIpcPayload {
return {
paneKey: PANE_KEY,
worktreeId: TEST_WORKTREE_ID,
agentType: 'codex',
providerSession: SESSION,
state: 'working',
prompt: 'keep working',
receivedAt: 10,
stateStartedAt: 10,
connectionId: null,
...overrides
}
}
/** A runtime whose first record write applies at once but holds its durable write open. */
function makeRuntimeWithSlowFirstWrite() {
const { runtimeStore, getSession, setSession } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
)
let releaseFirstWrite = (): void => {}
const firstWriteDurable = new Promise<void>((resolve) => {
releaseFirstWrite = resolve
})
let writes = 0
const applyLayoutCommand = vi.fn(
async (
command: Parameters<typeof applyCommandToSession>[2],
hostIds: ExecutionHostId[]
): Promise<LayoutApplyOutcome> => {
for (const hostId of hostIds) {
const change = applyCommandToSession(hostId, getSession(), command)
if (change.session) {
setSession(change.session)
}
}
writes += 1
if (writes === 1) {
await firstWriteDurable
}
return { ok: true, result: {} }
}
)
const runtime = new OrcaRuntimeService(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the fixture store implements the runtime store methods these tests reach.
Object.assign(runtimeStore, { applyLayoutCommand }) as never
)
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
return { runtime, getSession, releaseFirstWrite }
}
function sleepCapture(updatedAt: number): WorktreeSleepResumeCapture {
return new Map([
[
PTY_ID,
{
retiredPaneKeys: [],
records: [
{
paneKey: PANE_KEY,
tabId: 'host-tab',
worktreeId: TEST_WORKTREE_ID,
agent: 'codex',
providerSession: SESSION,
prompt: 'keep working',
state: 'done',
capturedAt: 20,
updatedAt,
origin: 'worktree-sleep',
restoreOnTabOpenOnly: true,
connectionId: null
}
]
}
]
])
}
describe('a queued hook status and a workspace sleep', () => {
it('keeps the sleep’s record when a status accepted before the sleep is written after it', async () => {
const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite()
// An earlier status's write is still on its way to disk when the next status is accepted.
runtime.recordHookStatusResume(codexRow())
await vi.waitFor(() =>
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined()
)
runtime.recordHookStatusResume(codexRow({ state: 'blocked', receivedAt: 11 }))
// The sleep captured that status from the hook store and saves its record.
const slept = runtime['sleepResumeRecords'].persist(TEST_WORKTREE_ID, sleepCapture(11), [
PTY_ID
])
releaseFirstWrite()
await slept
await runtime['hookStatusResumeWrites']
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toMatchObject({
origin: 'worktree-sleep',
state: 'done',
restoreOnTabOpenOnly: true
})
})
it('keeps a window sleep’s record when a status accepted before it is written after it', async () => {
const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite()
runtime.recordHookStatusResume(codexRow())
await vi.waitFor(() =>
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toBeDefined()
)
runtime.recordHookStatusResume(codexRow({ state: 'blocked', receivedAt: 11 }))
const [record] = sleepCapture(11).get(PTY_ID)!.records
const slept = runtime.applyLayoutCommand({
type: 'sleep',
workspace: TEST_WORKTREE_ID,
tabId: 'host-tab',
paneKeys: [PANE_KEY],
records: [record!]
})
releaseFirstWrite()
await slept
await runtime['hookStatusResumeWrites']
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toMatchObject({
origin: 'worktree-sleep',
state: 'done'
})
})
it('lets a status accepted after the sleep is saved replace its record, as the window does', async () => {
const { runtime, getSession, releaseFirstWrite } = makeRuntimeWithSlowFirstWrite()
releaseFirstWrite()
await runtime['sleepResumeRecords'].persist(TEST_WORKTREE_ID, sleepCapture(11), [PTY_ID])
runtime.recordHookStatusResume(codexRow({ receivedAt: 30 }))
await runtime['hookStatusResumeWrites']
expect(getSession().sleepingAgentSessionsByPaneKey?.[PANE_KEY]).toMatchObject({
origin: 'live',
state: 'working'
})
})
})
+19 -12
View File
@@ -7,6 +7,7 @@ import {
} from './sidebar-inputs/orca-runtime-sidebar-temporal'
import { OrcaRuntimeWithMigrationCatalog } from './orca-runtime-migration-catalog'
import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../shared/sidebar-inputs-switch'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
import type { RuntimeCommandSurfaceHost } from './orca-runtime-core'
import type {
AgentLaunchTabPublished,
@@ -252,18 +253,24 @@ class OrcaRuntimeService extends OrcaRuntimeSidebarBase {
/** A window's layout command (`layout.apply`), applied to the partitions that hold its tab. */
async applyLayoutCommand(command: WindowLayoutCommand): Promise<LayoutApplyReply> {
const reply = await applyWindowLayoutCommand(
this.requireStore(),
(worktreeId, tabId) => {
if (tabId !== null) {
return this.getWorkspaceSessionHostIdsForTab(worktreeId, tabId)
}
const routed = this.tryGetWorkspaceSessionHostIdForWorktree(worktreeId)
return routed ? [routed] : []
},
command,
(move) => rekeyMovedPaneOwners(this, move)
)
const apply = (): Promise<LayoutApplyReply> =>
applyWindowLayoutCommand(
this.requireStore(),
(worktreeId, tabId) => {
if (tabId !== null) {
return this.getWorkspaceSessionHostIdsForTab(worktreeId, tabId)
}
const routed = this.tryGetWorkspaceSessionHostIdForWorktree(worktreeId)
return routed ? [routed] : []
},
command,
(move) => rekeyMovedPaneOwners(this, move)
)
// A window's sleep or wake lands after the hook statuses main accepted before it, as in the window.
const reply = await (ORCA_LAYOUT_OWNER_IS_MAIN &&
(command.type === 'sleep' || command.type === 'wake')
? this.inResumeRecordOrder(apply)
: apply())
// Sent before the reply, so the window can wait for the frame that shows the change.
return { ...reply, frameSeq: this.workspaceLayoutStream.settledSeq() }
}
@@ -20,6 +20,7 @@ import type { SleepingAgentSessionRecord } from '../../../../shared/agent-sessio
import type { ExecutionHostId } from '../../../../shared/execution-host'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { parsePaneKey } from '../../../../shared/stable-pane-id'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRuntimeWithStopTerminalsForWorktree {
@@ -41,11 +42,14 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu
capturedAt: Date.now()
}),
persist: async (worktreeId, capture, stoppedPtyIds) => {
try {
await this.commitWorkspaceSessionEdit(
const commit = (): Promise<void> =>
this.commitWorkspaceSessionEdit(
this.getWorkspaceSessionHostIdForWorktree(worktreeId),
(session) => applyWorktreeSleepResumeRecords(session, capture, stoppedPtyIds)
)
try {
// Switch point: a status accepted before the sleep is saved is written before it, as in the window.
await (ORCA_LAYOUT_OWNER_IS_MAIN ? this.inResumeRecordOrder(commit) : commit())
} catch (error) {
// Why: the PTYs already stopped; a lost record costs the resume, not the sleep.
console.error('[runtime] could not record slept agent sessions', { worktreeId, error })
@@ -77,9 +81,19 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu
return
}
this.previousHookStatusByPaneKey.set(row.paneKey, status)
this.hookStatusResumeWrites = this.hookStatusResumeWrites
.then(() => this.writeHookStatusResume(status))
.catch((error) => console.warn('[runtime] could not record an agent resume record', error))
this.inResumeRecordOrder(() => this.writeHookStatusResume(status)).catch((error) =>
console.warn('[runtime] could not record an agent resume record', error)
)
}
/** Runs `write` after every resume record write queued before it. */
protected inResumeRecordOrder<T>(write: () => Promise<T>): Promise<T> {
const written = this.hookStatusResumeWrites.then(write)
this.hookStatusResumeWrites = written.then(
() => {},
() => {}
)
return written
}
/** A cleared status leaves the record alone (lost contact is no evidence), as in the window. */
@@ -113,6 +127,7 @@ export abstract class OrcaRuntimeWithWorktreeSleepResumeRecording extends OrcaRu
launchConfigOf: this.paneLaunchConfigOf
})
if (command) {
// Not this.applyLayoutCommand: this runs inside the resume-record queue, which that would await.
await applyWindowLayoutCommand(this.store, hostIdsFor, command)
}
}