From 0a8e2b17df8dd2c84d91c87c29fdc92872f69e04 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:47:31 -0700 Subject: [PATCH] perf(codex): skip migration scans for nonshared reattaches --- src/main/codex/codex-pane-account-registry.ts | 7 + ...dex-session-migration-recent-exits.test.ts | 17 +++ .../codex-session-migration-recent-exits.ts | 52 +++++++ .../codex-session-migration-scheduler.test.ts | 140 ++++++++++++++++++ .../codex-session-migration-scheduler.ts | 76 ++++------ .../codex/codex-stale-pane-accounts.test.ts | 12 ++ src/main/index.ts | 23 +++ src/main/ipc/pty.test.ts | 19 ++- src/main/ipc/pty.ts | 80 +++++++++- 9 files changed, 376 insertions(+), 50 deletions(-) create mode 100644 src/main/codex/codex-session-migration-recent-exits.test.ts create mode 100644 src/main/codex/codex-session-migration-recent-exits.ts diff --git a/src/main/codex/codex-pane-account-registry.ts b/src/main/codex/codex-pane-account-registry.ts index 359bc3e9e8f..595f1d3e767 100644 --- a/src/main/codex/codex-pane-account-registry.ts +++ b/src/main/codex/codex-pane-account-registry.ts @@ -225,6 +225,13 @@ export function getCodexPaneAccount(ptyId: string): CodexPaneAccountRecord | nul return readRegistry().panes[ptyId] ?? null } +/** `custom-home` stays conservative because it can mask a non-comparable shared-home route. */ +export function isCodexPaneHomeRouteProvenAwayFromSharedHome( + route: CodexPaneHomeRoute | undefined +): boolean { + return route === 'real-home' || route === 'account-home' || route === 'wsl-home' +} + /** * Reports the lane each given PTY launched from, omitting panes with no record. * diff --git a/src/main/codex/codex-session-migration-recent-exits.test.ts b/src/main/codex/codex-session-migration-recent-exits.test.ts new file mode 100644 index 00000000000..84d08768402 --- /dev/null +++ b/src/main/codex/codex-session-migration-recent-exits.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import { CodexSessionMigrationRecentExits } from './codex-session-migration-recent-exits' + +describe('CodexSessionMigrationRecentExits', () => { + it('refreshes a rerecorded ID before evicting the oldest entry', () => { + const exits = new CodexSessionMigrationRecentExits() + for (let index = 0; index < 256; index += 1) { + exits.record(`pty-${index}`, index + 1) + } + + exits.record('pty-0', 500) + exits.record('pty-overflow', 501) + + expect(exits.matchesAfter('pty-0', 499)).toBe(true) + expect(exits.matchesAfter('pty-1', 0)).toBe(false) + }) +}) diff --git a/src/main/codex/codex-session-migration-recent-exits.ts b/src/main/codex/codex-session-migration-recent-exits.ts new file mode 100644 index 00000000000..58038e7f795 --- /dev/null +++ b/src/main/codex/codex-session-migration-recent-exits.ts @@ -0,0 +1,52 @@ +const RETENTION_MS = 60_000 +const MAX_ENTRIES = 256 + +type RecentPtyExit = { sequence: number; recordedAt: number } + +export class CodexSessionMigrationRecentExits { + private readonly exits = new Map() + + record(leaseId: string, sequence: number): void { + const now = Date.now() + for (const [id, exit] of this.exits) { + if (now - exit.recordedAt > RETENTION_MS) { + this.exits.delete(id) + } + } + this.exits.delete(leaseId) + this.exits.set(leaseId, { sequence, recordedAt: now }) + while (this.exits.size > MAX_ENTRIES) { + const oldestLeaseId = this.exits.keys().next().value + if (oldestLeaseId === undefined) { + break + } + this.exits.delete(oldestLeaseId) + } + } + + consumeAfter(leaseId: string, startedSequence: number | undefined): RecentPtyExit | null { + const exit = this.exits.get(leaseId) + this.exits.delete(leaseId) + if ( + !exit || + startedSequence === undefined || + exit.sequence <= startedSequence || + Date.now() - exit.recordedAt > RETENTION_MS + ) { + return null + } + return exit + } + + matchesAfter(leaseId: string, startedSequence: number | undefined): boolean { + const exit = this.exits.get(leaseId) + if (!exit) { + return false + } + if (Date.now() - exit.recordedAt > RETENTION_MS) { + this.exits.delete(leaseId) + return false + } + return startedSequence !== undefined && exit.sequence > startedSequence + } +} diff --git a/src/main/codex/codex-session-migration-scheduler.test.ts b/src/main/codex/codex-session-migration-scheduler.test.ts index 9f9aed3173c..a1a8d4e8d7a 100644 --- a/src/main/codex/codex-session-migration-scheduler.test.ts +++ b/src/main/codex/codex-session-migration-scheduler.test.ts @@ -491,6 +491,146 @@ describe('createCodexSessionMigrationScheduler', () => { await vi.waitFor(() => expect(finishScheduledRun).toHaveBeenCalledOnce()) }) + it('keeps an ignored reattach exit from poisoning a same-ID launch already starting', async () => { + const startBackfill = vi.fn().mockResolvedValue({ stopped: false }) + const scheduler = createCodexSessionMigrationScheduler({ + isEligible: () => true, + isQuitting: () => false, + resolveSystemCodexHomePathOverride: () => undefined, + startBackfill, + startIndexHeal: vi.fn().mockResolvedValue(null), + initialDelayMs: 1_000 + }) + + scheduler.ignoreLaunch('stable-pty', 1) + scheduler.finishLaunch('stable-pty', 3) + scheduler.beginLaunch('stable-pty', false, new Date(), 2) + await vi.advanceTimersByTimeAsync(1_000) + + expect(startBackfill).toHaveBeenCalledWith( + expect.objectContaining({ + scanDates: expect.any(Array), + writeCompletionMarker: false, + writeBoundedCompletionMarker: false + }), + undefined + ) + }) + + it('matches an ignored reattach exit that arrived before its lifecycle callback', async () => { + const startBackfill = vi.fn().mockResolvedValue({ stopped: false }) + const scheduler = createCodexSessionMigrationScheduler({ + isEligible: () => true, + isQuitting: () => false, + resolveSystemCodexHomePathOverride: () => undefined, + startBackfill, + startIndexHeal: vi.fn().mockResolvedValue(null), + initialDelayMs: 1_000 + }) + + scheduler.finishLaunch('stable-pty', 2) + scheduler.ignoreLaunch('stable-pty', 1) + scheduler.beginLaunch('stable-pty', false, new Date(), 3) + await vi.advanceTimersByTimeAsync(1_000) + + expect(startBackfill).toHaveBeenCalledWith( + expect.objectContaining({ + scanDates: expect.any(Array), + writeCompletionMarker: false, + writeBoundedCompletionMarker: false + }), + undefined + ) + }) + + it('keeps a newer same-ID launch active while the ignored incarnation exits', async () => { + const startBackfill = vi.fn().mockResolvedValue({ stopped: false }) + const scheduler = createCodexSessionMigrationScheduler({ + isEligible: () => true, + isQuitting: () => false, + resolveSystemCodexHomePathOverride: () => undefined, + startBackfill, + startIndexHeal: vi.fn().mockResolvedValue(null), + initialDelayMs: 1_000 + }) + + scheduler.ignoreLaunch('stable-pty', 1) + scheduler.beginLaunch('stable-pty', false, new Date(), 2) + scheduler.finishLaunch('stable-pty', 3) + await vi.advanceTimersByTimeAsync(1_000) + + expect(startBackfill).toHaveBeenCalledWith( + expect.objectContaining({ writeCompletionMarker: false }), + undefined + ) + + scheduler.finishLaunch('stable-pty', 4) + await vi.advanceTimersByTimeAsync(1_000) + expect(startBackfill).toHaveBeenLastCalledWith( + expect.objectContaining({ writeCompletionMarker: true }), + undefined + ) + }) + + it('matches every late duplicate ignored callback without poisoning reuse', async () => { + const startBackfill = vi.fn().mockResolvedValue({ stopped: false }) + const scheduler = createCodexSessionMigrationScheduler({ + isEligible: () => true, + isQuitting: () => false, + resolveSystemCodexHomePathOverride: () => undefined, + startBackfill, + startIndexHeal: vi.fn().mockResolvedValue(null), + initialDelayMs: 1_000 + }) + + scheduler.ignoreLaunch('stable-pty', 1) + scheduler.finishLaunch('stable-pty', 3) + expect(scheduler.matchesIgnoredLaunchExit('stable-pty', 2)).toBe(true) + expect(scheduler.matchesIgnoredLaunchExit('stable-pty', 2)).toBe(true) + scheduler.beginLaunch('stable-pty', false, new Date(), 4) + await vi.advanceTimersByTimeAsync(1_000) + expect(startBackfill).toHaveBeenCalledTimes(1) + + scheduler.finishLaunch('stable-pty', 5) + await vi.advanceTimersByTimeAsync(1_000) + expect(startBackfill).toHaveBeenCalledTimes(2) + expect(startBackfill).toHaveBeenLastCalledWith( + expect.objectContaining({ writeCompletionMarker: true }), + undefined + ) + }) + + it('keeps three exit-before-callback reattaches from stranding a reused ID', async () => { + const startBackfill = vi.fn().mockResolvedValue({ stopped: false }) + const scheduler = createCodexSessionMigrationScheduler({ + isEligible: () => true, + isQuitting: () => false, + resolveSystemCodexHomePathOverride: () => undefined, + startBackfill, + startIndexHeal: vi.fn().mockResolvedValue(null), + initialDelayMs: 1_000 + }) + + scheduler.finishLaunch('stable-pty', 4) + scheduler.ignoreLaunch('stable-pty', 1) + scheduler.ignoreLaunch('stable-pty', 2) + scheduler.ignoreLaunch('stable-pty', 3) + scheduler.beginLaunch('stable-pty', false, new Date(), 5) + await vi.advanceTimersByTimeAsync(1_000) + expect(startBackfill).toHaveBeenLastCalledWith( + expect.objectContaining({ writeCompletionMarker: false }), + undefined + ) + + scheduler.finishLaunch('stable-pty', 6) + await vi.advanceTimersByTimeAsync(1_000) + expect(startBackfill).toHaveBeenCalledTimes(2) + expect(startBackfill).toHaveBeenLastCalledWith( + expect.objectContaining({ writeCompletionMarker: true }), + undefined + ) + }) + it('does not consume an exit from an earlier stable-id incarnation', async () => { const startBackfill = vi.fn().mockResolvedValue({ stopped: false }) const scheduler = createCodexSessionMigrationScheduler({ diff --git a/src/main/codex/codex-session-migration-scheduler.ts b/src/main/codex/codex-session-migration-scheduler.ts index 28ce4b991da..7e493c31cbd 100644 --- a/src/main/codex/codex-session-migration-scheduler.ts +++ b/src/main/codex/codex-session-migration-scheduler.ts @@ -1,4 +1,5 @@ import { getCodexSessionBackfillDate } from './codex-session-backfill-date' +import { CodexSessionMigrationRecentExits } from './codex-session-migration-recent-exits' import type { CodexSessionBackfillDate, CodexSessionBackfillOptions @@ -9,11 +10,6 @@ type MigrationRun = ( systemCodexHomePathOverride?: string ) => Promise -const EARLY_PTY_EXIT_RETENTION_MS = 60_000 -const MAX_EARLY_PTY_EXITS = 256 - -type EarlyPtyExit = { sequence: number; recordedAt: number } - export type CodexSessionMigrationScheduler = { beginLaunch( leaseId: string, @@ -21,6 +17,8 @@ export type CodexSessionMigrationScheduler = { startedAt?: Date, startedSequence?: number ): void + ignoreLaunch(leaseId: string, startedSequence: number): void + matchesIgnoredLaunchExit(leaseId: string, startedSequence: number): boolean finishLaunch(leaseId: string, exitSequence?: number): void scheduleInitialRun(): void scheduleRun(fullScanRequired?: boolean): void @@ -46,7 +44,10 @@ export function createCodexSessionMigrationScheduler(args: { let pendingFullScan = false let migrationTask: Promise | null = null const activeLaunches = new Map() - const earlyPtyExits = new Map() + // Why: non-shared reattach exits must not masquerade as exit-before-begin races for a reused id. + const ignoredLaunches = new Set() + const ignoredPtyExits = new CodexSessionMigrationRecentExits() + const earlyPtyExits = new CodexSessionMigrationRecentExits() let activeRunStopObserved = false let rerunRequested = false @@ -194,18 +195,41 @@ export function createCodexSessionMigrationScheduler(args: { if (args.isQuitting() || activeLaunches.has(leaseId)) { return } - if (consumeEarlyPtyExit(earlyPtyExits, leaseId, startedSequence)) { + if (earlyPtyExits.consumeAfter(leaseId, startedSequence)) { scheduleRun(true) return } activeLaunches.set(leaseId, startedAt) scheduleRun(fullScanRequired) }, + ignoreLaunch(leaseId, startedSequence): void { + if (args.isQuitting() || ignoredLaunches.has(leaseId)) { + return + } + if (ignoredPtyExits.matchesAfter(leaseId, startedSequence)) { + return + } + const earlyExit = earlyPtyExits.consumeAfter(leaseId, startedSequence) + if (earlyExit) { + ignoredPtyExits.record(leaseId, earlyExit.sequence) + return + } + ignoredLaunches.add(leaseId) + }, + matchesIgnoredLaunchExit(leaseId, startedSequence): boolean { + return ignoredPtyExits.matchesAfter(leaseId, startedSequence) + }, finishLaunch(leaseId, exitSequence): void { + if (ignoredLaunches.delete(leaseId)) { + if (exitSequence !== undefined) { + ignoredPtyExits.record(leaseId, exitSequence) + } + return + } const startedAt = activeLaunches.get(leaseId) if (!startedAt) { if (exitSequence !== undefined) { - recordEarlyPtyExit(earlyPtyExits, leaseId, exitSequence) + earlyPtyExits.record(leaseId, exitSequence) } return } @@ -253,42 +277,6 @@ function compareBackfillDates( return left.join('-').localeCompare(right.join('-')) } -function recordEarlyPtyExit( - exits: Map, - leaseId: string, - sequence: number -): void { - const now = Date.now() - for (const [id, exit] of exits) { - if (now - exit.recordedAt > EARLY_PTY_EXIT_RETENTION_MS) { - exits.delete(id) - } - } - exits.set(leaseId, { sequence, recordedAt: now }) - while (exits.size > MAX_EARLY_PTY_EXITS) { - const oldestLeaseId = exits.keys().next().value - if (oldestLeaseId === undefined) { - break - } - exits.delete(oldestLeaseId) - } -} - -function consumeEarlyPtyExit( - exits: Map, - leaseId: string, - startedSequence: number | undefined -): boolean { - const exit = exits.get(leaseId) - exits.delete(leaseId) - return ( - exit !== undefined && - startedSequence !== undefined && - exit.sequence > startedSequence && - Date.now() - exit.recordedAt <= EARLY_PTY_EXIT_RETENTION_MS - ) -} - function isStoppedMigrationResult(result: unknown): boolean { return Boolean(result && typeof result === 'object' && 'stopped' in result && result.stopped) } diff --git a/src/main/codex/codex-stale-pane-accounts.test.ts b/src/main/codex/codex-stale-pane-accounts.test.ts index 720f2101f81..ebc953a4be7 100644 --- a/src/main/codex/codex-stale-pane-accounts.test.ts +++ b/src/main/codex/codex-stale-pane-accounts.test.ts @@ -9,6 +9,7 @@ import { getCodexPaneAccount, hasRecordedLegacySharedCodexPane, hasRecordedManagedHostCodexPane, + isCodexPaneHomeRouteProvenAwayFromSharedHome, reconcileCodexPaneAccountsWithLivePtys, recordCodexPaneAccount } from './codex-pane-account-registry' @@ -47,6 +48,17 @@ afterEach(() => { }) describe('codex pane account registry', () => { + it.each([ + ['real-home', true], + ['account-home', true], + ['wsl-home', true], + ['shared-home', false], + ['custom-home', false], + [undefined, false] + ] as const)('classifies whether %s proves a pane avoided the shared home', (route, expected) => { + expect(isCodexPaneHomeRouteProvenAwayFromSharedHome(route)).toBe(expected) + }) + it('survives a process restart so a daemon-backed shell stays attributable', () => { recordCodexPaneAccount('pty-1', { selectionKey: 'host', diff --git a/src/main/index.ts b/src/main/index.ts index 55180cc9ff8..922480e205e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -39,7 +39,10 @@ import { shutdownDaemon } from './daemon/daemon-init' import { + type CodexPaneHomeRoute, + getCodexPaneAccount, hasRecordedManagedHostCodexPane, + isCodexPaneHomeRouteProvenAwayFromSharedHome, reconcileCodexPaneAccountsWithLivePtys } from './codex/codex-pane-account-registry' import { closeAllWatchers } from './ipc/filesystem-watcher' @@ -401,10 +404,30 @@ function handleCodexHomePtySpawned(args: { id: string codexHomePath: string | null reattached?: boolean + reattachedHomeRoute?: CodexPaneHomeRoute | null launchEnv?: NodeJS.ProcessEnv startedAt?: Date startedSequence?: number }): void { + // Why: only shared or ambiguous retained shells can create rollout logs that still need publication. + if (args.reattached && args.startedSequence !== undefined) { + const paneAccount = getCodexPaneAccount(args.id) + const homeRoute = + args.reattachedHomeRoute !== undefined + ? (args.reattachedHomeRoute ?? undefined) + : paneAccount?.homeRoute + if (codexSessionMigration && isCodexPaneHomeRouteProvenAwayFromSharedHome(homeRoute)) { + codexSessionMigration.ignoreLaunch(args.id, args.startedSequence) + return + } + if ( + !paneAccount && + homeRoute === undefined && + codexSessionMigration?.matchesIgnoredLaunchExit(args.id, args.startedSequence) + ) { + return + } + } const fullScanRequired = codexRuntimeHome?.beginHostSystemDefaultSessionMigrationLaunch(args.codexHomePath, { reattached: args.reattached, diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index d3ec26f91a6..d462541ed72 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -70,6 +70,7 @@ const { clearPaneKeyAliasesForPtyMock, recordCodexPaneAccountMock, forgetCodexPaneAccountMock, + getCodexPaneAccountMock, ensureCodexBackfillRecoveryMock } = vi.hoisted(() => ({ handleMock: vi.fn(), @@ -107,6 +108,7 @@ const { clearPaneKeyAliasesForPtyMock: vi.fn(), recordCodexPaneAccountMock: vi.fn(), forgetCodexPaneAccountMock: vi.fn(), + getCodexPaneAccountMock: vi.fn(), ensureCodexBackfillRecoveryMock: vi.fn(() => Promise.resolve()) })) @@ -221,7 +223,8 @@ vi.mock('../agent-hooks/migration-unsupported-pty-state', () => ({ vi.mock('../codex/codex-pane-account-registry', () => ({ recordCodexPaneAccount: recordCodexPaneAccountMock, - forgetCodexPaneAccount: forgetCodexPaneAccountMock + forgetCodexPaneAccount: forgetCodexPaneAccountMock, + getCodexPaneAccount: getCodexPaneAccountMock })) vi.mock('../codex/codex-state-db-backfill-recovery', () => ({ @@ -410,6 +413,7 @@ describe('registerPtyHandlers', () => { clearPaneKeyAliasesForPtyMock.mockReset() recordCodexPaneAccountMock.mockReset() forgetCodexPaneAccountMock.mockReset() + getCodexPaneAccountMock.mockReset() ensureCodexBackfillRecoveryMock.mockReset() ensureCodexBackfillRecoveryMock.mockResolvedValue(undefined) mainWindow.webContents.on.mockReset() @@ -9092,16 +9096,20 @@ describe('registerPtyHandlers', () => { { label: 'git worktree', worktreeId: 'repo-1::/tmp/live-owner', - cwd: '/tmp/live-owner' + cwd: '/tmp/live-owner', + paneAccountAtStart: { homeRoute: 'shared-home' }, + expectedHomeRouteAtStart: 'shared-home' }, { label: 'folder workspace', worktreeId: 'folder:live-owner', - cwd: '/tmp' + cwd: '/tmp', + paneAccountAtStart: null, + expectedHomeRouteAtStart: null } ])( 'adopts a completed runtime-owned pane before replacement launch preflight ($label)', - async ({ worktreeId, cwd }) => { + async ({ worktreeId, cwd, paneAccountAtStart, expectedHomeRouteAtStart }) => { type StableAdoption = { result: { id: string; incarnationId?: string; isReattach?: boolean } owner: { handle?: string; tabId: string; leafId: string; ptyId: string } @@ -9308,8 +9316,10 @@ describe('registerPtyHandlers', () => { runtime.beginPtyRegistration.mockImplementation(() => { runtimeSecondAdoption ??= spawnController.adoptStablePane(adoptionArgs) }) + getCodexPaneAccountMock.mockReturnValue(paneAccountAtStart) const rendererFirstMount = handlers.get('pty:spawn')!(null, mountArgs) await vi.waitFor(() => expect(providerSpawn).toHaveBeenCalledTimes(3)) + getCodexPaneAccountMock.mockReturnValue({ homeRoute: 'real-home' }) releaseAttach() await vi.waitFor(() => expect(runtimeSecondAdoption).not.toBeNull()) const pendingRuntimeAdoption = runtimeSecondAdoption @@ -9354,6 +9364,7 @@ describe('registerPtyHandlers', () => { id: 'pty-live-owner', codexHomePath: null, reattached: true, + reattachedHomeRoute: expectedHomeRouteAtStart, startedAt: expect.any(Date), startedSequence: expect.any(Number) }) diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 24786811086..040fccaab99 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -198,7 +198,9 @@ import { } from '../codex-accounts/managed-codex-auth-readiness' import { forgetCodexPaneAccount, - recordCodexPaneAccount + getCodexPaneAccount, + recordCodexPaneAccount, + type CodexPaneHomeRoute } from '../codex/codex-pane-account-registry' import { resolveCodexPaneLaunchAccount } from '../codex/codex-pane-launch-account' import { getSystemCodexHomePath } from '../codex/codex-home-paths' @@ -1238,6 +1240,7 @@ export type CodexHomePtySpawnedLifecycleArgs = { id: string codexHomePath: string | null reattached?: boolean + reattachedHomeRoute?: CodexPaneHomeRoute | null launchEnv?: NodeJS.ProcessEnv startedAt?: Date startedSequence?: number @@ -1449,6 +1452,30 @@ function recordCodexPaneAccountForSpawn(args: { recordCodexPaneAccount(args.ptyId, record) } +function snapshotCodexPaneHomeRoutes( + ptyIds: readonly (string | null | undefined)[] +): ReadonlyMap { + const routes = new Map() + for (const ptyId of ptyIds) { + if (!ptyId || routes.has(ptyId)) { + continue + } + routes.set(ptyId, getCodexPaneAccount(ptyId)?.homeRoute ?? null) + } + return routes +} + +function codexReattachedHomeRouteField( + routes: ReadonlyMap, + ptyId: string, + reattached: boolean +): { reattachedHomeRoute?: CodexPaneHomeRoute | null } { + if (!reattached || !routes.has(ptyId)) { + return {} + } + return { reattachedHomeRoute: routes.get(ptyId) ?? null } +} + function readEnvWithProcessFallback( baseEnv: Record, key: string @@ -4407,6 +4434,12 @@ export function registerPtyHandlers( const codexHomeLaunchStartedAt = !args.connectionId ? new Date() : undefined const codexHomeLaunchStartedSequence = !args.connectionId ? ++ptyLifecycleSequence : undefined const preAdoptedStablePane = args.adoptedStablePane ?? null + const reattachedCodexHomeRoutes = !args.connectionId + ? snapshotCodexPaneHomeRoutes([ + preAdoptedStablePane?.result.id, + args.sessionId ? getAppPtyId(args.connectionId, args.sessionId) : undefined + ]) + : new Map() const startupPromise = getLocalPtyStartupPromise(args.connectionId) if (startupPromise) { await startupPromise @@ -4440,7 +4473,8 @@ export function registerPtyHandlers( codexHomePath: null, reattached: true, startedAt: codexHomeLaunchStartedAt, - startedSequence: codexHomeLaunchStartedSequence + startedSequence: codexHomeLaunchStartedSequence, + ...codexReattachedHomeRouteField(reattachedCodexHomeRoutes, result.id, true) }) } return result @@ -5115,6 +5149,7 @@ export function registerPtyHandlers( reattached: true, startedAt: codexHomeLaunchStartedAt, startedSequence: codexHomeLaunchStartedSequence, + ...codexReattachedHomeRouteField(reattachedCodexHomeRoutes, result.id, true), ...(env ? { launchEnv: env } : {}) }) } @@ -5288,6 +5323,11 @@ export function registerPtyHandlers( codexHomePath: selectedCodexHomePath, startedAt: codexHomeLaunchStartedAt, startedSequence: codexHomeLaunchStartedSequence, + ...codexReattachedHomeRouteField( + reattachedCodexHomeRoutes, + result.id, + result.isReattach === true + ), ...(result.isReattach === true ? { reattached: true } : env ? { launchEnv: env } : {}) }) } @@ -5786,6 +5826,37 @@ export function registerPtyHandlers( ) => { const codexHomeLaunchStartedAt = !args.connectionId ? new Date() : undefined const codexHomeLaunchStartedSequence = !args.connectionId ? ++ptyLifecycleSequence : undefined + const initialLeafId = + typeof args.leafId === 'string' && isTerminalLeafId(args.leafId) ? args.leafId : null + const initialPaneKey = + typeof args.worktreeId === 'string' && + typeof args.tabId === 'string' && + isValidTerminalTabId(args.tabId) && + args.tabId.length <= 512 && + initialLeafId + ? makePaneKey(args.tabId, initialLeafId) + : null + const initialStablePanePtyId = (() => { + try { + return !args.connectionId && initialPaneKey + ? resolveStablePaneOwner( + runtime, + store, + initialPaneKey, + args.worktreeId, + args.connectionId + )?.ptyId + : undefined + } catch { + return undefined + } + })() + const reattachedCodexHomeRoutes = !args.connectionId + ? snapshotCodexPaneHomeRoutes([ + initialStablePanePtyId, + args.sessionId ? getAppPtyId(args.connectionId, args.sessionId) : undefined + ]) + : new Map() const spawnTiming = createPtySpawnTiming() const startupPromise = getLocalPtyStartupPromise(args.connectionId) if (startupPromise) { @@ -6840,6 +6911,11 @@ export function registerPtyHandlers( codexHomePath: selectedCodexHomePath, startedAt: codexHomeLaunchStartedAt, startedSequence: codexHomeLaunchStartedSequence, + ...codexReattachedHomeRouteField( + reattachedCodexHomeRoutes, + result.id, + result.isReattach === true + ), ...(result.isReattach === true ? { reattached: true } : baseEnv