Files
orca/src/main/runtime/orca-runtime-structured-session-restore.test.ts
T
641e4792b9 Startup shows chats as Interrupted or Done without reading their history (#32146)
* feat(agent-session-journal): save each chat's open root turn beside its epoch

A restart could only tell whether a chat was cut mid-turn by replaying its
whole journal. journal_sessions now keeps the open root turn
(open_turn_id, open_turn_since), set by the row that opens a turn and
cleared only by a finish the provider reported, in that row's own
transaction. A turn a quit, crash or Stop cut short stays open, which is
what the next launch reads as Interrupted.

Journal schema version 5 adds the two nullable columns with a forward
migration; a new epoch starts with no turn open.

* fix(native-chat): restore chat tabs at startup without replaying any history

Startup opened every chat in the saved tab index and replayed its whole
journal into the main process to learn its status. With hundreds of tabs
and gigabytes of history, Orca froze in GC on every launch.

Startup now lists each restored chat from its record and publishes its
row as Interrupted (a turn was open when its host stopped, or the last
quit's resume offer lists it) or Done, through the status stream and the
agent-status sink, reading only each chat's journal_sessions row and its
newest row's time. A chat's history is read when something opens it; that
open settles what the gone generation left running and publishes the
journal's own row over the restored one.

No other startup path replays chats: restart offers list from their
markers without opening chats, and orchestration mail opens the chats it
must read one at a time. Opened chats are capped at 24 resident folds;
opening one past the cap closes the least recently active folds that are
only a cache, with the idle sweep's own close test.

* Fix lazy chat restoration and cache ownership

* Restore saved chat work across runtime restarts

* Wait for committed structural progress in migration fixture

* Correct restored ownership and bounded journal recovery

* Preserve bounded work selection during legacy recovery

* Complete deferred restart status publication

* Fix restart recovery test fixtures

* Use legal legacy recovery fixtures

* Correct population delete observation

* Bound legacy journal replay in a readonly worker

* Repair legacy worker proof oracles

* Reduce legacy scratch key serialization cost

* Decode population meter namespaces with scratch UTF-16 encoding

* Consolidate structured session imports for CI

* Remove redundant casts and document checked type exceptions

* Correct compiler contracts and retain journal fixture invariants

* Fix checked test fixture contracts after compiler validation

* Name population fixture identity type in exported signature

* Preserve historical quit presentation after rewind

* Fix structured session retry and remote worker delivery

* Preserve settlement and restored status contracts

* Enable legacy chat readiness on standalone hosts

* Repair structured session static checks

* Verify retained submission identity across rewinds

* Show saved chat status without replaying history at startup

* Keep chats closed when startup status finishes loading

* Keep startup owner recovery independent of history loading

* Preserve intentional quit verdicts in saved chat status

* Use a checked host fixture in startup restoration tests

* Group saved startup roster publication and verify scoped status work

* Use checked array access in status cut counters

* Complete checked saved status fixture typing

* Declare portable roster counter contract

* Use current saved chat startup APIs in contracts

* Preserve complete chat tab authority across startup

* Repair checked startup restoration test consumers

* Keep authority fixture module mocks in test entries

* Consolidate authority test fixture imports

* Check restore fixture contracts and recorded queue recovery

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-10-29 10:20:52 -07:00

538 lines
21 KiB
TypeScript

import { partialRuntimeStore } from './runtime-store-partial-fixture'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
import { agentSessionRecordFixture } from '../native-chat/agent-session-record-test-fixture'
import { agentSessionConversationView } from '../../shared/agent-session-conversation-view'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { setStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
import { OrcaRuntimeService } from './orca-runtime'
import type { StructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-host'
import { hostTestState } from '../native-chat/agent-session-wire/structured-agent-session-host-test-harness'
afterEach(() => setStructuredAgentSessionHost(null))
function installRestoreHost(
overrides: Partial<
Pick<
StructuredAgentSessionHost,
| 'reconcileRestartLeases'
| 'seedSessionTabs'
| 'listSessionTabs'
| 'getPersistedVisibleSessionTabIndex'
| 'close'
| 'setSessionTabVisibility'
>
>
) {
const { host } = hostTestState()
const index = overrides.getPersistedVisibleSessionTabIndex?.()
const rows = overrides.listSessionTabs?.() ?? []
const records = new Map(
rows.map((row) => {
const sessionId = row.sessionId.replace(/^(agent-session:)+/, '')
return [
sessionId,
agentSessionConversationView(
agentSessionRecordFixture({
sessionId,
provider: row.agent,
location: { workspaceId: row.workspaceId }
})
)
] as const
})
)
for (const sessionId of index?.sessionIds ?? []) {
if (!records.has(sessionId)) {
records.set(sessionId, agentSessionConversationView(agentSessionRecordFixture({ sessionId })))
}
}
const visible = new Set(index?.present ? index.sessionIds : records.keys())
let present = index?.present ?? visible.size > 0
vi.spyOn(host.deps.store, 'getConversation').mockImplementation((id) => records.get(id) ?? null)
vi.spyOn(host.deps.store, 'isSessionTabVisible').mockImplementation(
(id) => visible.has(id) && records.has(id)
)
vi.spyOn(host.deps.store, 'isSessionTabRetired').mockImplementation(
(id) => present && !visible.has(id)
)
vi.spyOn(host.deps.store, 'getVisibleSessionTabIndex').mockImplementation(() => ({
present,
sessionIds: [...visible]
}))
vi.spyOn(host, 'showSessionTabs').mockResolvedValue(undefined)
const { setSessionTabVisibility, ...methods } = overrides
Object.assign(host, methods)
const listed =
rows.length > 0
? rows
: [...records.values()].map((record) => ({
sessionId: record.sessionId,
workspaceId: record.location.workspaceId,
agent: record.provider
}))
vi.spyOn(host, 'listSessionTabs').mockImplementation(() =>
listed.filter((row) => visible.has(row.sessionId.replace(/^(agent-session:)+/, '')))
)
vi.spyOn(host, 'getPersistedVisibleSessionTabIndex').mockImplementation(() =>
host.deps.store.getVisibleSessionTabIndex()
)
vi.spyOn(host, 'setSessionTabVisibility').mockImplementation(async (...args) => {
await setSessionTabVisibility?.(...args)
const [id, show] = args
present = true
if (show) {
visible.add(id)
} else {
visible.delete(id)
}
})
setStructuredAgentSessionHost(host)
return host
}
describe('structured session cold restoration', () => {
it('publishes a versioned title change and derives listAll titles from the record', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
runtime.projectStructuredAgentSessionTab({
activate: false,
sessionId: 'session-1',
workspaceId: 'workspace-1',
agent: 'claude'
})
const before = await runtime.listMobileSessionTabs('id:workspace-1')
let name: string | undefined = 'auth/login'
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: title projection reads only the record and replacement list from this host stub.
setStructuredAgentSessionHost({
deps: {
store: {
getConversation: () => ({
location: { workspaceId: 'workspace-1' },
conversationName: name
})
}
},
conversationReplacements: () => []
} as never)
const listed = await runtime.listAllMobileSessionTabs()
expect(listed.find((row) => row.worktree === 'workspace-1')?.tabs[0]?.title).toBe('auth/login')
const events: { snapshotVersion: number; tabs: { title: string }[] }[] = []
const unsubscribe = runtime.onMobileSessionTabsChanged((snapshot) => {
if (snapshot.worktree === 'workspace-1') {
events.push(snapshot)
}
})
runtime.refreshStructuredConversationTabTitle('workspace-1', 'session-1')
expect(events.at(-1)?.snapshotVersion).toBeGreaterThan(before.snapshotVersion)
expect(events.at(-1)?.tabs[0]?.title).toBe('auth/login')
name = undefined
runtime.refreshStructuredConversationTabTitle('workspace-1', 'session-1')
expect(events.at(-1)?.tabs[0]?.title).toBe('Claude Chat')
unsubscribe()
})
it('skips every heavy recovery step when no durable session store exists', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const refresh = vi.fn(async () => new Set<string>())
const ensureHost = vi.fn(async () => undefined)
const reconcileRestartLeases = vi.fn(async () => undefined)
const internal = runtime as unknown as {
hasPersistedStructuredAgentSessionStore(): boolean
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
internal.hasPersistedStructuredAgentSessionStore = () => false
internal.refreshMobileSessionPtyRecords = refresh
internal.ensureStructuredAgentSessionHost = ensureHost
setStructuredAgentSessionHost({ reconcileRestartLeases } as never)
await runtime.prepareStructuredAgentSessionStartupRestoration()
expect(ensureHost).not.toHaveBeenCalled()
expect(refresh).not.toHaveBeenCalled()
expect(reconcileRestartLeases).not.toHaveBeenCalled()
})
it('keeps historical journal parsing outside the terminal-safety fence', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const refresh = vi.fn(async () => new Set<string>())
const ensureHost = vi.fn(async () => undefined)
const reconcileRestartLeases = vi.fn(async () => undefined)
const seedSessionTabs = vi.fn(async () => undefined)
const internal = runtime as unknown as {
hasPersistedStructuredAgentSessionStore(): boolean
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.refreshMobileSessionPtyRecords = refresh
internal.ensureStructuredAgentSessionHost = ensureHost
installRestoreHost({ reconcileRestartLeases, seedSessionTabs })
await runtime.prepareStructuredAgentSessionStartupRestoration()
expect(ensureHost).toHaveBeenCalledOnce()
expect(refresh).toHaveBeenCalledOnce()
expect(reconcileRestartLeases).toHaveBeenCalledOnce()
expect(seedSessionTabs).not.toHaveBeenCalled()
})
it('loads records, inventories PTYs, restores ownership, then projects tabs exactly once', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const hydrate = vi.fn()
const refresh = vi.fn(async () => new Set<string>())
const ensureHost = vi.fn(async () => undefined)
const reconcileRestartLeases = vi.fn(async () => undefined)
const seedSessionTabs = vi.fn(async () => undefined)
const internal = runtime as unknown as {
hasPersistedStructuredAgentSessionStore(): boolean
getKnownWorkspaceSessionWorktreeIds(): Set<string>
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(
worktreeId?: string,
options?: { allowAttachedWindow?: boolean; onlyRuntimeOwnedTerminals?: boolean }
): Set<string>
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.getKnownWorkspaceSessionWorktreeIds = () => new Set(['workspace-1'])
internal.hydrateHeadlessMobileSessionTabsFromWorkspaceSession = hydrate
internal.refreshMobileSessionPtyRecords = refresh
internal.ensureStructuredAgentSessionHost = ensureHost
installRestoreHost({
reconcileRestartLeases,
seedSessionTabs,
listSessionTabs: () => []
})
const first = runtime.restoreStructuredAgentSessionTabs()
const second = runtime.restoreStructuredAgentSessionTabs()
expect(second).toBe(first)
await Promise.all([first, second])
expect(hydrate).toHaveBeenCalledWith('workspace-1', {
allowAttachedWindow: true,
onlyRuntimeOwnedTerminals: true
})
expect(hydrate).toHaveBeenCalledWith()
expect(refresh).toHaveBeenCalledOnce()
expect(reconcileRestartLeases).toHaveBeenCalledOnce()
expect(seedSessionTabs).toHaveBeenCalledOnce()
expect(ensureHost).toHaveBeenCalledOnce()
expect(ensureHost.mock.invocationCallOrder[0]).toBeLessThan(
refresh.mock.invocationCallOrder[0] ?? Infinity
)
expect(refresh.mock.invocationCallOrder[0]).toBeLessThan(
reconcileRestartLeases.mock.invocationCallOrder[0] ?? Infinity
)
expect(reconcileRestartLeases.mock.invocationCallOrder[0]).toBeLessThan(
seedSessionTabs.mock.invocationCallOrder[0] ?? Infinity
)
expect(seedSessionTabs.mock.invocationCallOrder[0]).toBeLessThan(
hydrate.mock.invocationCallOrder[0] ?? Infinity
)
})
it('prefers the durable visible-session index after a legacy profile drops agent tabs', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const seedSessionTabs = vi.fn(async () => undefined)
const internal = runtime as unknown as {
store: { getWorkspaceSession: () => unknown }
hasPersistedStructuredAgentSessionStore(): boolean
getKnownWorkspaceSessionWorktreeIds(): Set<string>
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(): Set<string>
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
const getWorkspaceSession = (): WorkspaceSessionState => ({
...getDefaultWorkspaceSession(),
activeRepoId: null,
activeWorktreeId: 'workspace-1',
activeTabId: null,
tabsByWorktree: {},
terminalLayoutsByTabId: {},
unifiedTabs: { 'workspace-1': [] }
})
internal.store = { ...partialRuntimeStore({ getWorkspaceSession }), getWorkspaceSession }
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.getKnownWorkspaceSessionWorktreeIds = () => new Set()
internal.hydrateHeadlessMobileSessionTabsFromWorkspaceSession = () => new Set()
internal.refreshMobileSessionPtyRecords = async () => new Set()
internal.ensureStructuredAgentSessionHost = async () => undefined
installRestoreHost({
reconcileRestartLeases: async () => undefined,
getPersistedVisibleSessionTabIndex: () => ({
present: true,
sessionIds: ['session-survives-rollback']
}),
seedSessionTabs,
listSessionTabs: () => []
})
await runtime.restoreStructuredAgentSessionTabs()
expect(seedSessionTabs).toHaveBeenCalledExactlyOnceWith()
expect(hostTestState().host.getPersistedVisibleSessionTabIndex()).toEqual({
present: true,
sessionIds: ['session-survives-rollback']
})
expect(hostTestState().host.isSessionTabVisible('session-survives-rollback')).toBe(true)
expect((await runtime.listMobileSessionTabs('id:workspace-1')).tabs).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: 'agent-session', sessionId: 'session-survives-rollback' })
])
)
})
it('treats an empty durable visible-session index as authoritative', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const seedSessionTabs = vi.fn(async () => undefined)
const internal = runtime as unknown as {
store: { getWorkspaceSession: () => unknown }
hasPersistedStructuredAgentSessionStore(): boolean
getKnownWorkspaceSessionWorktreeIds(): Set<string>
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(): Set<string>
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
const getWorkspaceSession = (): WorkspaceSessionState => ({
...getDefaultWorkspaceSession(),
activeRepoId: null,
activeWorktreeId: 'workspace-1',
activeTabId: 'agent-session:closed-session',
tabsByWorktree: {},
terminalLayoutsByTabId: {},
activeTabIdByWorktree: { 'workspace-1': 'agent-session:closed-session' },
unifiedTabs: {
'workspace-1': [
{
id: 'agent-session:closed-session',
entityId: 'closed-session',
groupId: 'group-1',
worktreeId: 'workspace-1',
contentType: 'agent-session',
label: 'Codex Chat',
customLabel: null,
color: null,
sortOrder: 0,
createdAt: 1
}
]
}
})
internal.store = { ...partialRuntimeStore({ getWorkspaceSession }), getWorkspaceSession }
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.getKnownWorkspaceSessionWorktreeIds = () => new Set()
internal.hydrateHeadlessMobileSessionTabsFromWorkspaceSession = () => new Set()
internal.refreshMobileSessionPtyRecords = async () => new Set()
internal.ensureStructuredAgentSessionHost = async () => undefined
installRestoreHost({
reconcileRestartLeases: async () => undefined,
getPersistedVisibleSessionTabIndex: () => ({ present: true, sessionIds: [] }),
seedSessionTabs,
listSessionTabs: () => []
})
await runtime.restoreStructuredAgentSessionTabs()
expect(seedSessionTabs).toHaveBeenCalledExactlyOnceWith()
expect(hostTestState().host.getPersistedVisibleSessionTabIndex()).toEqual({
present: true,
sessionIds: []
})
expect(hostTestState().host.isSessionTabVisible('closed-session')).toBe(false)
expect((await runtime.listMobileSessionTabs('id:workspace-1')).tabs).toEqual([])
})
it('normalizes a restored tab id and removes it when closed', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const closeSessionTab = vi.fn(async () => undefined)
const closeStructuredSession = vi.fn(async () => {
const snapshot = await runtime.listMobileSessionTabs('id:workspace-1')
expect(snapshot.tabs.some((tab) => tab.type === 'agent-session')).toBe(false)
})
const setSessionTabVisibility = vi.fn(async () => undefined)
runtime.setNotifier({ closeSessionTab } as never)
const internal = runtime as unknown as {
hasPersistedStructuredAgentSessionStore(): boolean
getKnownWorkspaceSessionWorktreeIds(): Set<string>
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(): Set<string>
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.getKnownWorkspaceSessionWorktreeIds = () => new Set()
internal.hydrateHeadlessMobileSessionTabsFromWorkspaceSession = () => new Set()
internal.refreshMobileSessionPtyRecords = async () => new Set()
internal.ensureStructuredAgentSessionHost = async () => undefined
installRestoreHost({
reconcileRestartLeases: async () => undefined,
seedSessionTabs: async () => undefined,
close: closeStructuredSession,
setSessionTabVisibility,
listSessionTabs: () => [
{
sessionId: 'agent-session:agent-session:restored-session',
workspaceId: 'workspace-1',
agent: 'codex'
}
]
})
runtime.syncWindowGraph(1, {
tabs: [],
leaves: [],
mobileSessionTabs: [
{
worktree: 'workspace-1',
publicationEpoch: 'renderer-restored',
snapshotVersion: 1,
activeGroupId: 'group-1',
activeTabId: 'terminal-tab::leaf-1',
activeTabType: 'terminal',
tabGroups: [{ id: 'group-1', activeTabId: 'terminal-tab', tabOrder: ['terminal-tab'] }],
tabs: [
{
type: 'terminal',
id: 'terminal-tab::leaf-1',
parentTabId: 'terminal-tab',
leafId: 'leaf-1',
title: 'Terminal',
isActive: true
},
{
type: 'terminal',
id: 'terminal-tab::leaf-2',
parentTabId: 'terminal-tab',
leafId: 'leaf-2',
title: 'Terminal',
isActive: false
}
]
}
]
})
await runtime.restoreStructuredAgentSessionTabs()
const restored = await runtime.listMobileSessionTabs('id:workspace-1')
expect(restored.tabs).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'terminal',
id: 'terminal-tab::leaf-1'
}),
expect.objectContaining({
type: 'terminal',
id: 'terminal-tab::leaf-2'
}),
expect.objectContaining({
type: 'agent-session',
id: 'agent-session:restored-session',
sessionId: 'restored-session'
})
])
)
expect(restored.tabs).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'agent-session',
id: 'agent-session:agent-session:restored-session'
})
])
)
expect(restored.tabGroups?.[0]?.tabOrder).toEqual([
'terminal-tab',
'agent-session:restored-session'
])
await runtime.closeMobileSessionTab('id:workspace-1', 'agent-session:restored-session', {
reason: 'user'
})
expect(closeSessionTab).toHaveBeenCalledWith('agent-session:restored-session', 'workspace-1')
expect(closeStructuredSession).toHaveBeenCalledWith('restored-session', 'user-close')
expect(setSessionTabVisibility).toHaveBeenCalledWith('restored-session', false)
expect(setSessionTabVisibility.mock.invocationCallOrder[0]).toBeLessThan(
closeStructuredSession.mock.invocationCallOrder[0]!
)
const closed = await runtime.listMobileSessionTabs('id:workspace-1')
expect(closed.tabs.map((tab) => tab.id)).toEqual([
'terminal-tab::leaf-1',
'terminal-tab::leaf-2'
])
expect(closed.tabGroups?.[0]?.tabOrder).toEqual(['terminal-tab'])
})
it('publishes restored Claude tabs with the Claude title', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
const project = vi.spyOn(runtime, 'projectStructuredAgentSessionTab')
const internal = runtime as unknown as {
hasPersistedStructuredAgentSessionStore(): boolean
getKnownWorkspaceSessionWorktreeIds(): Set<string>
hydrateHeadlessMobileSessionTabsFromWorkspaceSession(): Set<string>
refreshMobileSessionPtyRecords(): Promise<Set<string> | null>
ensureStructuredAgentSessionHost(): Promise<void>
}
internal.hasPersistedStructuredAgentSessionStore = () => true
internal.getKnownWorkspaceSessionWorktreeIds = () => new Set()
internal.hydrateHeadlessMobileSessionTabsFromWorkspaceSession = () => new Set()
internal.refreshMobileSessionPtyRecords = async () => new Set()
internal.ensureStructuredAgentSessionHost = async () => undefined
installRestoreHost({
reconcileRestartLeases: async () => undefined,
seedSessionTabs: async () => undefined,
listSessionTabs: () => [
{
sessionId: 'agent-session:agent-session:restored-claude',
workspaceId: 'workspace-1',
agent: 'claude'
}
]
})
await runtime.restoreStructuredAgentSessionTabs()
expect(project).toHaveBeenCalledWith({
workspaceId: 'workspace-1',
sessionId: 'restored-claude',
agent: 'claude',
activate: false,
notify: false
})
const restored = await runtime.listMobileSessionTabs('id:workspace-1')
expect(restored.tabs).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'agent-session',
id: 'agent-session:restored-claude',
title: 'Claude Chat',
agent: 'claude'
})
])
)
})
it('commits the host close when the renderer already removed the structured tab', async () => {
const runtime = new OrcaRuntimeService(partialRuntimeStore())
runtime.setNotifier({
closeSessionTab: vi.fn(async () => {
throw new Error('session_tab_not_found')
})
} as never)
await runtime.publishStructuredAgentSessionTab({
workspaceId: 'workspace-1',
sessionId: 'session-1',
agent: 'codex',
activate: true
})
await runtime.closeMobileSessionTab('id:workspace-1', 'agent-session:session-1', {
reason: 'user'
})
const snapshot = await runtime.listMobileSessionTabs('id:workspace-1')
expect(snapshot.tabs).toEqual([])
})
})