feat(codex): session dedup across roots, canonical real-home resume, and sqlite index heal - #8921
brennanb2025 wants to merge 53 commits into
Conversation
…once per host Orca-launched Codex sessions currently land only in the Orca-managed runtime home, so the user's own `codex resume` picker and app history never see them (#4444, #8612). Backfill the managed sessions tree into the real ~/.codex/sessions/YYYY/MM/DD layout once per host: - hardlink first (one physical rollout log), copy as the cross-volume fallback; existing target files are always skipped, nothing in either home is deleted or moved - idempotent; per-file failures leave the completion marker unset so the next startup retries cheaply - JSONL audit log of every link/copy/failure under <userData>/codex-session-backfill/ - honors the custom Codex session source home override, mirroring the existing system->managed bridge WSL managed homes are distro-local and need an in-distro variant; that is a follow-up.
Staged internal flag (default OFF, no settings UI): route the SYSTEM-DEFAULT Codex account at the user's real ~/.codex instead of Orca's managed runtime home. Flag OFF is byte-identical to today; managed (multi-account) selections are unchanged in either state. Routing (flag ON + host system default = no managed account): - CodexRuntimeHomeService.prepareForCodexLaunch / prepareForRateLimitFetch return null so the PTY/env layer injects no managed CODEX_HOME and the rate-limit fetcher + auth-presence gate fall back to ~/.codex (the background poller stops spawning Codex against the managed home — the #5370 auth war). - buildPtyHostEnv strips only a nested-Orca-inherited Orca-owned override (CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a user-set CODEX_HOME. Shell-ready re-exports already no-op without the marker. - The headless commit-message Codex path strips the same inherited override. Hook install for the real-home lane (append-last into ~/.codex/hooks.json, trust via the app-server client) lands with the trust plumbing; the managed hook install is skipped for this lane meanwhile. Credit @jellychoco (#8606) for the native-home routing direction. Depends on the codex trust-rpc-grant plumbing for the real-home hook installer.
…eal-home routing The daemon spawns PTYs from its own inherited environment and honors only spawnOptions.envToDelete, so mutating the sparse env object was not enough to strip an Orca-owned CODEX_HOME the daemon already carries. Add the strip to envToDelete for both daemon host-spawn paths, preserving a user-set CODEX_HOME. Verified live via CDP against a sandboxed dev instance (flag ON): an Orca-spawned pane reports empty CODEX_HOME/ORCA_CODEX_HOME, so Codex resolves its own ~/.codex. Adds daemon-path unit coverage (strip Orca-owned, preserve user-owned, no-op when flag OFF).
…nt ledger Short-lived codex app-server JSON-RPC client (hooks/list + config/batchWrite, the same pair the Codex TUI 'Trust all' flow calls), run in a bundled ELECTRON_RUN_AS_NODE entry so synchronous launch prep can block on it with a hard deadline and guaranteed child reap. Capability cache modeled on GitCapabilityCache, scoped per execution host (native vs each WSL distro), with a narrow unknown-method/missing-subcommand unsupported predicate. The grant ledger records verified grants so steady-state launches skip the RPC.
…tall/refresh Host and WSL installs now grant trust for Orca's managed status hooks through codex's own hooks/list -> config/batchWrite -> re-list verify, scoped to exactly the managed entries; the previous computeTrustedHash lane is the unchanged fallback for incapable/erroring CLIs. getStatus and the removal paths recognize ledger-recorded codex hashes so drift between codex's real algorithm and the replica no longer misreports or strands trust. SSH remote install is untouched by design.
Adds the two cases for the headless commit-message Codex env under real-home routing: a nested-Orca-inherited Orca-owned CODEX_HOME is stripped, and a user-owned CODEX_HOME is preserved.
…o real-home phase1
…r grant client With the real-home flag ON and the system-default selection, install Orca's status hook into the user's real ~/.codex before any pane spawns: - entry APPENDED LAST per managed event: codex hook trust keys are positional (source:event:group:handler), so appending keeps every user entry's position and trust record intact; user entries and unknown top-level hooks.json fields are preserved verbatim - trust is granted exclusively through the codex app-server client (hooks/list + config/batchWrite, verified by re-list); Orca never writes [hooks.state] into the user's real config.toml itself - if the grant lane is unavailable (old binary, unsupported RPC, verify failure), the appended entry is rolled back byte-exactly and the host keeps the managed-home lane end to end (PTY env, rate limits, commit messages) via a lane gate on the runtime-home service - one-time pristine backup of the user's hooks.json under Orca's userData; a rolling .bak sits next to the file (existing atomic writer) - hook opt-out sweeps Orca entries from the real home and drops Orca-owned trust records; flag-off downgrade re-arms the existing legacy system-home sweep, which removes the entry and its trust keys cleanly - the legacy system-home sweep is suppressed only while the real-home lane owns ~/.codex/hooks.json, so managed installs cannot delete the entry
The grant bridge is reachable from plain-Node CLI entries, where the
plain-node entry guard rejects any chunk containing require("electron").
Resolve the bundled session entry from __dirname (root chunk and chunks/
layouts) with an app.asar -> app.asar.unpacked rewrite for packaged runs,
instead of electron's app path APIs.
Use asynchronous, sequential filesystem operations for the one-time rollout backfill, and avoid repeated target-directory probes. Treat inaccessible managed session roots as retryable failures instead of writing a false completion marker.
On a real Codex home whose filesystem supports no hardlinks (exFAT/FAT, some network mounts), the staged cross-volume copy was installed with a non-atomic copyFile(..., COPYFILE_EXCL) straight into the final rollout-*.jsonl name. An install interrupted mid-copy (app quit, crash, ENOSPC during the deferred run) could strand a truncated rollout that the next run then skips as already-present, defeating the staging design's own guarantee that a failed copy never leaves a partial session behind. Install the fully-staged copy with an atomic rename instead, guarded by an existence re-check so it keeps the never-overwrite contract (and the rename source is the same immutable managed rollout, so any clobber would be byte-identical). Cover the no-hardlink-support target and an interrupted install that must leave no partial in the user's sessions tree.
…ies stay electron-free The build guard rejects any electron require reachable from plain-node entries; the bridge now maps app.asar to app.asar.unpacked by string replacement instead of consulting electron app paths. CLI typecheck project lists the new trust-grant module graph.
…nt' into brennanb2025/codex-real-home-phase1 # Conflicts: # src/main/codex/codex-app-server-grant-bridge.ts
…nt' into brennanb2025/codex-real-home-phase1
…nt' into brennanb2025/codex-real-home-phase1
…ill' into brennanb2025/codex-session-dedup-index-heal # Conflicts: # src/main/index.ts
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (1)
src/main/codex/hook-service.ts (1)
749-750: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
path.posix.joinfor the WSL hook path.Avoid manually appending
/; construct this guest-side path with Node’s POSIX path utility.buildManagedCommand: (linuxRuntimeHome) => - wrapReadablePosixHookCommand(`${linuxRuntimeHome}/.orca/agent-hooks/codex-hook.sh`), + wrapReadablePosixHookCommand( + pathPosix.join(linuxRuntimeHome, '.orca', 'agent-hooks', 'codex-hook.sh') + ),As per coding guidelines, “Use
path.joinor Electron/Node path utilities for file paths; never assume/or\.”Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a62dd875-08ee-4374-82c4-1216e02602aa
📒 Files selected for processing (65)
build-plugins/plain-node-entry-guard.tsconfig/tsconfig.cli.jsonelectron.vite.config.tssrc/main/ai-vault/codex-session-root-dedup.test.tssrc/main/ai-vault/codex-session-root-dedup.tssrc/main/ai-vault/remote-session-scanner-sources.tssrc/main/ai-vault/remote-session-scanner-types.tssrc/main/ai-vault/remote-session-scanner.test.tssrc/main/ai-vault/remote-session-scanner.tssrc/main/ai-vault/session-scanner-codex-dual-root.test.tssrc/main/ai-vault/session-scanner-types.tssrc/main/ai-vault/session-scanner.tssrc/main/codex-accounts/runtime-home-service.test.tssrc/main/codex-accounts/runtime-home-service.tssrc/main/codex-accounts/wsl-codex-command.tssrc/main/codex/codex-app-server-capability-cache.test.tssrc/main/codex/codex-app-server-capability-cache.tssrc/main/codex/codex-app-server-client.test.tssrc/main/codex/codex-app-server-client.tssrc/main/codex/codex-app-server-grant-bridge.tssrc/main/codex/codex-app-server-grant-entry.tssrc/main/codex/codex-app-server-grant-envelope.tssrc/main/codex/codex-app-server-session.tssrc/main/codex/codex-home-paths.tssrc/main/codex/codex-hook-trust-grant.test.tssrc/main/codex/codex-hook-trust-grant.tssrc/main/codex/codex-managed-trust-reconciliation.tssrc/main/codex/codex-real-home-flag.test.tssrc/main/codex/codex-real-home-flag.tssrc/main/codex/codex-real-home-hook-install.test.tssrc/main/codex/codex-real-home-hook-install.tssrc/main/codex/codex-real-home-path.tssrc/main/codex/codex-session-backfill-audit.tssrc/main/codex/codex-session-backfill-copy.tssrc/main/codex/codex-session-backfill.test.tssrc/main/codex/codex-session-backfill.tssrc/main/codex/codex-session-file-listing.tssrc/main/codex/codex-session-index-heal-state.tssrc/main/codex/codex-session-index-heal.test.tssrc/main/codex/codex-session-index-heal.tssrc/main/codex/codex-trust-config-rollback.test.tssrc/main/codex/codex-trust-config-rollback.tssrc/main/codex/codex-trust-grant-host.test.tssrc/main/codex/codex-trust-grant-host.tssrc/main/codex/codex-trust-grant-ledger.test.tssrc/main/codex/codex-trust-grant-ledger.tssrc/main/codex/codex-wsl-hook-install-plan.test.tssrc/main/codex/codex-wsl-hook-install-plan.tssrc/main/codex/hook-service-trust-grant.test.tssrc/main/codex/hook-service-wsl-runtime.test.tssrc/main/codex/hook-service.tssrc/main/daemon/pty-subprocess.test.tssrc/main/daemon/pty-subprocess.tssrc/main/daemon/types.tssrc/main/index.tssrc/main/ipc/pty.test.tssrc/main/ipc/pty.tssrc/main/text-generation/commit-message-agent-environment.test.tssrc/main/text-generation/commit-message-agent-environment.tssrc/main/window/attach-main-window-services.tssrc/renderer/src/components/terminal-pane/pty-connection.test.tssrc/shared/ai-vault-resume-command.test.tssrc/shared/constants.tssrc/shared/telemetry-events.tssrc/shared/types.ts
| export function resolveCodexTrustGrantHost(host: CodexTrustGrantHost): ResolvedCodexTrustGrantHost { | ||
| if (host.kind === 'wsl') { | ||
| return { | ||
| binaryStamp: { kind: 'wsl', distro: host.distro }, | ||
| buildRequest: (input) => ({ | ||
| invocation: { | ||
| command: 'wsl.exe', | ||
| args: buildWslCodexAppServerArgs(host.distro, host.linuxRuntimeHome), | ||
| timeoutMs: WSL_GRANT_TIMEOUT_MS | ||
| }, | ||
| hooksListCwd: host.linuxRuntimeHome, | ||
| expectedTrustKeys: input.expectedTrustKeys, | ||
| managedCommand: input.managedCommand | ||
| }) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Include the WSL Codex binary identity in binaryStamp.
{ kind: 'wsl', distro } remains unchanged when the distro’s Codex binary is upgraded or switched. The ledger can therefore pass binaryStampsMatch and skip re-granting with hashes produced by a different binary. Resolve and stamp the WSL binary/version similarly to the native lane.
| const file = readLedgerFile(ledgerPath) | ||
| file.homes[getCodexTrustGrantHomeKey(runtimeHomePath)] = home | ||
| writeFileSync(ledgerPath, `${JSON.stringify(file, null, 2)}\n`, { | ||
| encoding: 'utf-8', | ||
| mode: 0o600 | ||
| }) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Create the ledger directory before the first write.
On a fresh profile, writeFileSync throws ENOENT unless another path guarantees that dirname(ledgerPath) already exists. The supplied test setup creates only userDataDir, so make this persistence function self-contained.
Proposed fix
-import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'
+import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
...
const file = readLedgerFile(ledgerPath)
file.homes[getCodexTrustGrantHomeKey(runtimeHomePath)] = home
+ mkdirSync(dirname(ledgerPath), { recursive: true, mode: 0o700 })
writeFileSync(ledgerPath, `${JSON.stringify(file, null, 2)}\n`, {#!/bin/bash
ast-grep outline src/main/codex/codex-home-paths.ts --match getOrcaManagedCodexHomePath --view expanded
rg -n -C3 '\bgetOrcaManagedCodexHomePath\s*\(' src/main| let tmpHome: string | ||
| let userDataDir: string | ||
| let previousUserDataPath: string | undefined | ||
|
|
||
| beforeEach(() => { | ||
| tmpHome = mkdtempSync(join(tmpdir(), 'orca-codex-home-')) | ||
| userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-user-data-')) | ||
| previousUserDataPath = process.env.ORCA_USER_DATA_PATH | ||
| process.env.ORCA_USER_DATA_PATH = userDataDir | ||
| homedirMock.mockReturnValue(tmpHome) | ||
| resolveCodexCommandMock.mockReturnValue(process.execPath) | ||
| getPathMock.mockImplementation((name: string) => { | ||
| if (name === 'userData') { | ||
| return userDataDir | ||
| } | ||
| throw new Error(`unexpected app.getPath(${name})`) | ||
| }) | ||
| trustGrantInternals.resetDiagnostics() | ||
| codexAppServerCapabilityCache.clear() | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| trustGrantInternals.setGrantSessionRunnerSync(null) | ||
| trustGrantInternals.resetDiagnostics() | ||
| codexAppServerCapabilityCache.clear() | ||
| delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC | ||
| rmSync(tmpHome, { recursive: true, force: true }) | ||
| rmSync(userDataDir, { recursive: true, force: true }) | ||
| if (previousUserDataPath === undefined) { | ||
| delete process.env.ORCA_USER_DATA_PATH | ||
| } else { | ||
| process.env.ORCA_USER_DATA_PATH = previousUserDataPath | ||
| } | ||
| vi.clearAllMocks() | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve and reset ORCA_DISABLE_CODEX_TRUST_RPC.
A pre-existing flag forces these RPC tests down the fallback lane, and afterEach then deletes the caller’s original value. Snapshot it, clear it before each test, and restore it afterward.
Proposed fix
let previousUserDataPath: string | undefined
+let previousDisableTrustRpc: string | undefined
beforeEach(() => {
+ previousDisableTrustRpc = process.env.ORCA_DISABLE_CODEX_TRUST_RPC
+ delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC
// ...
})
afterEach(() => {
- delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC
+ if (previousDisableTrustRpc === undefined) {
+ delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC
+ } else {
+ process.env.ORCA_DISABLE_CODEX_TRUST_RPC = previousDisableTrustRpc
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let tmpHome: string | |
| let userDataDir: string | |
| let previousUserDataPath: string | undefined | |
| beforeEach(() => { | |
| tmpHome = mkdtempSync(join(tmpdir(), 'orca-codex-home-')) | |
| userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-user-data-')) | |
| previousUserDataPath = process.env.ORCA_USER_DATA_PATH | |
| process.env.ORCA_USER_DATA_PATH = userDataDir | |
| homedirMock.mockReturnValue(tmpHome) | |
| resolveCodexCommandMock.mockReturnValue(process.execPath) | |
| getPathMock.mockImplementation((name: string) => { | |
| if (name === 'userData') { | |
| return userDataDir | |
| } | |
| throw new Error(`unexpected app.getPath(${name})`) | |
| }) | |
| trustGrantInternals.resetDiagnostics() | |
| codexAppServerCapabilityCache.clear() | |
| }) | |
| afterEach(() => { | |
| trustGrantInternals.setGrantSessionRunnerSync(null) | |
| trustGrantInternals.resetDiagnostics() | |
| codexAppServerCapabilityCache.clear() | |
| delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC | |
| rmSync(tmpHome, { recursive: true, force: true }) | |
| rmSync(userDataDir, { recursive: true, force: true }) | |
| if (previousUserDataPath === undefined) { | |
| delete process.env.ORCA_USER_DATA_PATH | |
| } else { | |
| process.env.ORCA_USER_DATA_PATH = previousUserDataPath | |
| } | |
| vi.clearAllMocks() | |
| }) | |
| let tmpHome: string | |
| let userDataDir: string | |
| let previousUserDataPath: string | undefined | |
| let previousDisableTrustRpc: string | undefined | |
| beforeEach(() => { | |
| previousDisableTrustRpc = process.env.ORCA_DISABLE_CODEX_TRUST_RPC | |
| delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC | |
| tmpHome = mkdtempSync(join(tmpdir(), 'orca-codex-home-')) | |
| userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-user-data-')) | |
| previousUserDataPath = process.env.ORCA_USER_DATA_PATH | |
| process.env.ORCA_USER_DATA_PATH = userDataDir | |
| homedirMock.mockReturnValue(tmpHome) | |
| resolveCodexCommandMock.mockReturnValue(process.execPath) | |
| getPathMock.mockImplementation((name: string) => { | |
| if (name === 'userData') { | |
| return userDataDir | |
| } | |
| throw new Error(`unexpected app.getPath(${name})`) | |
| }) | |
| trustGrantInternals.resetDiagnostics() | |
| codexAppServerCapabilityCache.clear() | |
| }) | |
| afterEach(() => { | |
| trustGrantInternals.setGrantSessionRunnerSync(null) | |
| trustGrantInternals.resetDiagnostics() | |
| codexAppServerCapabilityCache.clear() | |
| if (previousDisableTrustRpc === undefined) { | |
| delete process.env.ORCA_DISABLE_CODEX_TRUST_RPC | |
| } else { | |
| process.env.ORCA_DISABLE_CODEX_TRUST_RPC = previousDisableTrustRpc | |
| } | |
| rmSync(tmpHome, { recursive: true, force: true }) | |
| rmSync(userDataDir, { recursive: true, force: true }) | |
| if (previousUserDataPath === undefined) { | |
| delete process.env.ORCA_USER_DATA_PATH | |
| } else { | |
| process.env.ORCA_USER_DATA_PATH = previousUserDataPath | |
| } | |
| vi.clearAllMocks() | |
| }) |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/codex/codex-app-server-client.ts (1)
114-146: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRemove strict listing count checks to support normalized key aliases.
The implementation correctly added
setContainsEveryto verify normalized key coverage, but incorrectly retained the strict listing count checks (managedListings.length !== expectedKeys.sizeandverifiedListings.length !== expectedKeys.size).Because
matchManagedfilters the incoming listings such that only expected keys are kept, the only way the length check can fail while coverage passes is if legitimate duplicate aliases exist (e.g., length > size). Retaining this check will incorrectly reject valid trust grants when aliases are present, reintroducing the exact issue the normalized coverage check was meant to solve.🐛 Proposed fix
- if ( - managedListings.length !== expectedKeys.size || - !setContainsEvery(managedKeyCoverage, expectedKeys) - ) { + if (!setContainsEvery(managedKeyCoverage, expectedKeys)) { return { outcome: 'verify-failed', reason: `hooks/list reported ${managedListings.length} entries covering ${managedKeyCoverage.size} of ${expectedKeys.size} expected managed entries` } } const needingTrust = managedListings.filter((listing) => listing.trustStatus !== 'trusted') if (needingTrust.length > 0) { // Why: same wire shape as the Codex TUI "Trust all" flow — one upsert // edit under hooks.state with each key's Codex-computed current hash. const value: Record<string, { trusted_hash: string }> = {} for (const listing of needingTrust) { value[listing.key] = { trusted_hash: listing.currentHash } } await rpc.request('config/batchWrite', { edits: [{ keyPath: 'hooks.state', value, mergeStrategy: 'upsert' }], reloadUserConfig: true }) } const verifyResult = await rpc.request('hooks/list', { cwds: [request.hooksListCwd] }) const verifiedListings = collectHookListings(verifyResult).filter(matchManaged) const verifiedKeyCoverage = normalizedKeyCoverage(verifiedListings) const untrusted = verifiedListings.filter((listing) => listing.trustStatus !== 'trusted') - if ( - verifiedListings.length !== expectedKeys.size || - !setContainsEvery(verifiedKeyCoverage, expectedKeys) || - untrusted.length > 0 - ) { + if (!setContainsEvery(verifiedKeyCoverage, expectedKeys) || untrusted.length > 0) { return { outcome: 'verify-failed',
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: dec95d73-20fb-4c75-a990-6bec2bbe8378
📒 Files selected for processing (13)
src/main/codex-accounts/runtime-home-service.test.tssrc/main/codex-accounts/runtime-home-service.tssrc/main/codex/codex-app-server-client.test.tssrc/main/codex/codex-app-server-client.tssrc/main/codex/codex-app-server-session.tssrc/main/codex/codex-real-home-path.test.tssrc/main/codex/codex-real-home-path.tssrc/main/codex/codex-session-backfill-audit.tssrc/main/codex/codex-session-backfill-copy.tssrc/main/codex/codex-session-backfill.test.tssrc/main/codex/codex-session-backfill.tssrc/main/codex/codex-session-index-heal-state.tssrc/main/codex/codex-session-index-heal.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- src/main/codex/codex-real-home-path.ts
- src/main/codex/codex-session-backfill-audit.ts
- src/main/codex-accounts/runtime-home-service.test.ts
- src/main/codex/codex-app-server-client.test.ts
- src/main/codex-accounts/runtime-home-service.ts
- src/main/codex/codex-session-index-heal.test.ts
- src/main/codex/codex-session-backfill.ts
- src/main/codex/codex-session-index-heal-state.ts
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 6fb4468b-2d04-4abd-887e-9745c9dc7716
📒 Files selected for processing (4)
src/main/ai-vault/codex-session-root-dedup.test.tssrc/main/ai-vault/codex-session-root-dedup.tssrc/main/codex/codex-app-server-client.test.tssrc/main/codex/codex-app-server-session.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/ai-vault/codex-session-root-dedup.test.ts
- src/main/ai-vault/codex-session-root-dedup.ts
The mobile resume surface types a bare real-home codex resume into a freshly created pane, but never asked for CODEX_HOME/ORCA_CODEX_HOME deletion at pane spawn, so an agentDefaultEnv-pinned or daemon-inherited Codex home rerouted the resume away from the user's real ~/.codex while the same session resumed correctly on desktop. Share the deletion helper from the AI Vault resume builders and forward it through the mobile launch and session.tabs.createTerminal call.
Live E2E validation — PASS (5/5)Validated live against the real Electron app with the real-home flag forced ON (branch head
112/112 targeted tests passed; worktree stayed clean. Automated live validation run as part of the pre-RC hardening sweep. |
fa3e83b to
b31658b
Compare
…ssion-dedup-index-heal # Conflicts: # src/main/ai-vault/remote-session-scanner.ts # src/main/ai-vault/session-scanner.ts
…9501) * feat(codex): backfill managed-home sessions into the real Codex home once per host Orca-launched Codex sessions currently land only in the Orca-managed runtime home, so the user's own `codex resume` picker and app history never see them (#4444, #8612). Backfill the managed sessions tree into the real ~/.codex/sessions/YYYY/MM/DD layout once per host: - hardlink first (one physical rollout log), copy as the cross-volume fallback; existing target files are always skipped, nothing in either home is deleted or moved - idempotent; per-file failures leave the completion marker unset so the next startup retries cheaply - JSONL audit log of every link/copy/failure under <userData>/codex-session-backfill/ - honors the custom Codex session source home override, mirroring the existing system->managed bridge WSL managed homes are distro-local and need an in-distro variant; that is a follow-up. * feat(codex): flag-gated system-default real-home routing scaffolding Staged internal flag (default OFF, no settings UI): route the SYSTEM-DEFAULT Codex account at the user's real ~/.codex instead of Orca's managed runtime home. Flag OFF is byte-identical to today; managed (multi-account) selections are unchanged in either state. Routing (flag ON + host system default = no managed account): - CodexRuntimeHomeService.prepareForCodexLaunch / prepareForRateLimitFetch return null so the PTY/env layer injects no managed CODEX_HOME and the rate-limit fetcher + auth-presence gate fall back to ~/.codex (the background poller stops spawning Codex against the managed home — the #5370 auth war). - buildPtyHostEnv strips only a nested-Orca-inherited Orca-owned override (CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a user-set CODEX_HOME. Shell-ready re-exports already no-op without the marker. - The headless commit-message Codex path strips the same inherited override. Hook install for the real-home lane (append-last into ~/.codex/hooks.json, trust via the app-server client) lands with the trust plumbing; the managed hook install is skipped for this lane meanwhile. Credit @jellychoco (#8606) for the native-home routing direction. Depends on the codex trust-rpc-grant plumbing for the real-home hook installer. * fix(codex): strip the daemon-inherited Orca CODEX_HOME override for real-home routing The daemon spawns PTYs from its own inherited environment and honors only spawnOptions.envToDelete, so mutating the sparse env object was not enough to strip an Orca-owned CODEX_HOME the daemon already carries. Add the strip to envToDelete for both daemon host-spawn paths, preserving a user-set CODEX_HOME. Verified live via CDP against a sandboxed dev instance (flag ON): an Orca-spawned pane reports empty CODEX_HOME/ORCA_CODEX_HOME, so Codex resolves its own ~/.codex. Adds daemon-path unit coverage (strip Orca-owned, preserve user-owned, no-op when flag OFF). * fix(codex): harden one-time session backfill * test(codex): cover staged cross-volume install * feat(codex): app-server trust-grant client, capability cache, and grant ledger Short-lived codex app-server JSON-RPC client (hooks/list + config/batchWrite, the same pair the Codex TUI 'Trust all' flow calls), run in a bundled ELECTRON_RUN_AS_NODE entry so synchronous launch prep can block on it with a hard deadline and guaranteed child reap. Capability cache modeled on GitCapabilityCache, scoped per execution host (native vs each WSL distro), with a narrow unknown-method/missing-subcommand unsupported predicate. The grant ledger records verified grants so steady-state launches skip the RPC. * fix(codex): grant managed hook trust via codex app-server RPCs in install/refresh Host and WSL installs now grant trust for Orca's managed status hooks through codex's own hooks/list -> config/batchWrite -> re-list verify, scoped to exactly the managed entries; the previous computeTrustedHash lane is the unchanged fallback for incapable/erroring CLIs. getStatus and the removal paths recognize ledger-recorded codex hashes so drift between codex's real algorithm and the replica no longer misreports or strands trust. SSH remote install is untouched by design. * test(codex): cover app-server trust grant client, cache, ledger, and lanes * test(codex): cover commit-message real-home override strip/preserve Adds the two cases for the headless commit-message Codex env under real-home routing: a nested-Orca-inherited Orca-owned CODEX_HOME is stripped, and a user-owned CODEX_HOME is preserved. * test(codex): WSL grant-lane coverage — in-distro invocation and fallback parity * feat(codex): real-home hook installer trusted via the codex app-server grant client With the real-home flag ON and the system-default selection, install Orca's status hook into the user's real ~/.codex before any pane spawns: - entry APPENDED LAST per managed event: codex hook trust keys are positional (source:event:group:handler), so appending keeps every user entry's position and trust record intact; user entries and unknown top-level hooks.json fields are preserved verbatim - trust is granted exclusively through the codex app-server client (hooks/list + config/batchWrite, verified by re-list); Orca never writes [hooks.state] into the user's real config.toml itself - if the grant lane is unavailable (old binary, unsupported RPC, verify failure), the appended entry is rolled back byte-exactly and the host keeps the managed-home lane end to end (PTY env, rate limits, commit messages) via a lane gate on the runtime-home service - one-time pristine backup of the user's hooks.json under Orca's userData; a rolling .bak sits next to the file (existing atomic writer) - hook opt-out sweeps Orca entries from the real home and drops Orca-owned trust records; flag-off downgrade re-arms the existing legacy system-home sweep, which removes the entry and its trust keys cleanly - the legacy system-home sweep is suppressed only while the real-home lane owns ~/.codex/hooks.json, so managed installs cannot delete the entry * fix(codex): resolve the trust-grant entry without requiring electron The grant bridge is reachable from plain-Node CLI entries, where the plain-node entry guard rejects any chunk containing require("electron"). Resolve the bundled session entry from __dirname (root chunk and chunks/ layouts) with an app.asar -> app.asar.unpacked rewrite for packaged runs, instead of electron's app path APIs. * fix(codex): keep session backfill off main thread Use asynchronous, sequential filesystem operations for the one-time rollout backfill, and avoid repeated target-directory probes. Treat inaccessible managed session roots as retryable failures instead of writing a false completion marker. * fix(codex): harden app-server trust grant fallback * fix(codex): install cross-volume session backfill copies atomically On a real Codex home whose filesystem supports no hardlinks (exFAT/FAT, some network mounts), the staged cross-volume copy was installed with a non-atomic copyFile(..., COPYFILE_EXCL) straight into the final rollout-*.jsonl name. An install interrupted mid-copy (app quit, crash, ENOSPC during the deferred run) could strand a truncated rollout that the next run then skips as already-present, defeating the staging design's own guarantee that a failed copy never leaves a partial session behind. Install the fully-staged copy with an atomic rename instead, guarded by an existence re-check so it keeps the never-overwrite contract (and the rename source is the same immutable managed rollout, so any clobber would be byte-identical). Cover the no-hardlink-support target and an interrupted install that must leave no partial in the user's sessions tree. * fix(codex): resolve grant entry from __dirname so plain-node CLI entries stay electron-free The build guard rejects any electron require reachable from plain-node entries; the bridge now maps app.asar to app.asar.unpacked by string replacement instead of consulting electron app paths. CLI typecheck project lists the new trust-grant module graph. * fix(codex): harden trust grant reconciliation * fix(codex): restore trust config permissions on rollback * fix(codex): harden real-home routing cleanup and retries * fix(codex): preserve unicode trust RPC responses * fix(codex): preserve remote env and complete real-home cleanup * fix(codex): preserve real-home lane invariants * test(terminal): isolate replacement idle reset assertion * fix(codex): preserve real-home dotfile links * fix(codex): preserve verified trust grants across launch prep * fix(codex): preserve dangling config symlinks on rollback * fix(codex): don't revoke a just-granted WSL home on a false 'missing' probe The async wsl.exe canonical-path settlement could report the runtime home 'missing' immediately after a verified RPC grant (a false negative — codex had just written and re-listed trust there), which drove the reconciliation 'remove' branch to delete all six granted [hooks.state] tables, leaving a bare [hooks.state] the launching pane read as 'hooks need review'. A 'missing' settlement now revokes only when no successful install ran this generation; a genuinely moved home still resolves to a different path and reinstalls. * test(codex): model codex config/batchWrite faithfully on Windows The grant-lane stub simulated codex by calling Orca's upsertHookTrustEntries, which writes both separator variants for a Windows key (a fallback-lane compat shim real codex never does) — fabricating duplicate tables and whitespace the RPC path never produces, so the byte-stable and no-duplicate assertions failed on win32. Replace it with a single-variant, blank-line-separated writer that matches the real 0.144.x binary's output. * feat(codex): collapse duplicate session listings across Codex roots Backfilled/bridged rollouts are hardlinked into both the real ~/.codex and Orca's managed runtime home, so AI Vault listed each session once per root (#7521). Dedup candidates by rollout file name pre-parse and parsed sessions by session id post-parse, keeping the canonical root: host real home first (unprefixed resume), then the managed runtime home, then other homes. Applies to local, WSL, and SSH-remote scans. * feat(codex): background sqlite index heal for backfilled sessions Codex's own state-DB metadata backfill is one-shot, so rollouts hardlinked in by Orca's session backfill never become visible to Codex's DB-driven surfaces. Extract the app-server stdio JSONL transport into codex-app-server-session (shared with the trust-grant client) and add a bounded, resumable background pass that drives Codex's lazy indexing via thread/read per backfilled session: recent-first, batched onto one short-lived server per batch with small concurrency, ledger + marker so steady-state startups are a no-op, stop-aware on quit, and capability-aware on CLIs without the app-server surface. * fix(codex): preserve session identity during dedup heal * fix(codex): preserve user trust during real-home cleanup * fix(codex): harden real-home heal boundaries * fix(codex): fail closed on unsafe backfill install * fix: harden real-home hook cleanup * fix(ai-vault): preserve execution boundaries and reap children * fix(codex): narrow app-server unsupported detection * fix(codex): bound user hook trust rebase retries per host The rebase lane ran a codex app-server session on every launch prep while a host was stuck (CLI without app-server support, or keys hooks/list cannot match). Gate the transaction on the shared capability cache and add the same 5-minute transient cooldown the grant lane uses, so sweep and legacy-cleanup retries cost plain fs reads instead of a codex session per pane spawn. * fix(codex): enforce real-home resume and heal boundaries * fix(codex): establish real-home lane before cleanup * fix(codex): stop index heal before delayed spawn * fix(codex): protect symlinked rolling backups * fix(ai-vault): preserve resume env deletion through drag * fix(codex): strip inherited Codex homes on mobile real-home resume The mobile resume surface types a bare real-home codex resume into a freshly created pane, but never asked for CODEX_HOME/ORCA_CODEX_HOME deletion at pane spawn, so an agentDefaultEnv-pinned or daemon-inherited Codex home rerouted the resume away from the user's real ~/.codex while the same session resumed correctly on desktop. Share the deletion helper from the AI Vault resume builders and forward it through the mobile launch and session.tabs.createTerminal call. * fix(codex): gate session migration on real-home lane * fix(codex): stop session backfill after opt-out * fix(codex): keep session heal failures retryable * fix(codex): keep session migration state recoverable * fix(codex): retry republished missing session heals * fix(codex): preserve hook symlink trust path * fix(codex): disambiguate POSIX trust paths * fix(codex): align hook trust source paths * fix(codex): harden trust grant lifecycle * fix(codex): restore envToDelete on client invocation type after base reconcile * test(codex): type child.stdout as PassThrough for oversized-output write * Assemble RC: reconcile app-server transport API across PRs Unify on the object RPC surface from the index-heal transport (#8921) while preserving the default-home env strip (#8828) and the narrowed missing-app-server capability signal (#8847): adapt the user-hook-trust-rebase consumer + tests, port envToDelete stripping into the shared session, and route stderr classification through the canonical capability-signal module. * RC: enable system-default real-home routing by default (flag ON) Flip codexSystemDefaultRealHomeEnabled to default ON for this RC's staged rollout (a user can still opt out by setting it false, which stays byte-identical to managed-home behavior). This is the only intended behavior difference between the RC branch and the individual PRs. Updates the two tests that assumed the prior OFF default. * fix(codex): snapshot hooks.json bytes+parse in one read to close real-home clobber race The install/sweep/legacy-cleanup paths parsed hooks.json, then did a separate later read to capture the previous bytes for the pre-write generation guard. A concurrent save (second Orca instance or the user editing the file) could land between the parse and that second read and be silently overwritten. readHooksJsonWithRaw returns the raw bytes and parse from a single read so the guard compares against exactly what it parsed. Adds a regression test that mutates hooks.json mid-RPC and asserts the sweep aborts without clobbering. * fix(codex): sanitize managed account config trust * fix(codex): guard OAuth add for custom providers * fix(codex): persist outgoing managed tokens before real-home lane takeover (PR-C) prepareForCodexLaunch returns null early for the real-home / system-default lane before syncForCurrentSelection runs. If a managed account is still recorded as synced when the selection has dropped to the system default (nulled without a sync pass, or auto-deselect on missing managed auth), a Codex-refreshed token stranded in the shared runtime home is never persisted to its canonical per-account home -> token loss. Read the outgoing managed account's refreshed token back before the real home takes over. The real-home lane implies host === null, so running the managed->system-default transition restores only Orca's runtime mirror from ~/.codex and never writes the real ~/.codex. It is a no-op once the selection has already been reconciled, so the normal select path does not double-write. * fix(codex): preserve refreshes across all default transitions * feat(codex): show system-default/real-home account identity in switcher (PR-B) The account switcher modeled the system-default Codex account as activeAccountId:null with no identity fields, so the null row rendered blank ("System default" / generic subtitle) even though its effective login is whatever ~/.codex/auth.json currently is. Add a CodexSystemDefaultIdentity descriptor {hasAuth, authKind, email, providerAccountId, workspaceLabel} to CodexRateLimitAccountsState, resolved live and READ-ONLY from ~/.codex by the accounts service and returned from listAccounts()/getSnapshot(). The settings switcher now renders the null (system-default) row as that real identity: the OAuth email when signed in, "Custom provider — no usage tracked." for env-key/custom-provider logins (auth.json with OPENAI_API_KEY, or an OPENAI_API_KEY env with no auth.json), and the generic fallback when signed out. Identity is host-scoped (per-distro WSL keeps the generic label). Orca never writes ~/.codex; managed-account switches only touch Orca-owned homes, so the system-default identity stays a stable, displayed source of truth. Usage already routes to the real home via getSystemCodexHomePath, so the switcher now attributes it to a real face. Tests (sandboxed temp homes only): OAuth email/provider resolution, api-key auth.json and env-key (no auth.json) as custom-provider, signed-out, and select/deselect of a managed account never mutating ~/.codex/auth.json. * fix(codex): parse multiline provider pins in OAuth guard * fix(codex): harden managed trust sanitization * fix(codex): harden system-default identity rendering * feat(codex): give each managed account a self-contained CODEX_HOME; retire shared mirror (PR-E) With the real-home flag ON, a host managed account now launches directly against its own codex-accounts/<id>/home instead of the shared runtime mirror + auth.json hot-swap: - codex-home-paths: syncSystemCodexResourcesIntoManagedHome links system resources into any managed home (ownership-marker discipline; never symlinks into / mutates ~/.codex). - runtime-home-service: prepareForCodexLaunch / prepareForRateLimitFetch / syncForCurrentSelection route the per-account home directly and skip the shared-home hot-swap + token read-back; each home keeps its own auth in place (fixes GAP-5 concurrent auth race). Session discovery scans every per-account home. - hook-service / hook-trust-promotion: install/getStatus/refresh accept a runtimeHomePath so hooks + RPC-granted trust land in the per-account home. - service: config mirror into a self-contained home uses the trust- preserving merge so granted hook/project trust survives account switches. - codex-session-root-dedup: rank codex-accounts/<id>/home as canonical managed alongside the shared runtime home. Flag-OFF and the system-default real-home (null) lane are unchanged; the nested-Orca CODEX_HOME===ORCA_CODEX_HOME daemon strip (#5370) is preserved. Sandboxed tests only; ~/.codex is never mutated. * fix(codex): validate per-account home ownership * fix(codex): keep managed rollouts discoverable across real-home opt-out WI-4 lossless migration/rollback validation for pre-E shared-mirror managed accounts. Session discovery gated the per-account home scan on the real-home flag, so opting back out (flag OFF) hid every rollout an account accumulated while the flag was ON — the data stayed on disk but vanished from the AI Vault until the flag flipped back on. Scan a managed host home whenever it holds a sessions/ tree, independent of the flag; a never-enabled install keeps its homes credential-only so opt-out stays byte-identical to today. Forward migration was already lossless (the shared mirror is always scanned) and the opt-out credential read-back already refuses to overwrite a fresher per-account token; add tests locking all three invariants. Sandboxed tests only; ~/.codex is never touched. * fix(codex): migrate stranded shared auth on E takeover * test(e2e): isolate Electron from developer Codex home * test(codex): add real-account validation harness * fix(codex): finish C and E matcher composition * fix(codex): bound validation harness shutdown * test(codex): isolate hook lifecycle user data * test(codex): cover realistic account-home migration * fix(codex): keep standalone home tripwire active * test(codex): fingerprint system auth in validation reports * fix(codex): bind managed homes to account ownership * fix(codex): normalize Windows trust source identity * fix(codex): make Windows trust upgrade transactional * test(codex): use TypeScript pipeline for validation scripts * test(codex): run validation modules through native node * test(codex): allow slow Windows tripwire startup * fix(codex): survive lingering Windows codex login processes in add-account On Windows, codex login can keep running (with descendants) after it has written auth.json, holding OS handles on the per-account managed home (log/codex-login.log). That made doAddAccount's post-login cleanup fail with ENOTEMPTY (rmSync) and left an orphaned codex-accounts/<id>/home. - runCodexLogin now watches for auth.json on Windows and force-kills the login process tree (taskkill /t) if it lingers past a short grace period; the forced exit is treated as a successful login. The 120s timeout path also kills the whole tree instead of only the direct child. macOS/Linux behavior is unchanged. - safeRemoveManagedHome now removes homes with rmSync maxRetries / retryDelay (mirroring the local-worktree-filesystem Windows policy) and no longer lets a cleanup failure mask the original add error. - run-codex-real-account-validation.mjs accepts --temp-parent / ORCA_CODEX_VALIDATION_TEMP_PARENT so the disposable root can live outside %USERPROFILE% on Windows, and fails with an actionable message before creating anything when the temp parent is inside the primary home. The real-home guard is unchanged. * fix(codex): preserve managed-account MCP .credentials.json on per-account-home migration (#8440) Codex file-mode MCP OAuth tokens live in $CODEX_HOME/.credentials.json, keyed by MCP server URL with no account identity of their own. The legacy shared-mirror -> per-account-home migration only carried auth.json, so an existing managed account with authed MCP servers had its tokens stranded on upgrade and silently needed re-auth. Carry the shared mirror's .credentials.json into the same identity-proven per-account home alongside auth.json: only into the single uniquely-matched active account (no cross-account leak), only when the destination has none yet (never clobber a newer file the account authed in its own home), atomic 0600, absent-source no-op. New MCP auth already lands in the per-account home since that home is CODEX_HOME. * fix(codex): preserve Windows reauthentication login flow * test(codex): build real-account validation harness cross-platform on Windows The harness built its app with execFileSync('npx', ['electron-vite', ...]), but npx resolves to a .cmd shim on Windows that execFileSync cannot launch (ENOENT), so the harness could not build its own app there and required --skip-build with a prebuilt out/main/index.js. Extract resolveElectronViteBuildCommand(repoRoot): it runs the repository-local electron-vite JS entry (node_modules/electron-vite/bin/electron-vite.js) with the current Node binary (process.execPath), which resolves identically on macOS, Linux, and Windows with no shell. It throws a clear error if the local entry is missing (install deps or pass --skip-build). --skip-build behavior is unchanged. Add regression coverage asserting the build command uses process.execPath and the repo-local JS entry (not npx), and that a missing entry fails clearly. * fix(codex): version the MCP creds migration independently of the auth marker The auth carry and the MCP .credentials.json carry (#8440) shared one existence-only v1 marker, so any build that stamped the auth-only marker first would strand the MCP store forever. The MCP carry now concludes via its own per-account-mcp-creds-migration-v1.json marker and runs even when the auth marker is already present; ordering is code-enforced instead of landing-discipline-enforced. Also isolate per-account read failures: one stale or deleted account home no longer aborts the whole migration. The broken account stays in the unique-identity ambiguity gate via its stored fields but is never read or written, so the active account still migrates. * fix(codex): fail corrupt managed auth.json without echoing credential bytes A raw JSON.parse SyntaxError from loadOAuthCredentials could carry auth file fragments into logs and the add/reauth error surface. Throw a sanitized error instead; filesystem errors still propagate unchanged. * fix(mobile): give the pairing runtime a disposable home for the E2E boot guard The main-process guard now refuses to start with ORCA_E2E_USER_DATA_DIR set but the real user home, and this was the one caller not updated — the temporary pairing runtime crashed before emitting its pairing URL. * test(codex): canonicalize harness containment guards and retry cleanup Resolve symlinks before the disposable-root containment checks so a symlinked temp parent cannot smuggle the throwaway home inside the primary home, and give the final cleanup rm Windows retry/force so a briefly lingering codex handle cannot strand the credential-bearing root. * test(codex): add lane-aware containment mode to the real-account harness The Windows gate-D run proved strict zero-event whole-profile containment is structurally unreachable with the real-home flag ON: system-default spawn sites deliberately delete CODEX_HOME so native codex resolves the real ~/.codex, and on Windows the binary ignores the USERPROFILE sandbox. Its own volatile runtime churn (root sqlite/WAL/SHM, tmp/, log/) is the shipped Phase-1 design, not a candidate defect. --lane-aware-containment records those designed events without aborting while every other real-home write — auth.json, config.toml, .credentials.json, hooks.json, sessions/, anything unknown — remains a hard violation and still aborts the run. Default behavior is unchanged (strict); the absolute zero-event claim stays carried by macOS runs, where HOME does sandbox native codex. * test(codex): allow the real-account harness to pin the real-home flag off --system-default-real-home off seeds and env-pins the flag OFF so every codex spawn gets an explicit managed CODEX_HOME and native codex never resolves the OS profile. This is the only Windows configuration where the strict zero-event whole-profile tripwire is reachable, and it matches the stable-rollout default; flag-ON runs keep lane-aware classification. * test(codex): correct the flag-off harness comment to kill-switch rationale The rollout ships all codex-home changes at once (no phased rollout), so flag OFF is the emergency kill-switch lane, not the stable default. * test(e2e): canonicalize the isolated E2E home path The disposable HOME lives under os.tmpdir(), whose spelling is an alias on CI (macOS /var symlink, Windows 8.3 RUNNER~1). Git canonicalizes worktree paths, so worktrees created under the aliased home never matched the app's listing — golden core flows and the packaged crash-survival harness failed with 'worktree created but not found in listing'. Resolve the home to its canonical spelling at creation in both the e2e helper and the packaged-app driver. * fix(codex): address CodeRabbit review on the landing PR - carry envToDelete through the mobile agent-resume startup plan so a real-home Codex resume cannot inherit an ambient CODEX_HOME - strip Orca-owned Codex overrides in the commit-message WSL fallback, matching the host fallback - strip ELECTRON_RUN_AS_NODE in the computer-e2e driver like every other home-isolation caller - drop the unused hooksEnabled parameter from isRealHomeCodexHookLaneUsable * feat(codex): ship real-home routing unconditionally, remove the rollout flag The codexSystemDefaultRealHomeEnabled setting is gone from types and constants and the helper no longer consults settings — the system-default real-home lane and per-account homes ship for everyone in one release. This also un-strands profiles that rc-era builds stamped with false (the setting had no UI, so every stored false was a seeded artifact that would have silently kept those users on the legacy mirror forever). The ORCA_CODEX_SYSTEM_DEFAULT_REAL_HOME env override survives strictly as a test-rig control: the containment harness pins the legacy lane for strict zero-event Windows runs, e2e home isolation pins lanes inside disposable homes, and the legacy-lane test suites now route their per-test lane selection through it. --------- Co-authored-by: OrcaWin <alpha-eng@stably.ai>
|
Closing as superseded: this work landed on main via #9501 (real-home routing + self-contained multi-account homes) and shipped in v1.4.149 stable. Content verified present on main (module + wiring spot-checked). |
Stacking
Stacked on #8828 (flag-gated real-home routing), and via it on #8847 (app-server trust grant); this branch also merges #8802 (session backfill) because the dedup and heal work operates on the backfill's audit ledger and hardlinks. Base is
mainso CI runs, but the diff shown by GitHub includes the stacked PRs — the net-new work is the two commits22dc271b9and2f41d62d3. Intended to land via the real-home integration branch (flag default ON for the RC), not directly to main.What this PR does
Last functional gate before the real-home RC: without it, every backfilled Codex session lists twice in AI Vault (#7521, aggravated by the #8802 backfill), resumes can target the frozen managed home whose
auth.jsonstops refreshing after the flip, and Codex's own DB-driven surfaces never learn about backfilled rollouts (its sqlite metadata backfill is one-shot).1. Session dedup across Codex roots (
src/main/ai-vault/codex-session-root-dedup.ts)The session bridge and the real-home backfill hardlink one physical rollout into both the managed runtime home and the real
~/.codex, and AI Vault scans both roots. The listing id embeds the file path, so one session became one row per root.Two collapse passes, both pure and shared by the local, WSL, and SSH-remote scanners:
sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonllayout, so an identical rollout file name across Codex roots is the same session. Duplicate aliases stop consuming the parse budget and can no longer crowd the capped listing.session_metaids that differ from the file name.Canonical root rule (lowest rank wins):
codexHome: null) — canonical when present; display once, attribute to the real home.CODEX_HOMEs).Ties resolve newest-first, then stable path order. Path handling is separator-agnostic (a posix host scans remote/WSL win32 paths).
2. Canonical-root resume
AiVaultSession.codexHomedrives every resume stamp (AI Vault desktop command, the pre-bakedresumeCommandused by SSH-remote rows, and the mobile resume path — all through the shared builder). Because the canonical row for a both-roots session is the real-home row, its resume command is a barecodex resume <id>with noCODEX_HOMEprefix, and pane launches in the real-home lane inject no managedCODEX_HOME(routing from #8828). The hardlink makes this safe: appends through either root continue the same inode. Managed-only sessions (not backfilled — e.g. WSL for now, or a failed backfill copy) keep their managed-home stamp unchanged.3. Sqlite index heal (
src/main/codex/codex-session-index-heal.ts)Codex stamps its state-DB
backfill_statecomplete on first app-server startup, so rollouts hardlinked in later never reach the DB on their own (verified against codex 0.144.4).thread/readis Codex's sanctioned lazy-indexing path: it parses the rollout and upserts the thread row. Orca never writes Codex's sqlite schema itself.The heal pass:
thread/readper id against the real home (explicitCODEX_HOMEpin so a daemon-inherited managed home can never redirect it), batched onto one short-livedcodex app-serverper batch (50 reads/batch, 2 in-flight reads, 500 ms between batches, per-batch deadline with guaranteed child reap) — no thundering spawn;shouldStoppolled between reads/batches; wired to app quit) and resumable (per-id ledger appends; transport failures leave unprocessed ids unrecorded for the next pass);stats and no spawn;healed,missing(rollout deleted since audit),failed(recorded, not retried until a version bump); a CLI without the app-server surface orthread/readmarks the passunsupportedwith a 24 h re-probe backoff — no retry churn.src/main/index.ts), sharing its 15 s startup deferral.The app-server stdio JSONL transport was extracted from the trust-grant client into
codex-app-server-session.ts(spawn, handshake, framing, deadline, oversized-line guard, stderr capability classification, reap) and is now shared by the trust grant and the heal; the trust client's public API and wire behavior are unchanged (its 14 existing tests pass unmodified).Verification
Unit (all green;
vitest --config config/vitest.config.ts)codex-session-root-dedup.test.ts— both-roots → real home wins; managed-only / real-only unchanged; id collisions (same-rank newest-first, stable tie-break); WSL pair → managed runtime home; never collapses across execution hosts or agents; win32-separator paths from a posix host.session-scanner-codex-dual-root.test.ts— fullscanAiVaultSessionsover a sandboxed real home + managed home with a genuine hardlink: one row,codexHome: null, real-homefilePath, resume command with noCODEX_HOME; managed-only session keeps its stamp.remote-session-scanner.test.ts— bridged rollout in both remote homes collapses to the managed runtime-home row.ai-vault-resume-command.test.ts— real-home canonical session emits noCODEX_HOMEstamp (plus existing managed-home stamp cases).codex-session-index-heal.test.ts(11 cases, scenario-driven stub app-server) — recent-first order; marker no-op (no spawn); audit-growth resume; missing/failed recorded and not retried; batching = one server per batch with bounded in-flight reads;shouldStopstops promptly and resumes next pass; unsupported via method-not-found and via missing subcommand; mid-batch server death aborts without recording, retried next pass; nothing-to-heal completes with zero spawns; non-link/copy audit records ignored.src/main/ai-vault,src/main/codex,src/main/codex-accounts,src/main/ipc/pty.test.ts, renderer resume-command suites: 951+ tests passing; typecheck (node/web/cli) andpnpm lintclean.Live sandboxed end-to-end (real dev app, real codex 0.144.4, flag ON; sandboxed
HOME+ORCA_DEV_USER_DATA_PATH— the developer's real~/.codexuntouched and verified so)backfill-complete.json: 3 scanned / 3 linked; both pathsino=40679431 nlink=2).index-heal-complete.jsonhealedThreads: 3, ledger in recent-first order; codex's ownstate_5.sqlitethreadstable gained all 3 rows (previews + provider populated).codex app-server→thread/list {useStateDbOnly: true, modelProviders: [], sourceKinds: [...]}against the sandbox home returns all 3 healed sessions. (Verification gotchas, confirmed against codex-rs 0.144.4 source: omittingmodelProvidersfilters to the current config provider, and the DB upsert fromthread/readlands asynchronously — assert in a fresh server session.)window.api.aiVault.listSessions): exactly 3 codex rows, zero duplicate session ids, all attributed to the real home (codexHome: null) even though each rollout exists at two scanned paths.cd '<cwd>' && codex resume <id>; the spawned codex process env contains noCODEX_HOME(real-home lane, asserted via process env inspection). Append-to-same-inode proven through codex itself (thread/resume+ item injection against the sandbox home): the rollout grew 148 201 → 148 485 bytes at the same inode, and the appended marker is readable through the managed-home path — one physical log.up-to-datewith no app-server spawn.Rig notes: one launch inherited the production
ORCA_USER_DATA_PATHfrom the launching terminal, which made the scan also pick up the production runtime home — an artifact of launching the sandbox from inside another Orca instance (a scrubbed-env relaunch lists exactly 3/3); the dedup handles such foreign roots correctly regardless (file-name aliases still collapse). macOS PTYs resetHOMEto the login user, so the pane-resume TUI leg asserted command string + process env, with the append property proven against the sandboxed home via app-server.Constraints honored
getSpawnArgsForWindows; WSL/SSH lanes keep today's managed-home resume behavior.