From 2c87ee3eaa3badacacb8f800e0be30eebe69ef95 Mon Sep 17 00:00:00 2001 From: Aditya Vikram Singh <247195684+avs-io@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:15:34 +0530 Subject: [PATCH] fix(codex): walk billed+nest trees on the no-arg singleton filterOverlap required an explicit nest path, so export const codex = createCodexProvider() never deduped. When the resolved dir is a launcher nest and ~/.codex is a distinct existing tree, discover both and drop nest sources whose session id is already billed. Unique nest and billed sessions stay. Soften the Buzz doctor note to a presence heuristic. --- src/launcher-homes.ts | 7 +++-- src/providers/codex.ts | 53 +++++++++++++++++++++++------------- tests/launcher-homes.test.ts | 44 ++++++++++++++++++++++++++---- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/launcher-homes.ts b/src/launcher-homes.ts index bf603b72..1c5446f0 100644 --- a/src/launcher-homes.ts +++ b/src/launcher-homes.ts @@ -23,8 +23,9 @@ export function sameCodexHome(a: string, b: string): boolean { } /** True when `dir` is a Codex home nested under a launcher nest, and a distinct - * primary Codex home exists. Gate for overlap-only filtering on a *second* - * provider instance — it does not by itself drop sessions. */ + * primary Codex home exists. Gate for overlap-only filtering — it does not by + * itself drop sessions. The production no-arg factory uses this to walk both + * trees; an explicit nest factory uses it to drop overlapping nest ids. */ export function isNestedLauncherCodexHome( dir: string, opts: { primaryDir: string; launcherRoots: string[] }, @@ -125,7 +126,7 @@ export function collectLauncherNotes(home = homedir()): LauncherNote[] { name: 'buzz', path: buzz, billedVia: 'codex', - verdict: 'LAUNCHER (billed via Codex)', + verdict: 'LAUNCHER (heuristic; billed via Codex)', }) } const grokStore = join(home, '.grok') diff --git a/src/providers/codex.ts b/src/providers/codex.ts index ca3089ce..d036c4e6 100644 --- a/src/providers/codex.ts +++ b/src/providers/codex.ts @@ -1330,24 +1330,40 @@ export async function parseCodexFileFull(source: SessionSource, seenKeys: Set { + const id = rolloutFileSessionId(source.path) + return !(id && billedIds.has(id)) + }) +} + export function createCodexProvider( codexDir?: string, opts?: { primaryDir?: string; launcherRoots?: string[] }, ): Provider { const dir = getCodexDir(codexDir) - // Production singleton (no args) still honors CODEX_HOME / override as the - // one root. An explicit nest path — even without second-factory opts — - // overlap-filters against the default billed home using defaultLauncherRoots - // so a future createCodexProvider(nest) cannot double-count session ids. const primaryDir = opts?.primaryDir ?? defaultBilledCodexHome() const launcherRoots = opts?.launcherRoots ?? defaultLauncherRoots() + // Explicit nest factory whose path is a realpath alias of the billed home: + // empty so a second factory cannot double-count the same tree. The no-arg + // singleton must not take this branch — CODEX_HOME may be that alias. const duplicateHome = codexDir !== undefined && sameCodexHome(dir, primaryDir) && resolve(dir) !== resolve(primaryDir) - const filterOverlap = - codexDir !== undefined && - isNestedLauncherCodexHome(dir, { primaryDir, launcherRoots }) + const nestHome = isNestedLauncherCodexHome(dir, { primaryDir, launcherRoots }) + // Production `codex` singleton is createCodexProvider() with no args. When + // the resolved dir is a launcher nest and ~/.codex is a distinct existing + // tree, walk BOTH and drop nest sources whose session id is already billed. + const scanBoth = nestHome && codexDir === undefined return { name: 'codex', @@ -1364,13 +1380,12 @@ export function createCodexProvider( return toolNameMap[rawTool] ?? rawTool }, - // Same `dir` discoverSessionsInDir walks: /sessions (dated - // rollout files) and /archived_sessions. Honors CODEX_HOME. + // Trees discoverSessions actually walks. Honors CODEX_HOME; when the + // production singleton scans nest + billed home, both appear here. async probeRoots(): Promise { - return [ - { path: join(dir, 'sessions'), label: 'sessions' }, - { path: join(dir, 'archived_sessions'), label: 'archived' }, - ] + if (duplicateHome) return [] + if (scanBoth) return [...rootsFor(primaryDir), ...rootsFor(dir)] + return rootsFor(dir) }, async discoverSessions(): Promise { @@ -1378,12 +1393,12 @@ export function createCodexProvider( // distinct nest. isNestedLauncherCodexHome is false in that case. if (duplicateHome) return [] const sources = await discoverSessionsInDir(dir) - if (!filterOverlap) return sources - const primaryIds = listRolloutSessionIds(primaryDir) - return sources.filter(source => { - const id = rolloutFileSessionId(source.path) - return !(id && primaryIds.has(id)) - }) + if (scanBoth) { + const billed = await discoverSessionsInDir(primaryDir) + return [...billed, ...dropOverlappingNestSources(sources, primaryDir)] + } + if (!nestHome) return sources + return dropOverlappingNestSources(sources, primaryDir) }, createSessionParser(source: SessionSource, seenKeys: Set): SessionParser { diff --git a/tests/launcher-homes.test.ts b/tests/launcher-homes.test.ts index 69ebe3fd..465f670d 100644 --- a/tests/launcher-homes.test.ts +++ b/tests/launcher-homes.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { mkdtemp, mkdir, writeFile, rm, symlink } from 'fs/promises' import { join } from 'path' import { tmpdir } from 'os' @@ -114,6 +114,8 @@ describe('Codex discover overlap-only nest filter', () => { expect(names(await provider.discoverSessions())).toEqual(['rollout-buzz.jsonl']) const roots = await provider.probeRoots!() expect(roots.map(r => r.path)).toEqual([ + join(primary, 'sessions'), + join(primary, 'archived_sessions'), join(nested, 'sessions'), join(nested, 'archived_sessions'), ]) @@ -125,19 +127,51 @@ describe('Codex discover overlap-only nest filter', () => { } }) - it('default factory with CODEX_HOME=nest does not drop unique nest rollouts when ~/.codex has its own', async () => { + it('no-arg factory discovers billed + unique nest and drops overlapping nest ids', async () => { const home = root const primary = join(home, '.codex') const nested = join(home, '.buzz', '.codex') - await writeCodexSession(primary, 'rollout-primary.jsonl') - await writeCodexSession(nested, 'rollout-buzz.jsonl') + await writeCodexSession(primary, 'rollout-billed.jsonl', 'sess-shared') + await writeCodexSession(primary, 'rollout-billed-only.jsonl', 'sess-billed') + await writeCodexSession(nested, 'rollout-renamed.jsonl', 'sess-shared') + await writeCodexSession(nested, 'rollout-buzz-only.jsonl', 'sess-buzz') const prevHome = process.env.HOME const prevCodex = process.env.CODEX_HOME process.env.HOME = home process.env.CODEX_HOME = nested try { const provider = createCodexProvider() - expect(names(await provider.discoverSessions())).toEqual(['rollout-buzz.jsonl']) + expect(names(await provider.discoverSessions()).sort()).toEqual([ + 'rollout-billed-only.jsonl', + 'rollout-billed.jsonl', + 'rollout-buzz-only.jsonl', + ]) + } finally { + if (prevHome === undefined) delete process.env.HOME + else process.env.HOME = prevHome + if (prevCodex === undefined) delete process.env.CODEX_HOME + else process.env.CODEX_HOME = prevCodex + } + }) + + it('exported codex singleton dedups nest ids against billed home', async () => { + const home = root + const primary = join(home, '.codex') + const nested = join(home, '.buzz', '.codex') + await writeCodexSession(primary, 'rollout-billed.jsonl', 'sess-shared') + await writeCodexSession(nested, 'rollout-renamed.jsonl', 'sess-shared') + await writeCodexSession(nested, 'rollout-buzz-only.jsonl', 'sess-buzz') + const prevHome = process.env.HOME + const prevCodex = process.env.CODEX_HOME + process.env.HOME = home + process.env.CODEX_HOME = nested + vi.resetModules() + try { + const { codex } = await import('../src/providers/codex.js') + expect(names(await codex.discoverSessions()).sort()).toEqual([ + 'rollout-billed.jsonl', + 'rollout-buzz-only.jsonl', + ]) } finally { if (prevHome === undefined) delete process.env.HOME else process.env.HOME = prevHome