From 3f8f72cfb2fcadd6aded3e5840561c8f382af15e 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 eb116f6b960f..ec1c23882517 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1084,14 +1084,27 @@ 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) @@ -1143,6 +1156,7 @@ const layer = Layer.effect( if (task?.type === "subtask") { yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs }) + needsFullReload = true continue } @@ -1155,6 +1169,7 @@ const layer = Layer.effect( overflow: task.overflow, }) if (result === "stop") break + needsFullReload = true continue } @@ -1164,6 +1179,7 @@ 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 } @@ -1325,6 +1341,7 @@ const layer = Layer.effect( auto: true, overflow: !handle.message.finish, }) + needsFullReload = true } return "continue" as const }).pipe(