From 4d8360c72f246780dea3cf5ecccba94b6af86807 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 28 Feb 2026 13:30:49 +0900 Subject: [PATCH] fix(context-injector): use deterministic synthetic part ID for cache stability --- .../context-injector/injector.test.ts | 45 +++++++++++++++++++ src/features/context-injector/injector.ts | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/features/context-injector/injector.test.ts b/src/features/context-injector/injector.test.ts index 6fe9e7e8..09de376f 100644 --- a/src/features/context-injector/injector.test.ts +++ b/src/features/context-injector/injector.test.ts @@ -64,6 +64,51 @@ describe("createContextInjectorMessagesTransformHook", () => { expect(output.messages[2].parts[1].text).toBe("Second message") }) + it("uses deterministic synthetic part ID across repeated transforms", async () => { + // given + const hook = createContextInjectorMessagesTransformHook(collector) + const sessionID = "ses_transform_deterministic" + const baseMessage = createMockMessage("user", "Stable message", sessionID) + + collector.register(sessionID, { + id: "ctx-1", + source: "keyword-detector", + content: "Injected context", + }) + const firstOutput = { + messages: [structuredClone(baseMessage)], + } + + // when + await hook["experimental.chat.messages.transform"]!({}, firstOutput) + + // then + const firstSyntheticPart = firstOutput.messages[0].parts[0] + expect( + "synthetic" in firstSyntheticPart && firstSyntheticPart.synthetic === true + ).toBe(true) + + // given + collector.register(sessionID, { + id: "ctx-2", + source: "keyword-detector", + content: "Injected context", + }) + const secondOutput = { + messages: [structuredClone(baseMessage)], + } + + // when + await hook["experimental.chat.messages.transform"]!({}, secondOutput) + + // then + const secondSyntheticPart = secondOutput.messages[0].parts[0] + expect( + "synthetic" in secondSyntheticPart && secondSyntheticPart.synthetic === true + ).toBe(true) + expect(secondSyntheticPart.id).toBe(firstSyntheticPart.id) + }) + it("does nothing when no pending context", async () => { // given const hook = createContextInjectorMessagesTransformHook(collector) diff --git a/src/features/context-injector/injector.ts b/src/features/context-injector/injector.ts index ca676a11..8a52de91 100644 --- a/src/features/context-injector/injector.ts +++ b/src/features/context-injector/injector.ts @@ -148,7 +148,7 @@ export function createContextInjectorMessagesTransformHook( // synthetic part pattern (minimal fields) const syntheticPart = { - id: `synthetic_hook_${Date.now()}`, + id: `synthetic_hook_${sessionID}`, messageID: lastUserMessage.info.id, sessionID: (lastUserMessage.info as { sessionID?: string }).sessionID ?? "", type: "text" as const,