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
14 changes: 11 additions & 3 deletions hindsight-integrations/coding-agents/src/core/transcript-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ export const TOOL_TEXT_CAP = 2000;

/** Injected context — stripped from retained text so a write-back never re-ingests its own
* injected memory (a retain→reflect feedback loop). Covers every block the hooks inject, PLUS
* the host's own hook-transport wrappers (codex surfaces hook stdout/errors as `<hook_prompt>`
* user messages — transport noise, not the user's work). Tag-structural, not content-guessing. */
* the host's own hook-transport wrappers, which arrive as ordinary USER messages and would
* otherwise be extracted as things the user said (#3023):
* <hook_prompt> codex surfaces hook stdout/errors this way
* <task-notification> Claude Code reports a background task's outcome — task id, tool-use id,
* status; measured at 39 of these across 400 local transcripts, each one
* the entire message
* <system-reminder> same class; today it only rides inside `tool_result` blocks, which the
* Claude reader already drops, so this is insurance against it moving
* Tag-structural, not content-guessing. The block is removed and any surrounding text KEPT — a
* message that is nothing but a wrapper then renders empty and is dropped as a no-content turn. */
const MEMORY_TAG_RE =
/<(hook_prompt|hindsight_memory|hindsight_memories|hindsight_bank|relevant_memories|user_feedback|hindsight_knowledge|hindsight_knowledge_refresh)\b[\s\S]*?<\/\1>/g;
/<(hook_prompt|task-notification|system-reminder|hindsight_memory|hindsight_memories|hindsight_bank|relevant_memories|user_feedback|hindsight_knowledge|hindsight_knowledge_refresh)\b[\s\S]*?<\/\1>/g;

export function stripInjectedMemory(s: string): string {
return s.replace(MEMORY_TAG_RE, "");
Expand Down
62 changes: 62 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,68 @@ describe("readClaudeTranscript", () => {
expect(result[0].content).not.toContain("hindsight_memories");
});

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
// things the user said. Measured at 39 across 400 local transcripts, each the whole message
// (#3023 — which named skill bodies and <system-reminder>; both are already handled, this is
// the case that actually survived).
writeFileSync(
file,
JSON.stringify({
type: "user",
message: {
role: "user",
content:
"<task-notification>\n<task-id>b46ca19nr</task-id>\n" +
"<tool-use-id>toolu_0127NeaVZbdiAsbacNvasB78</tool-use-id>\n" +
"<status>stopped</status>\n<summary>No completion record</summary>\n</task-notification>",
},
})
);

expect(readClaudeTranscript(file)).toEqual([]); // renders empty -> no turn at all
});

it("keeps what the user wrote around a harness wrapper", () => {
// The stripper removes the BLOCK, never the message: replacing the whole turn with the tag's
// contents is what made the old plugin's strip_channel_envelope discard real user text (#3124).
writeFileSync(
file,
JSON.stringify({
type: "user",
message: {
role: "user",
content: "before <task-notification><status>done</status></task-notification> after",
},
})
);

const result = readClaudeTranscript(file);
expect(result).toHaveLength(1);
expect(result[0].content).toBe("before after");
expect(result[0].content).not.toContain("status");
});

it("drops a <system-reminder> block if the harness ever delivers one as user text", () => {
// Today these ride inside tool_result blocks, which this reader already drops entirely; the
// rule is tag-structural so it holds if that placement changes.
writeFileSync(
file,
JSON.stringify({
type: "user",
message: {
role: "user",
content: "<system-reminder>plan mode is active</system-reminder>\nship the fix",
},
})
);

const result = readClaudeTranscript(file);
expect(result).toHaveLength(1);
expect(result[0].content).toBe("ship the fix");
});

it("strips the reflect hook's <hindsight_memory> injection block (buildSystemInjection output)", () => {
// The exact block the UserPromptSubmit hook injects — wrapper tags, preamble, attribution
// text and the surfaced memory itself must ALL be gone from retained text, or the session
Expand Down