fix(context-injector): prepend to user message instead of separate synthetic message

- Change from creating separate synthetic user message to prepending context
  directly to last user message's text part
- Separate synthetic messages were ignored by model (treated as previous turn)
- Prepending to clone ensures: UI shows original, model receives prepended content
- Update tests to reflect new behavior
This commit is contained in:
YeonGyu-Kim
2026-01-06 18:03:31 +09:00
parent 24e983eac7
commit 04fc903d38
2 changed files with 18 additions and 41 deletions

View File

@@ -207,7 +207,7 @@ describe("createContextInjectorMessagesTransformHook", () => {
],
})
it("inserts synthetic message before last user message", async () => {
it("prepends context to last user message", async () => {
// #given
const hook = createContextInjectorMessagesTransformHook(collector)
const sessionID = "ses_transform1"
@@ -228,10 +228,8 @@ describe("createContextInjectorMessagesTransformHook", () => {
await hook["experimental.chat.messages.transform"]!({}, output)
// #then
expect(output.messages.length).toBe(4)
expect(output.messages[2].parts[0].text).toBe("Ultrawork context")
expect(output.messages[2].parts[0].synthetic).toBe(true)
expect(output.messages[3].parts[0].text).toBe("Second message")
expect(output.messages.length).toBe(3)
expect(output.messages[2].parts[0].text).toBe("Ultrawork context\n\n---\n\nSecond message")
})
it("does nothing when no pending context", async () => {

View File

@@ -124,47 +124,26 @@ export function createContextInjectorMessagesTransformHook(
return
}
const refInfo = lastUserMessage.info as unknown as {
sessionID?: string
agent?: string
model?: { providerID?: string; modelID?: string }
path?: { cwd?: string; root?: string }
const textPartIndex = lastUserMessage.parts.findIndex(
(p) => p.type === "text" && (p as { text?: string }).text
)
if (textPartIndex === -1) {
log("[context-injector] No text part found in last user message, skipping injection", {
sessionID,
partsCount: lastUserMessage.parts.length,
})
return
}
const syntheticMessageId = `synthetic_ctx_${Date.now()}`
const syntheticPartId = `synthetic_ctx_part_${Date.now()}`
const now = Date.now()
const textPart = lastUserMessage.parts[textPartIndex] as { text?: string }
const originalText = textPart.text ?? ""
textPart.text = `${pending.merged}\n\n---\n\n${originalText}`
const syntheticMessage: MessageWithParts = {
info: {
id: syntheticMessageId,
sessionID: sessionID,
role: "user",
time: { created: now },
agent: refInfo.agent ?? "Sisyphus",
model: refInfo.model ?? { providerID: "unknown", modelID: "unknown" },
path: refInfo.path ?? { cwd: "/", root: "/" },
} as unknown as Message,
parts: [
{
id: syntheticPartId,
sessionID: sessionID,
messageID: syntheticMessageId,
type: "text",
text: pending.merged,
synthetic: true,
time: { start: now, end: now },
} as Part,
],
}
messages.splice(lastUserMessageIndex, 0, syntheticMessage)
log("[context-injector] Injected synthetic message from collector", {
log("[context-injector] Prepended context to last user message", {
sessionID,
insertIndex: lastUserMessageIndex,
contextLength: pending.merged.length,
newMessageCount: messages.length,
originalTextLength: originalText.length,
})
},
}