From f6cf0bec940923dde3d58759ee3fe5ada873af24 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 9 Jun 2026 14:35:46 +0000 Subject: [PATCH] fix(session): cache messages across prompt loop to preserve prompt cache byte-identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filterCompactedEffect(sessionID) reloads all messages from the DB at the start of every prompt loop iteration. Between reloads, the DB returns rows with new object identity, breaking Anthropic's prompt cache which relies on byte-identical prefixes. Cache messages across loop iterations — first iteration does a full load, subsequent iterations only append new messages by ID. Full reload is flagged after subtask, compaction, or overflow operations. --- packages/app/vite.js | 1 + packages/opencode/src/session/prompt.ts | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/app/vite.js b/packages/app/vite.js index e1f851653c62..42997fe8922a 100644 --- a/packages/app/vite.js +++ b/packages/app/vite.js @@ -23,6 +23,7 @@ export default [ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), + "@opencode-ai/core": fileURLToPath(new URL("../core/src", import.meta.url)), }, }, define: { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 56ca82e3fcc0..c3b2fdaf0d30 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1218,14 +1218,27 @@ export const layer = Layer.effect( let structured: unknown let step = 0 const session = yield* sessions.get(sessionID).pipe(Effect.orDie) + let msgs: MessageV2.WithParts[] | undefined + let needsFullReload = true while (true) { yield* status.set(sessionID, { type: "busy" }) yield* Effect.logInfo("loop", { "session.id": sessionID, step }) - let msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe( - Effect.provideService(Database.Service, database), - ) + if (needsFullReload || !msgs) { + msgs = yield* MessageV2.filterCompactedEffect(sessionID).pipe( + Effect.provideService(Database.Service, database), + ) + needsFullReload = false + } else { + const fresh = yield* MessageV2.filterCompactedEffect(sessionID).pipe( + Effect.provideService(Database.Service, database), + ) + const knownIDs = new Set(msgs.map((m) => m.info.id)) + for (const m of fresh) { + if (!knownIDs.has(m.info.id)) msgs.push(m) + } + } const { user: lastUser, assistant: lastAssistant, finished: lastFinished, tasks } = MessageV2.latest(msgs) @@ -1277,6 +1290,7 @@ export const layer = Layer.effect( if (task?.type === "subtask") { yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs }) + needsFullReload = true continue } @@ -1289,6 +1303,7 @@ export const layer = Layer.effect( overflow: task.overflow, }) if (result === "stop") break + needsFullReload = true continue } @@ -1298,6 +1313,7 @@ export const layer = Layer.effect( (yield* compaction.isOverflow({ tokens: lastFinished.tokens, model })) ) { yield* compaction.create({ sessionID, agent: lastUser.agent, model: lastUser.model, auto: true }) + needsFullReload = true continue } @@ -1455,6 +1471,7 @@ export const layer = Layer.effect( auto: true, overflow: !handle.message.finish, }) + needsFullReload = true } return "continue" as const }).pipe(