From cac656885415475f7661aab4e1b45df3159f803c Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Fri, 10 Jul 2026 10:10:58 +0100 Subject: [PATCH 1/2] fix(agent): seed hydrated transcripts with estimated context usage Hydrated session transcripts write usage: 0 on every message, so a resumed SDK session believes its context is empty and cannot trigger auto-compaction until the first API response comes back (or fails). Seed the last assistant message with the estimated token count of the rebuilt conversation so context accounting starts near reality. Generated-By: PostHog Code Task-Id: c6065620-5694-4478-a20a-fec4ccde8f1a --- .../claude/session/jsonl-hydration.test.ts | 23 +++++++++++++++++++ .../claude/session/jsonl-hydration.ts | 18 +++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/packages/agent/src/adapters/claude/session/jsonl-hydration.test.ts b/packages/agent/src/adapters/claude/session/jsonl-hydration.test.ts index bd7aada3bf..bb6e76ad36 100644 --- a/packages/agent/src/adapters/claude/session/jsonl-hydration.test.ts +++ b/packages/agent/src/adapters/claude/session/jsonl-hydration.test.ts @@ -496,6 +496,29 @@ describe("conversationTurnsToJsonlEntries", () => { expect(conv[1].parentUuid).toBe(conv[0].uuid); }); + it("seeds only the last assistant message with the estimated context size", () => { + const lines = conversationTurnsToJsonlEntries( + [ + { role: "user", content: [{ type: "text", text: "q".repeat(400) }] }, + { + role: "assistant", + content: [{ type: "text", text: "a".repeat(400) }], + }, + { role: "user", content: [{ type: "text", text: "more" }] }, + { role: "assistant", content: [{ type: "text", text: "final" }] }, + ], + config, + ); + + const assistants = parseConversationEntries(lines).filter( + (entry) => entry.type === "assistant", + ); + expect(assistants.at(-1)?.message.usage.input_tokens).toBeGreaterThan(0); + for (const entry of assistants.slice(0, -1)) { + expect(entry.message.usage.input_tokens).toBe(0); + } + }); + it("emits one line per assistant block with shared message id", () => { const lines = conversationTurnsToJsonlEntries( [ diff --git a/packages/agent/src/adapters/claude/session/jsonl-hydration.ts b/packages/agent/src/adapters/claude/session/jsonl-hydration.ts index 9bc9c268b1..5f1a1a95bb 100644 --- a/packages/agent/src/adapters/claude/session/jsonl-hydration.ts +++ b/packages/agent/src/adapters/claude/session/jsonl-hydration.ts @@ -584,6 +584,24 @@ export function conversationTurnsToJsonlEntries( } } + // Seed the last assistant message with the estimated context size so the + // SDK's auto-compact accounting isn't blind until the first API response. + const estimatedTokens = turns.reduce( + (sum, turn) => sum + estimateTurnTokens(turn), + 0, + ); + for (let i = lines.length - 1; i >= 0; i--) { + const parsed = JSON.parse(lines[i]) as { + type?: string; + message?: { usage?: { input_tokens?: number } }; + }; + if (parsed.type === "assistant" && parsed.message?.usage) { + parsed.message.usage.input_tokens = estimatedTokens; + lines[i] = JSON.stringify(parsed); + break; + } + } + return lines; } From 03f7aa9cb5e0c0df32ddcb97a14dc39371ce160e Mon Sep 17 00:00:00 2001 From: Alessandro Pogliaghi Date: Fri, 10 Jul 2026 10:41:26 +0100 Subject: [PATCH 2/2] refactor(agent): set seeded usage inline instead of re-parsing emitted lines Generated-By: PostHog Code Task-Id: c6065620-5694-4478-a20a-fec4ccde8f1a --- .../claude/session/jsonl-hydration.ts | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/agent/src/adapters/claude/session/jsonl-hydration.ts b/packages/agent/src/adapters/claude/session/jsonl-hydration.ts index 5f1a1a95bb..f865791f8c 100644 --- a/packages/agent/src/adapters/claude/session/jsonl-hydration.ts +++ b/packages/agent/src/adapters/claude/session/jsonl-hydration.ts @@ -421,6 +421,13 @@ export function conversationTurnsToJsonlEntries( const permissionMode = config.permissionMode ?? "default"; const baseTime = Date.now() - turns.length * 3000; let turnIndex = 0; + // Seed the last assistant message with the estimated context size so the + // SDK's auto-compact accounting isn't blind until the first API response. + const estimatedTokens = turns.reduce( + (sum, turn) => sum + estimateTurnTokens(turn), + 0, + ); + const lastAssistantTurn = turns.findLast((turn) => turn.role === "assistant"); for (const turn of turns) { const timestamp = new Date(baseTime + turnIndex * 3000).toISOString(); @@ -530,7 +537,8 @@ export function conversationTurnsToJsonlEntries( stop_reason: isLast ? lastStopReason : null, stop_sequence: null, usage: { - input_tokens: 0, + input_tokens: + turn === lastAssistantTurn && isLast ? estimatedTokens : 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 0, @@ -584,24 +592,6 @@ export function conversationTurnsToJsonlEntries( } } - // Seed the last assistant message with the estimated context size so the - // SDK's auto-compact accounting isn't blind until the first API response. - const estimatedTokens = turns.reduce( - (sum, turn) => sum + estimateTurnTokens(turn), - 0, - ); - for (let i = lines.length - 1; i >= 0; i--) { - const parsed = JSON.parse(lines[i]) as { - type?: string; - message?: { usage?: { input_tokens?: number } }; - }; - if (parsed.type === "assistant" && parsed.message?.usage) { - parsed.message.usage.input_tokens = estimatedTokens; - lines[i] = JSON.stringify(parsed); - break; - } - } - return lines; }