Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
* whenever that cannot be established:
*
* - no cursor first write-back of the session, or the cache was evicted.
* - fingerprint drift the transcript was REWRITTEN rather than extended (Claude Code compaction,
* a truncated/rotated rollout), so what we already sent is no longer a prefix
* of what we hold and an append would splice two different conversations.
* - fingerprint drift the transcript was REWRITTEN rather than extended (an edited or redacted
* turn, a truncated/rotated rollout), so what we already sent is no longer a
* prefix of what we hold and an append would splice two conversations.
* NOT Claude Code compaction, which this used to cite: that appends a
* summary record and leaves every earlier record in place (#3379), so the
* prefix stays intact and the append path keeps working.
* - dirty the previous retain failed, or its outcome is unknown (the client aborts at
* 15s while the server may well have committed it). Appending on top of an
* unknown state is the one way to DUPLICATE turns inside the document, so we
Expand Down
30 changes: 30 additions & 0 deletions hindsight-integrations/coding-agents/src/core/transcript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,36 @@ describe("readClaudeTranscript", () => {
expect(result[0].content).not.toContain("hindsight_memories");
});

it("drops the compaction summary — Claude Code's recap, not the user's words", () => {
// Written when the context window fills, as a plain type:"user" record with no isMeta flag.
// Measured at 29 records / 474,016 chars across local transcripts, averaging 16KB each, every
// one retained as if the user had typed it — and each one summarising turns already retained,
// since compaction APPENDS to the transcript rather than rewriting it (#3379).
writeFileSync(
file,
[
JSON.stringify({
type: "user",
isCompactSummary: true,
message: {
role: "user",
content:
"This session is being continued from a previous conversation that ran out of " +
"context. The summary below covers <analysis>…</analysis>",
},
}),
JSON.stringify({
type: "user",
message: { role: "user", content: "now add the retry backoff" },
}),
].join("\n")
);

const result = readClaudeTranscript(file);
expect(result).toHaveLength(1);
expect(result[0].content).toBe("now add the retry backoff");
});

it("drops a <task-notification>: the harness's background-task plumbing, not the user's words", () => {
// Claude Code delivers these as an ordinary type:"user" message with a string body and no
// isMeta flag, so nothing else filters them and extraction saw task ids and status lines as
Expand Down
10 changes: 9 additions & 1 deletion hindsight-integrations/coding-agents/src/core/transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* no arguments, no outputs. That keeps WHICH files/commands the session touched without burying the
* decisions in mechanical noise; `tool_result` blocks are dropped entirely.
*
* Drops non-message lines (`last-prompt`, `mode`, `summary`, …), `isMeta` lines, `isSidechain`
* Drops non-message lines (`last-prompt`, `mode`, `summary`, …), `isMeta` lines, compaction
* summaries (`isCompactSummary`), `isSidechain`
* (subagent/Task) lines, `thinking` blocks, and turns that render to nothing. Injected recall
* context (`<hindsight_memories>` / `<hindsight_bank>` / `<relevant_memories>`) is stripped so the
* write-back can't feed recalled memory back into the bank (a retain→recall feedback loop). A
Expand All @@ -29,6 +30,7 @@ interface TranscriptLine {
type?: string;
isMeta?: boolean;
isSidechain?: boolean;
isCompactSummary?: boolean;
timestamp?: string;
message?: {
content?: string | ContentBlock[];
Expand Down Expand Up @@ -95,6 +97,12 @@ export function readClaudeTranscript(path: string): TransportTurn[] {
if (line.type !== "user" && line.type !== "assistant") continue;
if (line.isMeta === true) continue;
if (line.isSidechain === true) continue;
// Claude Code's own recap of the conversation so far, written when the context window fills.
// It arrives as a plain type:"user" record with NO isMeta flag, so nothing else filters it and
// a ~16KB machine-written summary was retained as something the user said. It is also a
// summary of turns ALREADY retained — compaction appends to the transcript rather than
// rewriting it — so keeping it extracts the same decisions twice (#3379).
if (line.isCompactSummary === true) continue;
if (typeof line.message !== "object" || line.message === null) continue;

// `type` is validated as "user" | "assistant" above; drive role from it (not the redundant
Expand Down