From ed35681c3527ce59c5c6944c5e12b7f4a5ed7eed Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 27 Apr 2026 10:36:24 -0700 Subject: [PATCH 1/2] Add Codex compaction events --- CHANGELOG.md | 2 + packages/cli/CHANGELOG.md | 2 + packages/cli/src/ingest.test.ts | 108 ++++++++++++++++++++++++++ packages/cli/src/ingest.ts | 10 +++ packages/ledger/CHANGELOG.md | 4 + packages/ledger/src/cursors.ts | 3 +- packages/reader/CHANGELOG.md | 4 + packages/reader/src/codex.test.ts | 55 +++++++++++++ packages/reader/src/codex.ts | 74 +++++++++++++++++- packages/reader/src/index.ts | 1 + tests/fixtures/codex/compaction.jsonl | 11 +++ 11 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/codex/compaction.jsonl diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb550b..c121ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added +- **Codex compaction parity with Claude.** Codex passive ingest now detects `compacted` session-log records, persists them as ledger compaction events, and anchors them to the preceding Codex turn so compaction-loss analysis can cover Codex sessions instead of only Claude Code. + - **`burn plans` honors per-cycle fidelity** ([#108](https://github.com/AgentWorkforce/burn/issues/108)). `computePlanUsage` now annotates each cycle with a `fidelity: { confidence, summary }` block so renderers can flag low-confidence totals. The CLI list view gains a `confidence` column and footer note when any cycle contains turns missing per-turn token data; `--json` emits the same `FidelitySummary` shape. Spend totals continue to include all contributing turns — the annotation marks them as a lower bound rather than silently under-counting. ## [0.27.0] - 2026-04-26 diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 58e2d50..f71c82d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Codex ingest persists compaction events.** The Codex passive ingest path now appends parser-emitted compactions through the existing ledger compaction writer, so `burn waste --kind compaction` can see Codex context compactions with the same event shape Claude uses. + - **`burn plans` honors per-cycle fidelity** ([#108](https://github.com/AgentWorkforce/burn/issues/108)). The list view continues to render every plan even when the cycle slice contains `partial` / `aggregate-only` / `cost-only` turns (no fidelity-based filter — `plans`, like `limits`, is permissive), but now flags low-confidence cycles so a "looks under budget" plan isn't read as authoritative. The text table grows a `confidence` column when at least one plan has any contributing turn missing per-turn input/output token data, marked `low (partial token data)`, and a footer note names the affected plan + lower-bound caveat (e.g. `note: claude-pro: 3 of 412 turns this cycle lack per-turn token data — totals are a lower bound.`). Full-fidelity cycles render exactly as before — no extra column, no footer. `--json` gains a per-plan `usage.fidelity: { confidence, summary }` block carrying the same `FidelitySummary` shape the analyze package emits elsewhere, so machine consumers can render exact counts without re-walking the ledger. `cost-only` source contributions count toward `spentUsd` and mark the cycle low-confidence on the token-coverage axis. ### Changed diff --git a/packages/cli/src/ingest.test.ts b/packages/cli/src/ingest.test.ts index 779052d..bf4c130 100644 --- a/packages/cli/src/ingest.test.ts +++ b/packages/cli/src/ingest.test.ts @@ -434,6 +434,28 @@ describe('ingestCodexSessions execution graph passthrough (#87)', () => { seen.add(key); } }); + + it('persists codex compaction events, no duplicates on re-ingest', async () => { + await writeCodexSession(tmpHome, 'rollout-compact-1', codexCompactionSession()); + await ingestCodexSessions(); + await ingestCodexSessions(); + + const ledger = await readFile(path.join(tmpRelay, 'ledger.jsonl'), 'utf8'); + const lines = ledger + .split('\n') + .map((l) => l.trim()) + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l) as { kind: string; record: Record }); + + const compactions = lines + .filter((l) => l.kind === 'compaction' && l.record['source'] === 'codex') + .map((l) => l.record); + + assert.equal(compactions.length, 1, 'exactly one codex compaction row appended'); + assert.equal(compactions[0]!['sessionId'], 'sess_codex_compact_ingest'); + assert.equal(compactions[0]!['precedingMessageId'], 'turn_compact_ingest_1'); + assert.equal(compactions[0]!['tokensBeforeCompact'], 1000); + }); }); // ---------- helpers ---------- @@ -668,6 +690,92 @@ function codexCommittedSession(sessionId: string, turnId: string, cwd: string): return lines.map((line) => JSON.stringify(line)).join('\n') + '\n'; } +function codexCompactionSession(): string { + const lines = [ + { + timestamp: '2026-04-20T03:00:00.000Z', + type: 'session_meta', + payload: { + id: 'sess_codex_compact_ingest', + cwd: '/tmp/project', + timestamp: '2026-04-20T03:00:00.000Z', + }, + }, + { + timestamp: '2026-04-20T03:00:00.100Z', + type: 'turn_context', + payload: { turn_id: 'turn_compact_ingest_1', cwd: '/tmp/project', model: 'gpt-5.4' }, + }, + { + timestamp: '2026-04-20T03:00:00.200Z', + type: 'event_msg', + payload: { type: 'task_started', turn_id: 'turn_compact_ingest_1' }, + }, + { + timestamp: '2026-04-20T03:00:01.000Z', + type: 'event_msg', + payload: { + type: 'token_count', + info: { + total_token_usage: { + input_tokens: 3000, + cached_input_tokens: 1000, + output_tokens: 200, + reasoning_output_tokens: 50, + }, + }, + }, + }, + { + timestamp: '2026-04-20T03:00:02.000Z', + type: 'event_msg', + payload: { type: 'task_complete', turn_id: 'turn_compact_ingest_1' }, + }, + { + timestamp: '2026-04-20T03:00:03.000Z', + type: 'compacted', + payload: { + message: '', + replacement_history: [ + { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'start' }] }, + { type: 'compaction', encrypted_content: 'opaque' }, + ], + }, + }, + { + timestamp: '2026-04-20T03:00:04.000Z', + type: 'turn_context', + payload: { turn_id: 'turn_compact_ingest_2', cwd: '/tmp/project', model: 'gpt-5.4' }, + }, + { + timestamp: '2026-04-20T03:00:04.100Z', + type: 'event_msg', + payload: { type: 'task_started', turn_id: 'turn_compact_ingest_2' }, + }, + { + timestamp: '2026-04-20T03:00:05.000Z', + type: 'event_msg', + payload: { + type: 'token_count', + info: { + total_token_usage: { + input_tokens: 6500, + cached_input_tokens: 1500, + output_tokens: 450, + reasoning_output_tokens: 90, + }, + }, + }, + }, + { + timestamp: '2026-04-20T03:00:06.000Z', + type: 'event_msg', + payload: { type: 'task_complete', turn_id: 'turn_compact_ingest_2' }, + }, + ]; + return lines.map((line) => JSON.stringify(line)).join('\n') + '\n'; +} + // A codex session that spawns one subagent (call_spawn_eg → agent_eg_xyz), // receives the spawn function_call_output, and commits the turn — exercising // the execution-graph passthrough path in `ingestCodexInto`. diff --git a/packages/cli/src/ingest.ts b/packages/cli/src/ingest.ts index fae776c..2174671 100644 --- a/packages/cli/src/ingest.ts +++ b/packages/cli/src/ingest.ts @@ -358,6 +358,9 @@ async function ingestCodexInto( rootSessionEmitted: priorCodex.rootSessionEmitted === true, nextEventIndex: priorCodex.nextEventIndex ?? 0, toolResultCounters: { ...(priorCodex.toolResultCounters ?? {}) }, + ...(priorCodex.lastCompletedTurn !== undefined + ? { lastCompletedTurn: priorCodex.lastCompletedTurn } + : {}), }; if (!rotated && startOffset >= st.size) { @@ -374,6 +377,7 @@ async function ingestCodexInto( const { turns, content, + events, userTurns, relationships, toolResultEvents, @@ -407,6 +411,9 @@ async function ingestCodexInto( if (content.length > 0) { await appendContent(content); } + if (events.length > 0) { + await appendCompactions(events); + } if (relationships.length > 0) { await appendRelationships(relationships); } @@ -432,6 +439,9 @@ async function ingestCodexInto( if (nextResume.toolResultCounters && Object.keys(nextResume.toolResultCounters).length > 0) { next.toolResultCounters = nextResume.toolResultCounters; } + if (nextResume.lastCompletedTurn !== undefined) { + next.lastCompletedTurn = nextResume.lastCompletedTurn; + } cursors[file] = next; } catch (err) { const msg = err instanceof Error ? err.message : String(err); diff --git a/packages/ledger/CHANGELOG.md b/packages/ledger/CHANGELOG.md index 510736a..e1dc784 100644 --- a/packages/ledger/CHANGELOG.md +++ b/packages/ledger/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Codex cursors remember the last committed turn.** `CodexCursor` now carries the reader's last completed Codex turn snapshot so incremental ingest can anchor a later compaction marker to the preceding turn without re-reading the whole session. + ## [0.31.0] - 2026-04-27 ### Added diff --git a/packages/ledger/src/cursors.ts b/packages/ledger/src/cursors.ts index fa58f50..bfe3aaf 100644 --- a/packages/ledger/src/cursors.ts +++ b/packages/ledger/src/cursors.ts @@ -1,7 +1,7 @@ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import * as path from 'node:path'; -import type { PersistedUserTurnSlot } from '@relayburn/reader'; +import type { CodexLastCompletedTurn, PersistedUserTurnSlot } from '@relayburn/reader'; import { withLock } from './lock.js'; import { cursorsPath } from './paths.js'; @@ -30,6 +30,7 @@ export interface CodexCursor { rootSessionEmitted?: boolean; nextEventIndex?: number; toolResultCounters?: Record; + lastCompletedTurn?: CodexLastCompletedTurn; } export interface OpencodeCursor { diff --git a/packages/reader/CHANGELOG.md b/packages/reader/CHANGELOG.md index 627495a..fedee40 100644 --- a/packages/reader/CHANGELOG.md +++ b/packages/reader/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Codex compaction markers emit `CompactionEvent`s.** `parseCodexSession` and `parseCodexSessionIncremental` now return an `events` array for Codex, matching Claude's compaction event surface. The reader detects Codex `type: "compacted"` records, anchors each event to the most recent committed Codex turn, and carries that turn through `CodexResumeState` so incremental parses preserve `precedingMessageId` and `tokensBeforeCompact` across cursor boundaries. + ## [0.26.0] - 2026-04-26 ### Added diff --git a/packages/reader/src/codex.test.ts b/packages/reader/src/codex.test.ts index 9323384..e2803a0 100644 --- a/packages/reader/src/codex.test.ts +++ b/packages/reader/src/codex.test.ts @@ -98,6 +98,23 @@ describe('parseCodexSession', () => { assert.equal(t2!.toolCalls[0]!.target, 'ls'); }); + it('emits a CompactionEvent anchored to the preceding committed turn', async () => { + const { turns, events } = await parseCodexSession(path.join(FIXTURES, 'compaction.jsonl')); + + assert.equal(turns.length, 2); + assert.equal(events.length, 1); + + const ev = events[0]!; + assert.equal(ev.source, 'codex'); + assert.equal(ev.sessionId, 'sess_codex_compact'); + assert.equal(ev.ts, '2026-04-20T03:00:03.000Z'); + assert.equal(ev.precedingMessageId, 'turn_compact_1'); + + const preceding = turns.find((t) => t.messageId === 'turn_compact_1')!; + assert.equal(ev.tokensBeforeCompact, preceding.usage.cacheRead); + assert.equal(ev.tokensBeforeCompact, 1000); + }); + it('produces stable argsHash for identical tool inputs', async () => { const a = await parseCodexSession(path.join(FIXTURES, 'with-tool-call.jsonl')); const b = await parseCodexSession(path.join(FIXTURES, 'with-tool-call.jsonl')); @@ -331,6 +348,44 @@ describe('parseCodexSessionIncremental', () => { } }); + it('anchors a resumed compaction event to the previous cursor turn', async () => { + const file = path.join(FIXTURES, 'compaction.jsonl'); + const raw = await readFile(file, 'utf8'); + const lines = raw.split('\n'); + // Offset right after the first task_complete line (line index 4, 0-based). + const cutoff = Buffer.byteLength(lines.slice(0, 5).join('\n') + '\n', 'utf8'); + + const { mkdtemp, writeFile, rm } = await import('node:fs/promises'); + const { tmpdir } = await import('node:os'); + const tmp = await mkdtemp(path.join(tmpdir(), 'burn-codex-compact-inc-')); + try { + const partialPath = path.join(tmp, 'partial.jsonl'); + await writeFile(partialPath, raw.slice(0, cutoff), 'utf8'); + const partial = await parseCodexSessionIncremental(partialPath); + assert.equal(partial.turns.length, 1); + assert.equal(partial.events.length, 0); + assert.deepEqual(partial.resume.lastCompletedTurn, { + messageId: 'turn_compact_1', + cacheRead: 1000, + }); + + const fullPath = path.join(tmp, 'full.jsonl'); + await writeFile(fullPath, raw, 'utf8'); + const resumed = await parseCodexSessionIncremental(fullPath, { + startOffset: partial.endOffset, + resume: partial.resume, + }); + + assert.equal(resumed.turns.length, 1); + assert.equal(resumed.turns[0]!.messageId, 'turn_compact_2'); + assert.equal(resumed.events.length, 1); + assert.equal(resumed.events[0]!.precedingMessageId, 'turn_compact_1'); + assert.equal(resumed.events[0]!.tokensBeforeCompact, 1000); + } finally { + await rm(tmp, { recursive: true, force: true }); + } + }); + it('does not advance endOffset if no task_complete seen in the tail', async () => { const file = path.join(FIXTURES, 'simple-turn.jsonl'); const raw = await readFile(file, 'utf8'); diff --git a/packages/reader/src/codex.ts b/packages/reader/src/codex.ts index 0eddff5..6e585af 100644 --- a/packages/reader/src/codex.ts +++ b/packages/reader/src/codex.ts @@ -5,6 +5,7 @@ import { EMPTY_COVERAGE, makeFidelity } from './fidelity.js'; import { resolveProject } from './git.js'; import { argsHash, contentHash } from './hash.js'; import type { + CompactionEvent, ContentRecord, ContentStoreMode, Coverage, @@ -53,6 +54,9 @@ export interface CodexResumeState { // always have exactly one output, but spawn_agent / wait fanouts can // produce multiple events for the same call_id. toolResultCounters?: Record; + // Most recent committed assistant turn. Kept in the resume/cursor so a + // later Codex compaction marker can be anchored to the turn it compacted. + lastCompletedTurn?: CodexLastCompletedTurn; } export interface PersistedUserTurnSlot { @@ -61,9 +65,15 @@ export interface PersistedUserTurnSlot { ts: string; } +export interface CodexLastCompletedTurn { + messageId: string; + cacheRead: number; +} + export interface ParseCodexIncrementalResult { turns: TurnRecord[]; content: ContentRecord[]; + events: CompactionEvent[]; userTurns: UserTurnRecord[]; // Execution graph (#42 / #87). `relationships` carries one `root` row per // newly-seen session id and one `subagent` row per `spawn_agent` call. @@ -267,6 +277,7 @@ interface UserTurnSlot { export interface ParseCodexResult { turns: TurnRecord[]; content: ContentRecord[]; + events: CompactionEvent[]; userTurns: UserTurnRecord[]; // Execution graph (#42 / #87). See `ParseCodexIncrementalResult` for the // shape; full parses always reflect the committed state at end-of-file. @@ -278,12 +289,12 @@ export async function parseCodexSession( filePath: string, options: ParseCodexOptions = {}, ): Promise { - const { turns, content, userTurns, relationships, toolResultEvents } = + const { turns, content, events, userTurns, relationships, toolResultEvents } = await parseCodexSessionIncremental(filePath, { ...options, startOffset: 0, }); - return { turns, content, userTurns, relationships, toolResultEvents }; + return { turns, content, events, userTurns, relationships, toolResultEvents }; } export async function parseCodexSessionIncremental( @@ -301,6 +312,7 @@ export async function parseCodexSessionIncremental( return { turns: [], content: [], + events: [], userTurns: [], relationships: [], toolResultEvents: [], @@ -375,6 +387,10 @@ export async function parseCodexSessionIncremental( offset: number; record: SessionRelationshipRecord; }> = []; + const pendingCompactions: Array<{ + offset: number; + event: CompactionEvent; + }> = []; // Commit snapshot — only advanced at task_complete boundaries. let committedEndOffset = startOffset; @@ -388,6 +404,8 @@ export async function parseCodexSessionIncremental( let committedRootSessionEmitted = rootSessionEmitted; let committedNextEventIndex = nextEventIndex; let committedToolResultCounters = new Map(toolResultCounters); + let lastCompletedTurn = cloneLastCompletedTurn(options.resume?.lastCompletedTurn); + let committedLastCompletedTurn = cloneLastCompletedTurn(lastCompletedTurn); let p = 0; while (p < buf.length) { @@ -450,6 +468,16 @@ export async function parseCodexSessionIncremental( const pl = payload as { type?: string }; + if (rec.type === 'compacted') { + if (sessionId) { + pendingCompactions.push({ + offset: lineEndOffset, + event: buildCodexCompactionEvent(sessionId, rec.timestamp ?? '', lastCompletedTurn), + }); + } + continue; + } + if (rec.type === 'event_msg') { if (pl.type === 'token_count') { const tc = payload as TokenCountPayload; @@ -554,7 +582,12 @@ export async function parseCodexSessionIncremental( // Stamp preceding so the next task_started knows this turn closed // off the slot and the record can be linked. userTurnSlot.precedingMessageId = openTurn.turnId; - finalized.push(finalizeTurn(openTurn, cumulative)); + const closed = finalizeTurn(openTurn, cumulative); + finalized.push(closed); + lastCompletedTurn = { + messageId: closed.turnId, + cacheRead: closed.usage.cacheRead, + }; openTurn = null; committedEndOffset = lineEndOffset; committedCumulative = { ...cumulative }; @@ -567,6 +600,7 @@ export async function parseCodexSessionIncremental( committedRootSessionEmitted = rootSessionEmitted; committedNextEventIndex = nextEventIndex; committedToolResultCounters = new Map(toolResultCounters); + committedLastCompletedTurn = cloneLastCompletedTurn(lastCompletedTurn); } continue; } @@ -911,8 +945,14 @@ export async function parseCodexSessionIncremental( toolResultCounters: Object.fromEntries(committedToolResultCounters), }; if (committedSessionCwd !== undefined) resume.sessionCwd = committedSessionCwd; + const lastCompleted = cloneLastCompletedTurn(committedLastCompletedTurn); + if (lastCompleted) resume.lastCompletedTurn = lastCompleted; const emittedUserTurns = userTurns.slice(0, committedUserTurnsCount); + const emittedEvents: CompactionEvent[] = []; + for (const e of pendingCompactions) { + if (e.offset <= committedEndOffset) emittedEvents.push(e.event); + } // Execution graph (#42 / #87). Emit only records whose source line ended // at-or-before the committed end offset — anything past it belongs to an @@ -929,6 +969,7 @@ export async function parseCodexSessionIncremental( return { turns, content, + events: emittedEvents, userTurns: emittedUserTurns, relationships: emittedRelationships, toolResultEvents: emittedToolResultEvents, @@ -961,6 +1002,24 @@ function collectReasoningText(rp: ReasoningPayload): string { return parts.join('\n'); } +function buildCodexCompactionEvent( + sessionId: string, + ts: string, + preceding: CodexLastCompletedTurn | undefined, +): CompactionEvent { + const event: CompactionEvent = { + v: 1, + source: 'codex', + sessionId, + ts, + }; + if (preceding) { + event.precedingMessageId = preceding.messageId; + event.tokensBeforeCompact = preceding.cacheRead; + } + return event; +} + function cloneResume(r: CodexResumeState | undefined): CodexResumeState { if (!r) { return { @@ -984,9 +1043,18 @@ function cloneResume(r: CodexResumeState | undefined): CodexResumeState { if (r.sessionCwd !== undefined) out.sessionCwd = r.sessionCwd; if (r.userTurnSlot) out.userTurnSlot = cloneSlot(r.userTurnSlot); else out.userTurnSlot = { blocks: [], ts: '' }; + const lastCompleted = cloneLastCompletedTurn(r.lastCompletedTurn); + if (lastCompleted) out.lastCompletedTurn = lastCompleted; return out; } +function cloneLastCompletedTurn( + turn: CodexLastCompletedTurn | undefined, +): CodexLastCompletedTurn | undefined { + if (!turn) return undefined; + return { ...turn }; +} + function cloneSlot(s: UserTurnSlot | PersistedUserTurnSlot): UserTurnSlot { const out: UserTurnSlot = { blocks: s.blocks.map((b) => ({ ...b })), diff --git a/packages/reader/src/index.ts b/packages/reader/src/index.ts index f5fd534..aa82818 100644 --- a/packages/reader/src/index.ts +++ b/packages/reader/src/index.ts @@ -47,6 +47,7 @@ export type { ParseCodexIncrementalOptions, ParseCodexIncrementalResult, CodexResumeState, + CodexLastCompletedTurn, PersistedUserTurnSlot, } from './codex.js'; export { parseOpencodeSession, parseOpencodeSessionIncremental } from './opencode.js'; diff --git a/tests/fixtures/codex/compaction.jsonl b/tests/fixtures/codex/compaction.jsonl new file mode 100644 index 0000000..2fba036 --- /dev/null +++ b/tests/fixtures/codex/compaction.jsonl @@ -0,0 +1,11 @@ +{"timestamp":"2026-04-20T03:00:00.000Z","type":"session_meta","payload":{"id":"sess_codex_compact","cwd":"/tmp/project","timestamp":"2026-04-20T03:00:00.000Z"}} +{"timestamp":"2026-04-20T03:00:00.100Z","type":"turn_context","payload":{"turn_id":"turn_compact_1","cwd":"/tmp/project","model":"gpt-5.4"}} +{"timestamp":"2026-04-20T03:00:00.200Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn_compact_1"}} +{"timestamp":"2026-04-20T03:00:01.000Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":3000,"cached_input_tokens":1000,"output_tokens":200,"reasoning_output_tokens":50,"total_tokens":3200}}}} +{"timestamp":"2026-04-20T03:00:02.000Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn_compact_1"}} +{"timestamp":"2026-04-20T03:00:03.000Z","type":"compacted","payload":{"message":"","replacement_history":[{"type":"message","role":"user","content":[{"type":"input_text","text":"start"}]},{"type":"compaction","encrypted_content":"opaque"}]}} +{"timestamp":"2026-04-20T03:00:03.100Z","type":"event_msg","payload":{"type":"context_compacted"}} +{"timestamp":"2026-04-20T03:00:04.000Z","type":"turn_context","payload":{"turn_id":"turn_compact_2","cwd":"/tmp/project","model":"gpt-5.4"}} +{"timestamp":"2026-04-20T03:00:04.100Z","type":"event_msg","payload":{"type":"task_started","turn_id":"turn_compact_2"}} +{"timestamp":"2026-04-20T03:00:05.000Z","type":"event_msg","payload":{"type":"token_count","info":{"total_token_usage":{"input_tokens":6500,"cached_input_tokens":1500,"output_tokens":450,"reasoning_output_tokens":90,"total_tokens":6950}}}} +{"timestamp":"2026-04-20T03:00:06.000Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn_compact_2"}} From a3ad66cc6997fb184e6f90a8456dd09b5b2c407c Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 27 Apr 2026 14:08:48 -0700 Subject: [PATCH 2/2] Move Codex compaction CHANGELOG entries to [Unreleased] The publish workflow promotes [Unreleased] at release time; entries written under an already-stamped section like [0.33.0] are post-release hand-edits that the next release won't pick up. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 6 ++++-- packages/cli/CHANGELOG.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f828cf9..d55b865 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Added + +- **Codex compaction parity with Claude.** Codex passive ingest now detects `compacted` session-log records, persists them as ledger compaction events, and anchors them to the preceding Codex turn so compaction-loss analysis can cover Codex sessions instead of only Claude Code. + ## [0.34.0] - 2026-04-27 ### Changed @@ -16,8 +20,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added -- **Codex compaction parity with Claude.** Codex passive ingest now detects `compacted` session-log records, persists them as ledger compaction events, and anchors them to the preceding Codex turn so compaction-loss analysis can cover Codex sessions instead of only Claude Code. - - **`burn plans` honors per-cycle fidelity** ([#108](https://github.com/AgentWorkforce/burn/issues/108)). `computePlanUsage` now annotates each cycle with a `fidelity: { confidence, summary }` block so renderers can flag low-confidence totals. The CLI list view gains a `confidence` column and footer note when any cycle contains turns missing per-turn token data; `--json` emits the same `FidelitySummary` shape. Spend totals continue to include all contributing turns — the annotation marks them as a lower bound rather than silently under-counting. ## [0.27.0] - 2026-04-26 diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 8e74c27..df4bf31 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Codex ingest persists compaction events.** The Codex passive ingest path now appends parser-emitted compactions through the existing ledger compaction writer, so `burn waste --kind compaction` can see Codex context compactions with the same event shape Claude uses. + ## [0.34.0] - 2026-04-27 ### Added @@ -21,8 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Codex ingest persists compaction events.** The Codex passive ingest path now appends parser-emitted compactions through the existing ledger compaction writer, so `burn waste --kind compaction` can see Codex context compactions with the same event shape Claude uses. - - **`burn plans` honors per-cycle fidelity** ([#108](https://github.com/AgentWorkforce/burn/issues/108)). The list view continues to render every plan even when the cycle slice contains `partial` / `aggregate-only` / `cost-only` turns (no fidelity-based filter — `plans`, like `limits`, is permissive), but now flags low-confidence cycles so a "looks under budget" plan isn't read as authoritative. The text table grows a `confidence` column when at least one plan has any contributing turn missing per-turn input/output token data, marked `low (partial token data)`, and a footer note names the affected plan + lower-bound caveat (e.g. `note: claude-pro: 3 of 412 turns this cycle lack per-turn token data — totals are a lower bound.`). Full-fidelity cycles render exactly as before — no extra column, no footer. `--json` gains a per-plan `usage.fidelity: { confidence, summary }` block carrying the same `FidelitySummary` shape the analyze package emits elsewhere, so machine consumers can render exact counts without re-walking the ledger. `cost-only` source contributions count toward `spentUsd` and mark the cycle low-confidence on the token-coverage axis. - **`burn waste` honors fidelity** ([#100](https://github.com/AgentWorkforce/burn/issues/100)). The attribution path (and the `--patterns retries|failures|reverts` detectors) now hard-filters the input slice against the coverage flags each detector requires — `attributeWaste` / `aggregateBy*` need `hasToolCalls` + `hasToolResultEvents`; `reverts` additionally needs `hasRawContent` (for `editPreHash` / `editPostHash`); `compaction` is unchanged because its sidecar is independent of `TurnRecord.fidelity`. When *all* turns fall below the prereq, `burn waste` exits non-zero with a message naming the missing prerequisite and the source kinds responsible (`burn waste: 142/142 turns lack tool-call/tool-result coverage required for waste attribution. Sources: codex (per-session-aggregate, missing tool-call records, tool-result events). No waste analysis was performed.`). When *some* turns survive, the text and JSON output gain an "analyzed N of M" coverage notice that names the gap per source. `--json` now carries a `fidelity` block (`{ analyzed, excluded, summary, refused }`) mirroring `summary --json`; `--patterns` JSON additionally exposes a `perDetector` array with each detector's `required` flags and `excludedBySource` breakdown. When `compaction` is in the selection it always runs — its sidecar has no per-turn fidelity requirement — so `--patterns retries,compaction` against an aggregate-only slice produces partial output rather than refusing.