Skip to content

Commit 5f06b37

Browse files
ericallamclaude
andcommitted
fix(sdk): persist injected context alongside a compaction
saveTranscript made compaction and injections mutually exclusive in the persisted runtime state, but restoreModelLane treats them independently: injections anchored past a valid compaction apply on top, and if the compaction fingerprint fails to match at boot the whole compacted lane is discarded. In that case a same-turn injection that only lived in the dropped `injections` field was lost. Emit both fields; restore already filters injections that a valid compaction covers, so there is no double-apply. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VG39FXXkFFU24U5EtJMwPi
1 parent d967cc1 commit 5f06b37

2 files changed

Lines changed: 51 additions & 3 deletions

File tree

packages/trigger-sdk/src/v3/ai.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7171,9 +7171,8 @@ function chatAgent<
71717171
fingerprint: prefixFingerprint(shadow, throughId),
71727172
},
71737173
}
7174-
: laneInjections.length > 0
7175-
? { injections: laneInjections }
7176-
: {}),
7174+
: {}),
7175+
...(laneInjections.length > 0 ? { injections: laneInjections } : {}),
71777176
...(queued.length > 0 ? { queued: [...queued] } : {}),
71787177
}
71797178
: null;

packages/trigger-sdk/test/transcript-changesets.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,4 +547,53 @@ describe("restoreModelLane", () => {
547547
expect(restored.compacted).toBe(false);
548548
expect(restored.messages.map((m) => m.content)).toEqual(["u-1", "a-1", "u-2"]);
549549
});
550+
551+
it("applies persisted injections when the compacted lane is invalidated", async () => {
552+
const state = {
553+
v: 1 as const,
554+
compaction: {
555+
modelMessages: [{ role: "assistant" as const, content: "STALE SUMMARY" }],
556+
throughId: "gone",
557+
fingerprint: "does-not-match",
558+
},
559+
injections: [
560+
{
561+
afterId: "",
562+
messages: [{ role: "user" as const, content: "[note] survives compaction loss" }],
563+
},
564+
],
565+
};
566+
const restored = await restoreModelLane([userMessage("kept", "k-1")], state, async (messages) =>
567+
messages.map((m) => ({ role: m.role, content: m.id }) as never)
568+
);
569+
expect(restored.compacted).toBe(false);
570+
const dump = JSON.stringify(restored.messages);
571+
expect(dump).toContain("[note] survives compaction loss");
572+
expect(dump).not.toContain("STALE SUMMARY");
573+
});
574+
575+
it("does not re-apply an injection a valid compaction already covers", async () => {
576+
const shadow = createTranscriptShadow([userMessage("one", "u-1")]);
577+
const state = {
578+
v: 1 as const,
579+
compaction: {
580+
modelMessages: [{ role: "assistant" as const, content: "SUMMARY WITH THE NOTE BAKED IN" }],
581+
throughId: "u-1",
582+
fingerprint: prefixFingerprint(shadow, "u-1"),
583+
},
584+
injections: [
585+
{
586+
afterId: "u-1",
587+
messages: [{ role: "user" as const, content: "[note] already in the summary" }],
588+
},
589+
],
590+
};
591+
const restored = await restoreModelLane([userMessage("one", "u-1")], state, async (messages) =>
592+
messages.map((m) => ({ role: m.role, content: m.id }) as never)
593+
);
594+
expect(restored.compacted).toBe(true);
595+
const dump = JSON.stringify(restored.messages);
596+
expect(dump).toContain("SUMMARY WITH THE NOTE BAKED IN");
597+
expect(dump).not.toContain("[note] already in the summary");
598+
});
550599
});

0 commit comments

Comments
 (0)