diff --git a/packages/opencode/src/kilocode/session/prompt.ts b/packages/opencode/src/kilocode/session/prompt.ts index 76c80299ad..0a4c7398c8 100644 --- a/packages/opencode/src/kilocode/session/prompt.ts +++ b/packages/opencode/src/kilocode/session/prompt.ts @@ -91,6 +91,7 @@ export namespace KiloSessionPrompt { messageID: input.msgs[idx].info.id, type: "text", text: input.cache.block, + synthetic: true, } satisfies MessageV2.TextPart, ], } diff --git a/packages/opencode/test/kilocode/session-compaction-safety.test.ts b/packages/opencode/test/kilocode/session-compaction-safety.test.ts index ea76bb8800..8254e748a6 100644 --- a/packages/opencode/test/kilocode/session-compaction-safety.test.ts +++ b/packages/opencode/test/kilocode/session-compaction-safety.test.ts @@ -416,6 +416,23 @@ describe("KiloSessionPrompt.stripHistoricalMedia", () => { // synthetic user preserved as-is expect(result[4]).toBe(msgs[4]) }) + + test("skips synthetic user turns with injected context when picking the cutoff", () => { + const currentImage = filePart("msg_current", "image/png", "current.png") + const msgs = [ + user("msg_hist", [textPart("msg_hist", "older"), filePart("msg_hist", "image/png", "old.png")]), + user("msg_current", [textPart("msg_current", "check this"), currentImage]), + user("msg_syn", [ + syntheticTextPart("msg_syn", "Summarize the task tool output above and continue with your task."), + syntheticTextPart("msg_syn", "\nCurrent time: now\n", "p_env"), + ]), + ] + const result = KiloSessionPrompt.stripHistoricalMedia(msgs) + expect(result[1].parts[1]).toBe(currentImage) + const hist = result[0].parts[1] + expect(hist.type).toBe("text") + expect((hist as MessageV2.TextPart).text).toBe("[Attached image/png: old.png]") + }) }) describe("KiloSessionPrompt.maybeStripHistoricalMedia", () => { diff --git a/packages/opencode/test/kilocode/session-prompt-compaction-safety.test.ts b/packages/opencode/test/kilocode/session-prompt-compaction-safety.test.ts index 19e5cc665c..d9b0dbec2c 100644 --- a/packages/opencode/test/kilocode/session-prompt-compaction-safety.test.ts +++ b/packages/opencode/test/kilocode/session-prompt-compaction-safety.test.ts @@ -180,6 +180,7 @@ const cfg = { temperature: false, tool_call: true, release_date: "2025-01-01", + modalities: { input: ["text" as const, "image" as const, "pdf" as const], output: ["text" as const] }, limit: { context: 100000, output: 10000 }, cost: { input: 0, output: 0 }, options: {}, @@ -209,7 +210,11 @@ function providerCfg(url: string) { } } -const user = Effect.fn("prompt-safety.user")(function* (sessionID: SessionID, text: string) { +const user = Effect.fn("prompt-safety.user")(function* ( + sessionID: SessionID, + text: string, + input?: { synthetic?: boolean; editorContext?: MessageV2.User["editorContext"] }, +) { const sessions = yield* Session.Service const msg = yield* sessions.updateMessage({ id: MessageID.ascending(), @@ -219,6 +224,7 @@ const user = Effect.fn("prompt-safety.user")(function* (sessionID: SessionID, te model: ref, time: { created: Date.now() }, tools: {}, + editorContext: input?.editorContext, } satisfies MessageV2.User) yield* sessions.updatePart({ id: PartID.ascending(), @@ -226,6 +232,7 @@ const user = Effect.fn("prompt-safety.user")(function* (sessionID: SessionID, te sessionID, type: "text", text, + synthetic: input?.synthetic, } satisfies MessageV2.TextPart) return msg }) @@ -343,4 +350,41 @@ describe("SessionPrompt compaction safety", () => { { git: true, config: providerCfg }, ), ) + + it.live("preserves current media before synthetic handoff with editor context", () => + provideTmpdirServer( + Effect.fnUntraced(function* ({ llm }) { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Prompt handoff safety", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + + const status = yield* user(chat.id, "status?") + yield* assistant(chat.id, status.id, { text: "summary body", summary: true }) + const hist = yield* user(chat.id, "older image") + yield* file(chat.id, hist.id, { mime: "image/png", name: "old.png", body: "OLDIMAGE" }) + const current = yield* user(chat.id, "check this image") + yield* file(chat.id, current.id, { mime: "image/png", name: "current.png", body: "CURRENTIMAGE" }) + yield* assistant(chat.id, current.id, { text: "handoff", summary: false }) + yield* user(chat.id, "Summarize the task tool output above and continue with your task.", { + synthetic: true, + editorContext: { activeFile: "src/app.ts" }, + }) + yield* llm.text("final answer") + + yield* prompt.loop({ sessionID: chat.id }) + + const inputs = yield* llm.inputs + const body = JSON.stringify(inputs.at(-1)?.messages) + expect(body).toContain("[Attached image/png: old.png]") + expect(body).toContain("CURRENTIMAGE") + expect(body).toContain("src/app.ts") + expect(body).not.toContain("OLDIMAGE") + expect(body).not.toContain("[Attached image/png: current.png]") + }), + { git: true, config: providerCfg }, + ), + ) })