From 9327a7b643bed7843e4ceaa1b2edbc2784d24200 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:41:09 -0700 Subject: [PATCH] fix(codex): resume compressed rollout representations --- .../codex/codex-legacy-session-resume.test.ts | 21 +++-- src/main/codex/codex-legacy-session-resume.ts | 2 +- src/main/codex/codex-session-file-listing.ts | 30 ++++++- .../codex/codex-session-resume-home.test.ts | 58 ++++++++++++ src/main/codex/codex-session-resume-home.ts | 89 ++++++++++++++----- 5 files changed, 169 insertions(+), 31 deletions(-) diff --git a/src/main/codex/codex-legacy-session-resume.test.ts b/src/main/codex/codex-legacy-session-resume.test.ts index c69d4e4ed39..852da6b2c91 100644 --- a/src/main/codex/codex-legacy-session-resume.test.ts +++ b/src/main/codex/codex-legacy-session-resume.test.ts @@ -9,7 +9,7 @@ import { writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { basename, join } from 'node:path' import { prepareLegacySharedCodexSessionResume } from './codex-legacy-session-resume' describe('prepareLegacySharedCodexSessionResume', () => { @@ -47,6 +47,16 @@ describe('prepareLegacySharedCodexSessionResume', () => { expect(statSync(targetPath).ino).toBe(statSync(rolloutPath).ino) }) + it('materializes a compressed legacy rollout without changing its representation', async () => { + const compressedPath = `${rolloutPath}.zst` + rmSync(rolloutPath) + rolloutPath = compressedPath + writeFileSync(rolloutPath, 'compressed-rollout', 'utf-8') + + await expect(prepare()).resolves.toEqual({ useRealCodexHome: true }) + expect(readFileSync(targetRolloutPath(), 'utf-8')).toBe('compressed-rollout') + }) + it('coalesces concurrent resumes of the same legacy rollout', async () => { const [first, second] = await Promise.all([prepare(), prepare()]) @@ -129,14 +139,7 @@ describe('prepareLegacySharedCodexSessionResume', () => { } function targetRolloutPath(): string { - return join( - systemHome, - 'sessions', - '2026', - '07', - '20', - 'rollout-2026-07-20T12-00-00-session.jsonl' - ) + return join(systemHome, 'sessions', '2026', '07', '20', basename(rolloutPath)) } }) diff --git a/src/main/codex/codex-legacy-session-resume.ts b/src/main/codex/codex-legacy-session-resume.ts index c2febd6d772..3976b4a0d8d 100644 --- a/src/main/codex/codex-legacy-session-resume.ts +++ b/src/main/codex/codex-legacy-session-resume.ts @@ -162,7 +162,7 @@ function isLegacyRolloutRelativePath(relativePath: string): boolean { /^\d{4}$/.test(parts[0] ?? '') && /^\d{2}$/.test(parts[1] ?? '') && /^\d{2}$/.test(parts[2] ?? '') && - /^rollout-.+\.jsonl$/.test(parts[3] ?? '') + /^rollout-.+\.jsonl(?:\.zst)?$/.test(parts[3] ?? '') ) } diff --git a/src/main/codex/codex-session-file-listing.ts b/src/main/codex/codex-session-file-listing.ts index 6532f6ee5c9..8b0a8dd6f2e 100644 --- a/src/main/codex/codex-session-file-listing.ts +++ b/src/main/codex/codex-session-file-listing.ts @@ -58,6 +58,34 @@ export async function* listCodexSessionJsonlFilesIncrementally( rootPath: string, options: CodexSessionBridgeIncrementalOptions, onDirectoryError?: (directoryPath: string, error: unknown) => void | Promise +): AsyncGenerator { + yield* listCodexSessionFilesIncrementally( + rootPath, + options, + (fileName) => fileName.endsWith('.jsonl'), + onDirectoryError + ) +} + +/** Yields both physical representations that current Codex can resume. */ +export async function* listCodexSessionRolloutFilesIncrementally( + rootPath: string, + options: CodexSessionBridgeIncrementalOptions, + onDirectoryError?: (directoryPath: string, error: unknown) => void | Promise +): AsyncGenerator { + yield* listCodexSessionFilesIncrementally( + rootPath, + options, + (fileName) => fileName.endsWith('.jsonl') || fileName.endsWith('.jsonl.zst'), + onDirectoryError + ) +} + +async function* listCodexSessionFilesIncrementally( + rootPath: string, + options: CodexSessionBridgeIncrementalOptions, + isSessionFile: (fileName: string) => boolean, + onDirectoryError?: (directoryPath: string, error: unknown) => void | Promise ): AsyncGenerator { const batchSize = Math.max(1, options.batchSize ?? INCREMENTAL_BRIDGE_BATCH_SIZE) const yieldMs = Math.max(0, options.yieldMs ?? INCREMENTAL_BRIDGE_YIELD_MS) @@ -75,7 +103,7 @@ export async function* listCodexSessionJsonlFilesIncrementally( const childPath = join(currentDirectory, entry.name) if (entry.isDirectory()) { pendingDirectories.push(childPath) - } else if (entry.isFile() && entry.name.endsWith('.jsonl')) { + } else if (entry.isFile() && isSessionFile(entry.name)) { yield childPath } entriesSinceYield += 1 diff --git a/src/main/codex/codex-session-resume-home.test.ts b/src/main/codex/codex-session-resume-home.test.ts index 03c2c60fa48..19df5e873e7 100644 --- a/src/main/codex/codex-session-resume-home.test.ts +++ b/src/main/codex/codex-session-resume-home.test.ts @@ -95,11 +95,69 @@ describe('resolveTrustedCodexSessionResumeHome', () => { ).toBe(homePath) }) + it('follows Codex when a persisted plain rollout was compressed in place', async () => { + const homePath = mkdtempSync(join(tmpdir(), 'orca-codex-resume-home-')) + tempRoots.push(homePath) + const plainPath = join( + homePath, + 'sessions', + '2026', + '07', + '20', + 'rollout-2026-07-20T12-00-00-session.jsonl' + ) + const compressedPath = `${plainPath}.zst` + mkdirSync(join(plainPath, '..'), { recursive: true }) + writeFileSync(compressedPath, 'compressed-rollout') + + await expect( + findTrustedCodexSessionResume({ + sessionId: 'session-a', + transcriptPath: plainPath, + trustedCodexHomes: [homePath] + }) + ).resolves.toEqual({ homePath, transcriptPath: compressedPath }) + + writeFileSync(plainPath, 'active-rollout') + await expect( + findTrustedCodexSessionResume({ + sessionId: 'session-a', + transcriptPath: compressedPath, + trustedCodexHomes: [homePath] + }) + ).resolves.toEqual({ homePath, transcriptPath: plainPath }) + }) + + it('finds compressed rollouts for legacy records without transcript provenance', async () => { + const homePath = mkdtempSync(join(tmpdir(), 'orca-codex-resume-home-')) + tempRoots.push(homePath) + const sessionId = '019f81b9-19a9-7651-a8d1-352d9420bd11' + const compressedPath = join( + homePath, + 'sessions', + '2026', + '07', + '20', + `rollout-2026-07-20T12-00-00-${sessionId}.jsonl.zst` + ) + mkdirSync(join(compressedPath, '..'), { recursive: true }) + writeFileSync(compressedPath, 'compressed-rollout') + + await expect( + findTrustedCodexSessionResume({ + sessionId, + transcriptPath: undefined, + trustedCodexHomes: [homePath] + }) + ).resolves.toEqual({ homePath, transcriptPath: compressedPath }) + }) + it('finds older saved sessions by id when transcript provenance is absent', async () => { const sessionId = '019f81b9-19a9-7651-a8d1-352d9420bd11' const rolloutPath = `/managed/account/home/sessions/2026/07/20/rollout-2026-07-20T15-50-19-${sessionId}.jsonl` const listSessionFiles = async function* (sessionsRoot: string): AsyncIterable { if (sessionsRoot === '/managed/account/home/sessions') { + yield `/managed/account/home/sessions/misplaced-${sessionId}.jsonl` yield rolloutPath } } diff --git a/src/main/codex/codex-session-resume-home.ts b/src/main/codex/codex-session-resume-home.ts index d8aa8456d1c..ce60683a941 100644 --- a/src/main/codex/codex-session-resume-home.ts +++ b/src/main/codex/codex-session-resume-home.ts @@ -5,9 +5,15 @@ import { normalizeRuntimePathForComparison, relativePathInsideRoot } from '../../shared/cross-platform-path' -import { listCodexSessionJsonlFilesIncrementally } from './codex-session-file-listing' +import { listCodexSessionRolloutFilesIncrementally } from './codex-session-file-listing' -const ROLLOUT_RELATIVE_PATH = /^\d{4}\/\d{2}\/\d{2}\/rollout-[^/]+\.jsonl$/ +// Why: only Codex's dated rollout layout may establish account-home provenance; nested/misplaced JSONL must not select credentials. +const ROLLOUT_RELATIVE_PATH = /^\d{4}\/\d{2}\/\d{2}\/rollout-[^/]+\.jsonl(?:\.zst)?$/ + +function isCodexRolloutInsideSessionsRoot(sessionsRoot: string, filePath: string): boolean { + const relativePath = relativePathInsideRoot(sessionsRoot, filePath) + return Boolean(relativePath && ROLLOUT_RELATIVE_PATH.test(relativePath.replace(/\\/g, '/'))) +} function isRegularFile(filePath: string): boolean { try { @@ -17,23 +23,52 @@ function isRegularFile(filePath: string): boolean { } } +function resolveExistingRolloutPath( + transcriptPath: string, + fileIsRegular: (filePath: string) => boolean +): string | null { + const plainPath = transcriptPath.endsWith('.jsonl.zst') + ? transcriptPath.slice(0, -'.zst'.length) + : transcriptPath.endsWith('.jsonl') + ? transcriptPath + : null + if (!plainPath) { + return fileIsRegular(transcriptPath) ? transcriptPath : null + } + if (fileIsRegular(plainPath)) { + return plainPath + } + const compressedPath = `${plainPath}.zst` + return fileIsRegular(compressedPath) ? compressedPath : null +} + +function resolveTrustedCodexSessionResume(args: { + transcriptPath: string | undefined + trustedCodexHomes: readonly string[] + fileIsRegular?: (filePath: string) => boolean +}): { homePath: string; transcriptPath: string } | null { + const persistedPath = args.transcriptPath?.trim() + const transcriptPath = persistedPath + ? resolveExistingRolloutPath(persistedPath, args.fileIsRegular ?? isRegularFile) + : null + if (!transcriptPath) { + return null + } + + for (const homePath of args.trustedCodexHomes) { + if (isCodexRolloutInsideSessionsRoot(join(homePath, 'sessions'), transcriptPath)) { + return { homePath, transcriptPath } + } + } + return null +} + export function resolveTrustedCodexSessionResumeHome(args: { transcriptPath: string | undefined trustedCodexHomes: readonly string[] fileIsRegular?: (filePath: string) => boolean }): string | null { - const transcriptPath = args.transcriptPath?.trim() - if (!transcriptPath || !(args.fileIsRegular ?? isRegularFile)(transcriptPath)) { - return null - } - - for (const homePath of args.trustedCodexHomes) { - const relativePath = relativePathInsideRoot(join(homePath, 'sessions'), transcriptPath) - if (relativePath && ROLLOUT_RELATIVE_PATH.test(relativePath.replace(/\\/g, '/'))) { - return homePath - } - } - return null + return resolveTrustedCodexSessionResume(args)?.homePath ?? null } export async function findTrustedCodexSessionResume(args: { @@ -43,9 +78,9 @@ export async function findTrustedCodexSessionResume(args: { fileIsRegular?: (filePath: string) => boolean listSessionFiles?: (sessionsRoot: string) => AsyncIterable }): Promise<{ homePath: string; transcriptPath: string } | null> { - const directHome = resolveTrustedCodexSessionResumeHome(args) - if (directHome && args.transcriptPath) { - return { homePath: directHome, transcriptPath: args.transcriptPath.trim() } + const directSession = resolveTrustedCodexSessionResume(args) + if (directSession) { + return directSession } if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(args.sessionId)) { return null @@ -54,7 +89,7 @@ export async function findTrustedCodexSessionResume(args: { const listSessionFiles = args.listSessionFiles ?? ((sessionsRoot: string) => - listCodexSessionJsonlFilesIncrementally(sessionsRoot, { batchSize: 64, yieldMs: 0 })) + listCodexSessionRolloutFilesIncrementally(sessionsRoot, { batchSize: 64, yieldMs: 0 })) const expectedSuffix = `-${args.sessionId}.jsonl`.toLowerCase() const seenHomes = new Set() for (const homePath of args.trustedCodexHomes) { @@ -68,8 +103,22 @@ export async function findTrustedCodexSessionResume(args: { continue } for await (const filePath of listSessionFiles(sessionsRoot)) { - if (getRuntimePathBasename(filePath).toLowerCase().endsWith(expectedSuffix)) { - return { homePath, transcriptPath: filePath } + const plainFilePath = filePath.endsWith('.jsonl.zst') + ? filePath.slice(0, -'.zst'.length) + : filePath + // Why: the directory entry already proves the compressed file exists; only probe its preferred plain sibling. + const preferredFilePath = + plainFilePath !== filePath && (args.fileIsRegular ?? isRegularFile)(plainFilePath) + ? plainFilePath + : filePath + const plainFileName = getRuntimePathBasename(preferredFilePath) + .toLowerCase() + .replace(/\.zst$/, '') + if ( + isCodexRolloutInsideSessionsRoot(sessionsRoot, preferredFilePath) && + plainFileName.endsWith(expectedSuffix) + ) { + return { homePath, transcriptPath: preferredFilePath } } } }