mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
fix(codex): prune retired pane routes safely
This commit is contained in:
@@ -248,6 +248,22 @@ export function hasRecordedLegacySharedCodexPane(): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
/** Drops records whose daemon PTYs are authoritatively absent. */
|
||||
export function reconcileCodexPaneAccountsWithLivePtys(livePtyIds: readonly string[]): void {
|
||||
const registry = readRegistry()
|
||||
const livePtyIdSet = new Set(livePtyIds)
|
||||
let changed = false
|
||||
for (const ptyId of Object.keys(registry.panes)) {
|
||||
if (!livePtyIdSet.has(ptyId)) {
|
||||
delete registry.panes[ptyId]
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
writeRegistry(registry)
|
||||
}
|
||||
}
|
||||
|
||||
export const _internals = {
|
||||
resetCache: (): void => {
|
||||
cachedRegistry = null
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
forgetCodexPaneAccount,
|
||||
getCodexPaneAccount,
|
||||
hasRecordedLegacySharedCodexPane,
|
||||
reconcileCodexPaneAccountsWithLivePtys,
|
||||
recordCodexPaneAccount
|
||||
} from './codex-pane-account-registry'
|
||||
import { forgetStaleCodexPanes, listStaleCodexPanes } from './codex-stale-pane-accounts'
|
||||
@@ -113,6 +114,29 @@ describe('codex pane account registry', () => {
|
||||
expect(hasRecordedLegacySharedCodexPane()).toBe(true)
|
||||
})
|
||||
|
||||
it('drops leaked records that are absent from the authoritative daemon inventory', () => {
|
||||
recordCodexPaneAccount('pty-live', {
|
||||
selectionKey: 'host',
|
||||
accountId: null,
|
||||
homeRoute: 'shared-home'
|
||||
})
|
||||
recordCodexPaneAccount('pty-dead', {
|
||||
selectionKey: 'host',
|
||||
accountId: null,
|
||||
homeRoute: 'shared-home'
|
||||
})
|
||||
|
||||
reconcileCodexPaneAccountsWithLivePtys(['pty-live'])
|
||||
_internals.resetCache()
|
||||
|
||||
expect(getCodexPaneAccount('pty-live')).not.toBeNull()
|
||||
expect(getCodexPaneAccount('pty-dead')).toBeNull()
|
||||
expect(hasRecordedLegacySharedCodexPane()).toBe(true)
|
||||
|
||||
reconcileCodexPaneAccountsWithLivePtys([])
|
||||
expect(hasRecordedLegacySharedCodexPane()).toBe(false)
|
||||
})
|
||||
|
||||
it('forgets a PTY so a reused id cannot inherit a dead pane account', () => {
|
||||
recordCodexPaneAccount('pty-1', { selectionKey: 'host', accountId: 'account-a' })
|
||||
|
||||
@@ -244,6 +268,29 @@ describe('listStaleCodexPanes', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps account-switch copy when the account and home route both change', () => {
|
||||
recordCodexPaneAccount('pty-1', {
|
||||
selectionKey: 'host',
|
||||
accountId: null,
|
||||
homeRoute: 'real-home'
|
||||
})
|
||||
|
||||
expect(
|
||||
listStaleCodexPanes({
|
||||
ptyIds: ['pty-1'],
|
||||
settings: settingsWithSelection('account-a'),
|
||||
activeHostHomeRoute: 'account-home'
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
ptyId: 'pty-1',
|
||||
launchAccountId: null,
|
||||
activeAccountId: 'account-a',
|
||||
reason: 'account-change'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('does not guess a route for panes recorded before route provenance', () => {
|
||||
recordCodexPaneAccount('pty-1', { selectionKey: 'host', accountId: null })
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export function listStaleCodexPanes(args: {
|
||||
ptyId,
|
||||
launchAccountId: record.accountId,
|
||||
activeAccountId,
|
||||
reason: homeRouteChanged ? 'home-route-change' : 'account-change'
|
||||
reason: accountChanged ? 'account-change' : 'home-route-change'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,6 +830,26 @@ export function getDaemonProvider(): DaemonProvider | null {
|
||||
return adapter
|
||||
}
|
||||
|
||||
/** Returns null unless every daemon generation supplied an authoritative inventory. */
|
||||
export async function listLiveDaemonPtyIds(): Promise<string[] | null> {
|
||||
if (!adapter) {
|
||||
return null
|
||||
}
|
||||
const adapters =
|
||||
adapter instanceof DaemonPtyRouter || adapter instanceof DegradedDaemonPtyProvider
|
||||
? adapter.getAllAdapters()
|
||||
: [adapter]
|
||||
const inventories = await Promise.allSettled(
|
||||
adapters.map((daemonAdapter) => daemonAdapter.listProcesses())
|
||||
)
|
||||
if (inventories.some((inventory) => inventory.status === 'rejected')) {
|
||||
return null
|
||||
}
|
||||
return inventories.flatMap((inventory) =>
|
||||
inventory.status === 'fulfilled' ? inventory.value.map((process) => process.id) : []
|
||||
)
|
||||
}
|
||||
|
||||
// Why: keep the module-level adapter and ipc/pty.ts's localProvider in sync so app-quit can't dispose a stale reference.
|
||||
export function replaceDaemonProvider(newAdapter: DaemonProvider): void {
|
||||
adapter = newAdapter
|
||||
|
||||
+13
-2
@@ -35,8 +35,13 @@ import {
|
||||
initDaemonPtyProvider,
|
||||
disconnectDaemon,
|
||||
getDaemonProvider,
|
||||
listLiveDaemonPtyIds,
|
||||
shutdownDaemon
|
||||
} from './daemon/daemon-init'
|
||||
import {
|
||||
hasRecordedLegacySharedCodexPane,
|
||||
reconcileCodexPaneAccountsWithLivePtys
|
||||
} from './codex/codex-pane-account-registry'
|
||||
import { closeAllWatchers } from './ipc/filesystem-watcher'
|
||||
import { disposeWorktreeBaseDirectoryWatchers } from './ipc/worktree-base-directory-watcher'
|
||||
import { registerCoreHandlers } from './ipc/register-core-handlers'
|
||||
@@ -844,6 +849,14 @@ function startTerminalRuntimeStartupServices(): Promise<void> {
|
||||
await initDaemonPtyProvider(signal, {
|
||||
macosLoginSessionWatch: process.platform === 'darwin' && !isServeMode
|
||||
})
|
||||
if (hasRecordedLegacySharedCodexPane()) {
|
||||
const livePtyIds = await listLiveDaemonPtyIds()
|
||||
if (livePtyIds) {
|
||||
reconcileCodexPaneAccountsWithLivePtys(livePtyIds)
|
||||
}
|
||||
}
|
||||
// Why: retained shells can invoke Codex immediately after the startup gate.
|
||||
codexRuntimeHome?.reconcileLegacySharedHomeForRetainedPanes()
|
||||
logStartupMilestone('startup-service-done', { service: 'daemon-pty-provider' })
|
||||
},
|
||||
// Why: PTY spawn env reads ORCA_AGENT_HOOK_* from live server state, so the renderer awaits this before restored terminals reconnect.
|
||||
@@ -2586,8 +2599,6 @@ void app.whenReady().then(async () => {
|
||||
userDataPath: app.getPath('userData')
|
||||
})
|
||||
}
|
||||
// Why: retained daemon shells can invoke Codex without crossing launch prep.
|
||||
codexRuntimeHome.reconcileLegacySharedHomeForRetainedPanes()
|
||||
if (shouldInstallManagedHooks(is.dev)) {
|
||||
// Why: check the persisted off switch before any auto-install so removed hooks don't silently reappear on launch.
|
||||
if (isAgentStatusHooksEnabled(store.getSettings())) {
|
||||
|
||||
@@ -95,16 +95,18 @@ describe('startup ordering', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('reconciles retained Codex homes once before serve and desktop startup diverge', () => {
|
||||
it('reconciles retained Codex homes after authoritative daemon inventory', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const hookStateIndex = source.indexOf('ensureRealHomeCodexHookState({')
|
||||
const reconciliation = 'codexRuntimeHome.reconcileLegacySharedHomeForRetainedPanes()'
|
||||
const reconciliationIndex = source.indexOf(reconciliation, hookStateIndex)
|
||||
const daemonInitIndex = source.indexOf('await initDaemonPtyProvider(signal')
|
||||
const inventoryIndex = source.indexOf('await listLiveDaemonPtyIds()', daemonInitIndex)
|
||||
const reconciliation = 'codexRuntimeHome?.reconcileLegacySharedHomeForRetainedPanes()'
|
||||
const reconciliationIndex = source.indexOf(reconciliation, inventoryIndex)
|
||||
const serveIndex = source.indexOf('if (serveOptions) {', reconciliationIndex)
|
||||
const desktopIndex = source.indexOf('Promise.resolve(openMainWindow())', serveIndex)
|
||||
|
||||
expect(hookStateIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(reconciliationIndex).toBeGreaterThan(hookStateIndex)
|
||||
expect(daemonInitIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(inventoryIndex).toBeGreaterThan(daemonInitIndex)
|
||||
expect(reconciliationIndex).toBeGreaterThan(inventoryIndex)
|
||||
expect(serveIndex).toBeGreaterThan(reconciliationIndex)
|
||||
expect(desktopIndex).toBeGreaterThan(serveIndex)
|
||||
expect(source.split(reconciliation)).toHaveLength(2)
|
||||
|
||||
Reference in New Issue
Block a user