The terminal server and desktop finish the two-pane terminal repair before startup continues (dark, layout-owner build only) (#30429)

* refactor(orcad): move startOrcad's option and handle types into their own module

Move-only: frees room in orcad-entry.ts for the layout-owner load repair.

* fix(orcad): run main's duplicate-terminal pane repair at orcad startup (layout-owner build only)

orcad builds its own runtime and never called the load-time repair, so an
orcad host kept a terminal drawn in two panes.

* test(orcad): type the hoisted test state without an assertion

* fix(orcad): await the startup pane repair so it lands before reconcile and RPC

* fix(startup): desktop awaits the duplicate-terminal repair before the window and RPC start (layout-owner build only)
This commit is contained in:
Jinwoo Hong
2026-10-18 21:25:53 -04:00
committed by GitHub
parent 1ad8755fa8
commit 8ffd3742ed
8 changed files with 551 additions and 35 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import type { OrcadOptions } from './orcad-entry'
import type { OrcadOptions } from './orcad-start-contract'
import { DEFAULT_WS_PORT } from '../runtime/runtime-rpc/runtime-rpc-pairing-types'
import { ORCAD_LOOPBACK_BIND_HOST } from './orcad-bind-address'
import { getServeOptionValidationError } from '../../shared/serve-option-validation'
@@ -0,0 +1,306 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, expect, it, vi, type Mock } from 'vitest'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import type { PersistedState } from '../../shared/persisted-state-types'
import { agentHookServer } from '../agent-hooks/server'
import { ProfileStateSqliteAuthority } from '../persistence/profile-state/profile-state-sqlite-authority'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID,
openTopologyStore
} from '../persistence/terminal-topology/terminal-topology-profile-fixture'
import { DelayedAuthority } from '../persistence/loading-store/profile-state-delayed-authority-fixture'
import { Store } from '../persistence/loading-store/store'
import {
leafIds,
WindowSession
} from '../persistence/terminal-topology/terminal-topology-window-session-fixture'
const state = vi.hoisted(() => {
const value: {
root: string
store: Store | null
layoutOwnerIsMain: boolean
rekeyWorkerTerminalResourcePaneKey: Mock<() => number>
/** What a startup step saw on disk; set by a test that watches the order. */
onStartupStep: ((step: string) => void) | null
} = {
root: '',
store: null,
layoutOwnerIsMain: true,
rekeyWorkerTerminalResourcePaneKey: vi.fn(() => 1),
onStartupStep: null
}
return value
})
vi.mock('../../shared/layout-owner-switch', () => ({
get ORCA_LAYOUT_OWNER_IS_MAIN() {
return state.layoutOwnerIsMain
}
}))
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
// Why: startup reconciles the user-global agent configs; keep it off this machine's real home.
vi.mock('../codex/codex-hook-reconcile', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
startCodexHooks: vi.fn()
}))
vi.mock('../agent-hooks/managed-agent-hook-controls', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
installManagedAgentHooks: vi.fn(async () => [])
}))
vi.mock('../agent-hooks/local-agent-cli-presence', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
hydrateAgentCliShellPath: async () => {}
}))
vi.mock('../agent-hooks/agent-home-login-shell', () => ({
adoptLoginShellAgentHomes: async () => {}
}))
vi.mock('../opencode/opencode-status-plugin-startup-refresh', () => ({
refreshInstalledOpenCodeStatusPlugins: vi.fn()
}))
vi.mock('./orcad-app-paths', () => ({
resolveOrcadInstallRoot: () => state.root,
resolveOrcadPath: () => state.root,
resolveUserDataPath: () => state.root
}))
vi.mock('./orcad-plaintext-credential-migration', () => ({
orcadCredentialRoot: () => state.root,
migratePlaintextCredentials: () => ({ sealed: 0, failed: 0 })
}))
vi.mock('./orcad-account-services', () => ({ createOrcadAccountServices: () => null }))
vi.mock('./orcad-browser-provider', () => ({ resolveOrcadBrowserProvider: async () => null }))
vi.mock('./orcad-instance-lock', () => ({
acquireOrcadInstanceLock: () => ({
path: join(state.root, 'orcad.lock'),
record: { pid: process.pid, startedAtMs: null, nonce: 'repair-instance' },
release() {}
})
}))
vi.mock('./orcad-daemon-supervision', () => ({
startOrcadDaemon: async () => {},
stopOrcadDaemon: async () => {}
}))
vi.mock('./orcad-health', () => ({ collectOrcadHealth: async () => ({}) }))
vi.mock('./orcad-automations', () => ({
startOrcadAutomations: () => {},
stopOrcadAutomationScheduler: () => {},
orcadAutomationsKeepHostBusy: () => false
}))
vi.mock('../runtime/agent-state-rules/agent-state-rules-live-update', () => ({
startAgentStateRulesLiveUpdates: () => {}
}))
vi.mock('../daemon/daemon-provider-state', () => ({ daemonOwnsFreshPersistentPtys: () => false }))
vi.mock('../ipc/pty/register-headless-runtime', () => ({
registerHeadlessPtyRuntime: async () => {}
}))
vi.mock('../ipc/pty/provider/registry', () => ({ getLocalPtyProvider: () => null }))
// The real Store orcad loads, on a profile holding one terminal drawn in two panes.
vi.mock('./orcad-profile-state-startup', () => ({
createOrcadProfileStateStartup: async () => ({
store: state.store,
authority: {
backend: 'sqlite',
classification: 'neither',
authority_mode: 'sqlite-candidate',
runtime: 'orcad',
migrated: false
}
})
}))
vi.mock('../orca-profiles/profile-index-store', () => ({
initOrcaProfilePaths() {},
ensureActiveOrcaProfile: () => ({
dataFile: join(state.root, 'profile.json'),
stateDatabaseFile: join(state.root, 'profile-state.db'),
profile: { id: 'repair-profile' }
})
}))
vi.mock('../ssh/ssh-host-key-store', () => ({ initSshHostKeyStoreFile() {} }))
vi.mock('../server/serve-readiness', () => ({
ServeReadinessPublisher: class {
async publish() {}
}
}))
vi.mock('../runtime/push/desktop-push-service', () => ({
DesktopPushService: { create: () => null }
}))
vi.mock('../runtime/orca-runtime', () => ({
OrcaRuntimeService: class {
readonly sidebarFacts = null
getRuntimeId() {
return 'repair-runtime'
}
getExistingOrchestrationDb() {
return { rekeyWorkerTerminalResourcePaneKey: state.rekeyWorkerTerminalResourcePaneKey }
}
rehydrateClientHostedBrowserPages() {}
restoreHostAccountLaunchEnv() {}
async refreshRestoredOrchestrationAuthority() {}
async reconcileLegacyWorkerTerminals() {
state.onStartupStep?.('reconcile')
}
async installPendingOrcadMigrationShellHistory() {}
async stopLegacyWorkerTerminalRecovery() {}
async closeNotificationDismissals() {}
syncWindowGraph() {}
}
}))
vi.mock('../runtime/runtime-rpc', () => ({
OrcaRuntimeRpcServer: class {
async start() {
state.onStartupStep?.('rpc.start')
}
async stop() {}
getWebSocketEndpoint() {
return null
}
}
}))
const TAB = 'tab-1'
const KEPT = '11111111-1111-4111-8111-111111111111'
const DUPLICATE = '22222222-2222-4222-8222-222222222222'
const directories: string[] = []
beforeEach(() => {
// startOrcad canonicalizes this; stubbing restores the prior value after each test.
vi.stubEnv('ORCA_USER_DATA_PATH', '')
})
afterEach(() => {
state.layoutOwnerIsMain = true
state.onStartupStep = null
state.rekeyWorkerTerminalResourcePaneKey.mockClear()
vi.unstubAllEnvs()
vi.restoreAllMocks()
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
function temporaryDirectory(prefix: string): string {
const directory = mkdtempSync(join(tmpdir(), prefix))
directories.push(directory)
return directory
}
/** The stored shape a window save leaves behind: one terminal drawn in two panes of one tab. */
function duplicatePtyProfile(): string {
const window = new WindowSession(getDefaultWorkspaceSession())
window.addTab(FIXTURE_GIT_WORKTREE_ID, TAB, KEPT)
window.setLayout(
TAB,
{
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: KEPT },
second: { type: 'leaf', leafId: DUPLICATE }
},
{ [KEPT]: 'pty-1', [DUPLICATE]: 'pty-1' }
)
return JSON.stringify({
...JSON.parse(emptyTerminalSessionProfile()),
workspaceSession: window.session
})
}
function storedLeafIds(directory: string): string[] {
const reader = new ProfileStateSqliteAuthority(join(directory, 'profile-state.db'), 'topology')
try {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the test wrote this profile as PersistedState.
const stored = JSON.parse(reader.readSerializedState() ?? '{}') as PersistedState
return leafIds(stored.workspaceSession?.terminalLayoutsByTabId?.[TAB]?.root)
} finally {
reader.close()
}
}
async function seedDuplicatePtyProfile(): Promise<string> {
state.root = temporaryDirectory('orcad-duplicate-pty-repair-')
const profileDirectory = temporaryDirectory('orcad-duplicate-pty-profile-')
state.store = await openTopologyStore(profileDirectory, duplicatePtyProfile())
return profileDirectory
}
it('repairs a stored duplicate-terminal pane at orcad startup, moving owners before the write', async () => {
const profileDirectory = await seedDuplicatePtyProfile()
const diskAtTransfer: string[][] = []
const transfer = vi
.spyOn(agentHookServer, 'transferPaneAuthority')
.mockImplementation(() => diskAtTransfer.push(storedLeafIds(profileDirectory)))
const { startOrcad } = await import('./orcad-entry')
const host = await startOrcad({ noPairing: true, json: true })
try {
await vi.waitFor(() => expect(storedLeafIds(profileDirectory)).toEqual([KEPT]))
expect(transfer).toHaveBeenCalledExactlyOnceWith(
`${TAB}:${DUPLICATE}`,
`${TAB}:${KEPT}`,
'pty-1',
expect.any(Number),
{ authorityVerified: true }
)
expect(state.rekeyWorkerTerminalResourcePaneKey).toHaveBeenCalledExactlyOnceWith({
fromPaneKey: `${TAB}:${DUPLICATE}`,
toPaneKey: `${TAB}:${KEPT}`
})
// The status moved while disk still held both panes.
expect(diskAtTransfer).toEqual([[KEPT, DUPLICATE]])
} finally {
await host.stop()
}
})
it('leaves the stored panes alone when the build keeps the window as layout owner', async () => {
state.layoutOwnerIsMain = false
const profileDirectory = await seedDuplicatePtyProfile()
const transfer = vi.spyOn(agentHookServer, 'transferPaneAuthority')
const { startOrcad } = await import('./orcad-entry')
const host = await startOrcad({ noPairing: true, json: true })
await host.stop()
expect(transfer).not.toHaveBeenCalled()
expect(state.rekeyWorkerTerminalResourcePaneKey).not.toHaveBeenCalled()
expect(storedLeafIds(profileDirectory)).toEqual([KEPT, DUPLICATE])
})
it('writes the repair before reconcile and RPC even when a profile write is in flight', async () => {
state.root = temporaryDirectory('orcad-duplicate-pty-repair-')
const profileDirectory = temporaryDirectory('orcad-duplicate-pty-profile-')
const inner = new ProfileStateSqliteAuthority(
join(profileDirectory, 'profile-state.db'),
'topology'
)
inner.writeSerializedState(Buffer.from(duplicatePtyProfile()))
const authority = new DelayedAuthority(inner)
const store = new Store({
dataFile: join(profileDirectory, 'orca-data.json'),
profileStateAuthority: authority
})
await store.flushPendingOrThrowAsync()
state.store = store
vi.spyOn(agentHookServer, 'transferPaneAuthority').mockImplementation(() => {})
const diskAtStep: Record<string, string[]> = {}
state.onStartupStep = (step) => (diskAtStep[step] = storedLeafIds(profileDirectory))
// An earlier profile write is still in flight; the repair queues behind it.
const held = authority.pause()
store.updateSettings({ agentStatusHooksEnabled: false })
const heldWrite = store.flushPendingOrThrowAsync()
await held.started.promise
const { startOrcad } = await import('./orcad-entry')
const startup = startOrcad({ noPairing: true, json: true })
await new Promise((resolve) => setTimeout(resolve, 200))
held.finish.resolve()
await heldWrite
const host = await startup
try {
expect(diskAtStep).toEqual({ reconcile: [KEPT], 'rpc.start': [KEPT] })
} finally {
await host.stop()
}
})
+8 -27
View File
@@ -13,7 +13,6 @@
import { join } from 'node:path'
import process from 'node:process'
import { getAppEnvironment } from '../../shared/app-environment'
import type { ServeReadiness } from '../server/serve-readiness'
import { resolveOrcadInstallRoot, resolveUserDataPath } from './orcad-app-paths'
import { prepareOrcadCliLauncher } from './orcad-cli-launcher'
import {
@@ -32,7 +31,7 @@ import { ORCAD_USAGE, parseArgs, resolveOrcadWebClientRoot } from './orcad-comma
import type { OrcadRuntimeCleanup } from './orcad-runtime-lifetime'
import { installOrcadStopRequestListeners } from './orcad-stop-request-listener'
import { prepareOrcadManagedStop } from './orcad-managed-stop-admission'
import type { OrcadManagedStopContext } from '../../shared/orcad-stop-request'
import type { OrcadHandle, OrcadOptions } from './orcad-start-contract'
import { beginOrcadIdleExit, bindOrcadIdleShutdown } from './orcad-managed-idle-exit-host'
import { orcadAutomationsKeepHostBusy, startOrcadAutomations } from './orcad-automations'
import {
@@ -43,36 +42,12 @@ import { stampRuntimeSourceEnv } from '../../shared/runtime-source-env'
import { adoptWslHostLoginEnvironment } from './orcad-wsl-host-login-environment'
import { installOrcadProcessErrorGuards } from './orcad-process-error-guards'
import { orcadWsPortOptions } from './orcad-ws-port-policy'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
export { parseArgs }
let closeOrcadObservability = (): void => {}
export type OrcadOptions = {
port?: number
json?: boolean
noPairing?: boolean
pairingAddress?: string
/** Desktop `orca serve` parity: a mobile-scoped offer with a terminal QR. */
mobilePairing?: boolean
/** Lets the paired runtime client drive this machine's desktop (computer.*). */
grantDesktopControl?: boolean
/** Desktop `orca serve` parity: print only the ephemeral-VM recipe line. */
recipeJson?: boolean
projectRoot?: string
/** Literal IP to bind. Defaults to loopback; see orcad-bind-address.ts. */
bind?: string
/** The browser client's directory; the slot carries none, so `orca serve` passes the app's. */
webClientRoot?: string
}
export type OrcadHandle = {
readiness: ServeReadiness
/** What an instance-bound stop request must name to stop this process. */
managedStop: OrcadManagedStopContext
stop(): Promise<void>
}
/**
* Boot the runtime and serve RPC. Resolves once the transport is listening and the
* readiness payload has been published, mirroring the desktop `--serve` contract byte
@@ -289,6 +264,12 @@ async function startOrcadRuntime(
startAgentStateRulesLiveUpdates(profileStore, (rules) =>
console.info(`[orcad] agent state rules ${rules.version} (${rules.source})`)
)
// Switch point: main's load repair, here too, now that this runtime can move pane owners.
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
const { repairDuplicatePtyPanesOnStart } = await import('../runtime/moved-pane-owner-rekey')
// Awaited: it queues behind any in-flight write, and must land before reconcile and RPC.
await repairDuplicatePtyPanesOnStart(profileStore, runtime)
}
// Why here too and not only on the desktop: nothing else republishes `session.tabs` when a
// pane's status row changes, and orcad's whole job is serving paired clients.
+1 -1
View File
@@ -2,7 +2,7 @@ import type { ServeReadiness } from '../server/serve-readiness'
import type { OrcaRuntimeRpcServer } from '../runtime/runtime-rpc'
import { buildServePairingReadiness } from '../server/serve-pairing-readiness'
import type { OrcadHealth } from './orcad-health'
import type { OrcadOptions } from './orcad-entry'
import type { OrcadOptions } from './orcad-start-contract'
/** The readiness payload orcad publishes once its RPC transport is listening. */
export async function buildOrcadServeReadiness(input: {
+27
View File
@@ -0,0 +1,27 @@
import type { ServeReadiness } from '../server/serve-readiness'
import type { OrcadManagedStopContext } from '../../shared/orcad-stop-request'
export type OrcadOptions = {
port?: number
json?: boolean
noPairing?: boolean
pairingAddress?: string
/** Desktop `orca serve` parity: a mobile-scoped offer with a terminal QR. */
mobilePairing?: boolean
/** Lets the paired runtime client drive this machine's desktop (computer.*). */
grantDesktopControl?: boolean
/** Desktop `orca serve` parity: print only the ephemeral-VM recipe line. */
recipeJson?: boolean
projectRoot?: string
/** Literal IP to bind. Defaults to loopback; see orcad-bind-address.ts. */
bind?: string
/** The browser client's directory; the slot carries none, so `orca serve` passes the app's. */
webClientRoot?: string
}
export type OrcadHandle = {
readiness: ServeReadiness
/** What an instance-bound stop request must name to stop this process. */
managedStop: OrcadManagedStopContext
stop(): Promise<void>
}
@@ -0,0 +1,202 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import type { PersistedState } from '../../shared/persisted-state-types'
import { agentHookServer } from '../agent-hooks/server'
import { DelayedAuthority } from '../persistence/loading-store/profile-state-delayed-authority-fixture'
import { Store } from '../persistence/loading-store/store'
import { ProfileStateSqliteAuthority } from '../persistence/profile-state/profile-state-sqlite-authority'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID
} from '../persistence/terminal-topology/terminal-topology-profile-fixture'
import {
leafIds,
WindowSession
} from '../persistence/terminal-topology/terminal-topology-window-session-fixture'
const fixture = vi.hoisted(() => {
const value: { state: Record<string, unknown>; userData: string } = {
// Account services are stubs: this file pins only the repair's place in readiness.
state: {
store: null,
stats: {},
runtime: null,
claudeAccounts: {},
codexAccounts: {},
rateLimits: {}
},
userData: ''
}
return value
})
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('electron', () => ({
app: { isPackaged: false, on: vi.fn(), once: vi.fn(), getPath: () => fixture.userData },
nativeTheme: {}
}))
vi.mock('@electron-toolkit/utils', () => ({ is: { dev: false } }))
vi.mock('./main-process-state', () => ({ mainProcessState: fixture.state }))
vi.mock('../runtime/orca-runtime', () => ({
OrcaRuntimeService: class {
readonly sidebarFacts = null
getExistingOrchestrationDb() {
return null
}
rehydrateClientHostedBrowserPages() {}
setAgentBrowserBridge() {}
setEmulatorBridge() {}
setArtifactService() {}
setSkillCloudService() {}
setAccountServices() {}
setCommitMessageAgentEnvironmentResolvers() {}
}
}))
vi.mock('../runtime/agent-session-claim-identity', () => ({
loadAgentSessionClaimSigner: () => null
}))
vi.mock('../ai-vault-search/session-search-enablement', () => ({
applySessionSearchSettingsChange: vi.fn(),
installChildSessionSearchService: () => null
}))
vi.mock('../runtime/agent-state-rules/agent-state-rules-live-update', () => ({
startAgentStateRulesLiveUpdates: () => {}
}))
vi.mock('../artifacts/artifact-cloud-service', () => ({ ArtifactCloudService: class {} }))
vi.mock('../skills/skill-cloud-service', () => ({ SkillCloudService: class {} }))
vi.mock('./wsl-orcad-account-env-wiring', () => ({ installWslOrcadAccountShim: vi.fn() }))
vi.mock('../codex/codex-hook-reconcile', () => ({ startCodexHooks: vi.fn() }))
vi.mock('../star-nag/service', () => ({
StarNagService: class {
start = vi.fn()
registerIpcHandlers = vi.fn()
}
}))
vi.mock('../browser/agent-browser-bridge', () => ({
AgentBrowserBridge: class {
sweepOrphanedSessions = vi.fn()
}
}))
vi.mock('../emulator/emulator-bridge', () => ({ EmulatorBridge: class {} }))
vi.mock('../runtime/rpc/dispatcher', () => ({ RpcDispatcher: class {} }))
vi.mock('../browser/browser-manager', () => ({
browserManager: { setBrowserGuestStateChangedListener: vi.fn() }
}))
vi.mock('../browser/browser-client-page-automation-runtime', () => ({
configureBrowserClientPageAutomationRuntime: vi.fn()
}))
vi.mock('../crash-reporting/process-gone-diagnostics', () => ({
startPreGoneCrashSampling: vi.fn()
}))
vi.mock('./main-window-lifecycle-flags', () => ({ recordProcessGoneCrash: vi.fn() }))
vi.mock('./gpu-lifecycle', () => ({ handleGpuChildCrash: vi.fn() }))
vi.mock('./configure-process', () => ({ shouldInstallManagedHooks: () => false }))
vi.mock('./main-process-observers', () => ({ initializeMainProcessObservers: vi.fn() }))
vi.mock('./branch-rename-hook', () => ({ installFirstWorkRenameHookSubscription: vi.fn() }))
vi.mock('./main-process-account-services', () => ({
initializeMainProcessAccountServices: vi.fn()
}))
vi.mock('./main-process-automations', () => ({ initializeMainProcessAutomations: vi.fn() }))
vi.mock('./main-process-plugins', () => ({ initializeMainProcessPlugins: vi.fn() }))
vi.mock('../worktree-trash', () => ({
collectWorktreeTrashSweepRoots: vi.fn(),
sweepStaleWorktreeTrash: vi.fn()
}))
vi.mock('./worktree-removal-records-load', () => ({ loadWorktreeRemovalRecordsForStore: vi.fn() }))
vi.mock('./first-window-deferral', () => ({ runAfterFirstWindowShown: vi.fn() }))
vi.mock('./startup-diagnostics', () => ({ logStartupMilestone: vi.fn() }))
vi.mock('../opencode/opencode-status-plugin-startup-refresh', () => ({
refreshInstalledOpenCodeStatusPlugins: vi.fn()
}))
vi.mock('../orcad/orcad-artifact-cache-retention', () => ({
pruneDesktopOrcadArtifactCache: vi.fn()
}))
import { initializeReadyRuntimeServices } from './main-process-ready-runtime'
const TAB = 'tab-1'
const KEPT = '11111111-1111-4111-8111-111111111111'
const DUPLICATE = '22222222-2222-4222-8222-222222222222'
const directories: string[] = []
const stores: Store[] = []
afterEach(async () => {
for (const store of stores.splice(0)) {
await store.freezeWritesAsync()
}
vi.restoreAllMocks()
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
function duplicatePtyProfile(): string {
const window = new WindowSession(getDefaultWorkspaceSession())
window.addTab(FIXTURE_GIT_WORKTREE_ID, TAB, KEPT)
window.setLayout(
TAB,
{
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: KEPT },
second: { type: 'leaf', leafId: DUPLICATE }
},
{ [KEPT]: 'pty-1', [DUPLICATE]: 'pty-1' }
)
return JSON.stringify({
...JSON.parse(emptyTerminalSessionProfile()),
workspaceSession: window.session
})
}
function storedLeafIds(directory: string): string[] {
const reader = new ProfileStateSqliteAuthority(join(directory, 'profile-state.db'), 'topology')
try {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the test wrote this profile as PersistedState.
const stored = JSON.parse(reader.readSerializedState() ?? '{}') as PersistedState
return leafIds(stored.workspaceSession?.terminalLayoutsByTabId?.[TAB]?.root)
} finally {
reader.close()
}
}
// Ready services finish before the window opens and RPC starts (main-process-ready.ts:24-31).
it('finishes the duplicate-terminal repair before ready services resolve, even behind a held write', async () => {
const directory = mkdtempSync(join(tmpdir(), 'desktop-duplicate-pty-repair-'))
directories.push(directory)
fixture.userData = directory
const inner = new ProfileStateSqliteAuthority(join(directory, 'profile-state.db'), 'topology')
inner.writeSerializedState(Buffer.from(duplicatePtyProfile()))
const authority = new DelayedAuthority(inner)
const store = new Store({
dataFile: join(directory, 'orca-data.json'),
profileStateAuthority: authority
})
stores.push(store)
await store.flushPendingOrThrowAsync()
fixture.state.store = store
vi.spyOn(agentHookServer, 'transferPaneAuthority').mockImplementation(() => {})
// An earlier profile write is still in flight; the repair queues behind it.
const held = authority.pause()
store.updateSettings({ agentStatusHooksEnabled: false })
const heldWrite = store.flushPendingOrThrowAsync()
await held.started.promise
let atReady: { memory: string[]; disk: string[] } | null = null
const ready = initializeReadyRuntimeServices().then(() => {
atReady = {
memory: leafIds(store.getWorkspaceSession().terminalLayoutsByTabId[TAB]?.root),
disk: storedLeafIds(directory)
}
})
await new Promise((resolve) => setTimeout(resolve, 200))
held.finish.resolve()
await heldWrite
await ready
expect(atReady).toEqual({ memory: [KEPT], disk: [KEPT] })
})
@@ -34,6 +34,8 @@ import { runAfterFirstWindowShown } from './first-window-deferral'
import { logStartupMilestone } from './startup-diagnostics'
import { refreshInstalledOpenCodeStatusPlugins } from '../opencode/opencode-status-plugin-startup-refresh'
import { pruneDesktopOrcadArtifactCache } from '../orcad/orcad-artifact-cache-retention'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
import { repairDuplicatePtyPanesOnStart } from '../runtime/moved-pane-owner-rekey'
// Headless serve never opens a window, so the sweep still has to run off a timer there.
const WORKTREE_TRASH_SWEEP_FALLBACK_MS = 15_000
@@ -143,5 +145,9 @@ export async function initializeReadyRuntimeServices(): Promise<void> {
void handleGpuChildCrash(details.reason, details.exitCode ?? null, crashedAt)
}
})
// Switch point: awaited, as it queues behind in-flight writes and every later phase must read its tree.
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
await repairDuplicatePtyPanesOnStart(store, runtime)
}
logStartupMilestone('services-initialized')
}
@@ -42,8 +42,6 @@ import {
} from '../runtime/agent-status-observed-pane-identity'
import { startAgentStateRulesLiveUpdates } from '../runtime/agent-state-rules/agent-state-rules-live-update'
import { recordDurableCrashBreadcrumb } from '../crash-reporting/durable-crash-breadcrumb'
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../shared/layout-owner-switch'
import { repairDuplicatePtyPanesOnStart } from '../runtime/moved-pane-owner-rekey'
export function getDesktopWindowStatus(): RuntimeDesktopWindowStatus {
const activation = state.desktopActivationGate
@@ -148,10 +146,6 @@ export function initializeMainProcessRuntime(): OrcaRuntimeService {
recordDurableCrashBreadcrumb('agent_state_rules_active', rules)
)
state.runtime = runtime
// Switch point: main's load repair is written only now that a runtime can move pane owners.
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
void repairDuplicatePtyPanesOnStart(store, runtime)
}
runtime.sidebarFacts?.attachSidebarAgentStatusStore(agentHookServer)
agentHookServer.subscribeEnrichedStatus((enriched) =>
recordObservedAgentStatusPaneIdentity(observedPaneIdentities, enriched.paneKey, runtime)