From 1d93beb5f0782e6811ee9665b4d1e3526600147f Mon Sep 17 00:00:00 2001 From: Rikkarth Date: Thu, 28 May 2026 23:25:40 +0100 Subject: [PATCH 1/2] test(session/prompt): assert source assembly order via regex for cache stability --- packages/opencode/test/session/prompt.test.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 4c4647457814..f2a5fcf05ee0 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -1,6 +1,7 @@ +import { readFileSync } from "fs" import { NodeFileSystem } from "@effect/platform-node" import { FetchHttpClient } from "effect/unstable/http" -import { expect } from "bun:test" +import { expect, test } from "bun:test" import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect" import path from "path" import { fileURLToPath, pathToFileURL } from "url" @@ -2343,3 +2344,21 @@ noLLMServer.instance( }), 30_000, ) + +test("session/prompt: source assembles system array with env at tail for cache stability", () => { + // Enforce that the assembled `system` array in runLoop puts the volatile + // env block (cwd, datetime, platform, model id) at the END so byte 0 of + // the system payload is invariant across sessions, days, and users. + // This enables exact-prefix cache reuse on every provider opencode targets. + // Related: anomalyco/opencode#20110, anomalyco/opencode#5224. + const source = readFileSync( + path.resolve(__dirname, "../../src/session/prompt.ts"), + "utf8", + ) + const match = source.match( + /const system = \[\.\.\.(\w+),\s*\.\.\.\(skills \? \[skills\] : \[\]\),\s*\.\.\.(\w+)\]/, + ) + expect(match).not.toBeNull() + expect(match![1]).toBe("instructions") + expect(match![2]).toBe("env") +}) From 8886112061437c98c07012f1fe21612db996d7a5 Mon Sep 17 00:00:00 2001 From: Rikkarth Date: Thu, 28 May 2026 23:25:44 +0100 Subject: [PATCH 2/2] fix(session): move env block to tail of system prompt for cache stability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled fixes for prefix-cache reuse across opencode sessions: 1. The current assembly order [env, instructions, skills] places volatile content (model id, cwd, worktree, git status, platform, today's date) inside the cacheable prefix region. Move env to the tail so the bulk of the system prompt is cacheable. 2. Emit a blank line ("\n\n") between and the env block preamble so the byte sequence at the seam is canonical and stable. Without this, downstream prefix caches keying on byte hash see a different separator pattern per request and miss. Measured on a 30K-token orchestrator prompt against oMLX serving Qwen3-Coder-Next-80B-A3B on an M3 Ultra: - before: every fresh session ~30s, cache.read=0 - after, same project, fresh sessions: ~3-5s, cache.read=28,672/30,000 (~95%) - after, cross-project sessions: ~22s cold seed, cache.read=2-8k (architectural ceiling — AGENTS.md/CLAUDE.md content varies per project) Refs: #20110, #5224 --- packages/opencode/src/session/prompt.ts | 7 +++++- packages/opencode/src/session/system.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 23 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 22fe4d81cd40..f1f80adb39c9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1438,7 +1438,12 @@ export const layer = Layer.effect( instruction.system().pipe(Effect.orDie), MessageV2.toModelMessagesEffect(msgs, model), ]) - const system = [...env, ...instructions, ...(skills ? [skills] : [])] + // Cache-friendly order: place stable instructions/skills first so byte 0 of + // the assembled system payload is invariant across sessions, days, and users. + // The env block (which contains a per-session datetime, per-project cwd, and + // per-agent model id) goes at the tail so it cannot defeat prefix-cache hits + // on the upstream model. See anomalyco/opencode#20110, anomalyco/opencode#5224. + const system = [...instructions, ...(skills ? [skills] : []), ...env] const format = lastUser.format ?? { type: "text" as const } if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) const result = yield* handle.process({ diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 06c71fa7dbdd..abd4b0108a95 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -49,7 +49,7 @@ export const layer = Layer.effect( const ctx = yield* InstanceState.context return [ [ - `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`, + `\nYou are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`, `Here is some useful information about the environment you are running in:`, ``, ` Working directory: ${ctx.directory}`, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index f2a5fcf05ee0..adbbe911c36d 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -2362,3 +2362,26 @@ test("session/prompt: source assembles system array with env at tail for cache s expect(match![1]).toBe("instructions") expect(match![2]).toBe("env") }) + +test("session/prompt: env block leads with \\n so the skills/env seam is a blank line", () => { + // The env block string returned by SystemPrompt.environment() must start + // with "\n" so that when request.ts joins the system array with "\n", the + // seam between and the env preamble becomes "\n\n" + // (a blank line). This makes the byte sequence at the boundary canonical + // and stable, enabling downstream prefix-cache hit rate to match what the + // LiteLLM opencode_reorder hook previously achieved server-side. + // + // We assert on the source of system.ts: the template-literal that opens + // the environment block must start with "\n" (a leading newline). + // Related: anomalyco/opencode#20110, anomalyco/opencode#5224. + const source = readFileSync( + path.resolve(__dirname, "../../src/session/system.ts"), + "utf8", + ) + // The environment() function builds the env string via a .join("\n") on an + // array. The first element of that array must be `"\nYou are powered by..." + // (i.e. the literal starts with a newline escape or a backtick-newline). + // We match the template literal opening that starts the env block string. + const match = source.match(/`\\nYou are powered by the model/) + expect(match).not.toBeNull() +})