Description
OpenCode v2 renders Current conversation session ID: ses_... as the second line of the <env> block, and the env block is the first instruction block in the assembled system prompt. The session ID is unique per session, so any two sessions' system prompts diverge roughly 425 tokens in. Provider prefix caching matches from the start of the request, so everything after that line is uncachable across sessions: the rest of the env block, core/date, the Code Mode catalog, loaded AGENTS.md instructions, the skills catalog, MCP instructions, and (for Anthropic-style caching) everything from the breakpoint onward.
Within one session the ID is stable, so per-turn caching works fine and the problem stays invisible until you compare first-call stats across sessions. The cost lands on the first request of every session and scales with instruction size (AGENTS.md plus the skills catalog) and with parallel sessions and subagent fan-out.
This line is v2-specific. The installed v1.18.32 binary contains no such string, and the string appears nowhere on the dev branch: the 2.0.x release line is 3,731 commits ahead of and 1,284 behind dev (diverged), so the fix belongs on the release line rather than a dev backport.
Where the line comes from
packages/core/src/instructions/builtins.ts at tag v2.0.15:
read: Effect.sync(() =>
[
"<env>",
` Current conversation session ID: ${sessionID}`, // <- line 2 of the first instruction block
` Working directory: ${location.directory}`,
` Workspace root folder: ${location.project.directory}`,
` Is directory a git repo: ${location.vcs?.type === "git" ? "yes" : "no"}`,
` Platform: ${process.platform}`,
` Prefer ${global.tmp} over generic system temporary directories such as /tmp; it is pre-created and approved for external access.`,
"</env>",
].join("\n"),
)
Render order in packages/core/src/session/context.ts:
instructions: Instructions.combine([
loaded.builtins, // core/environment (carries the session ID), core/date
CodeModeInstructions.make(...),
loaded.discovery, // AGENTS.md files
loaded.skills,
loaded.references,
loaded.mcp,
loaded.entries,
]),
Instructions.renderInitial renders sources in list order, so the per-session line sits near the front of the system message.
Measured evidence
Request capture (opencode run on 2.0.15, request body logged through a local proxy):
- The request has a single system message of 147,335 chars. The session ID sits at char 1,697, inside the env block.
- The 145,600 chars (~36K tokens) that follow it, the env remainder, date, Code Mode catalog, AGENTS.md content, and skills catalog, diverge per session and cannot be reused across sessions.
Probe against a vLLM-backed OpenAI-compatible endpoint, same request body sent twice with only the session-ID line changed:
| call |
prompt_tokens |
cached_tokens |
| session X, first send |
2234 |
0 |
| session X, identical resend |
2234 |
2176 |
| session Y, identical except the ID line |
2236 |
0 |
One differing line defeats the entire 2,176-token prefix. Same-ID resend hits the full prefix.
Local session DB, last 7 days, fresh session = session created within 60s of its first call:
- 87 fresh sessions, 10.9M first-call input tokens, 3.7M reported cache reads, 7.2M tokens re-processed uncached.
- 46 fresh sessions on one vLLM-backed model: 31 (67%) reported
cache read = 0 on their first call. The 15 others showed large first-call reads (172K to 183K tokens on tiny uncached inputs) whose exact match content I could not verify; full system-prompt reuse is impossible by construction either way, because the divergence point is the session ID itself.
- Turn-to-turn behavior inside one session confirms the mechanism. Turn 1 had input 97,446 and read 0; turn 2 read 97,444. The prefix only matches when the ID matches.
With Anthropic-style caching the same math applies: opencode marks the system message with cache_control: ephemeral, and the cached prefix stops at the first differing token, which is the session ID.
Steps to reproduce
- Send a request whose system prompt contains a long stable preamble and the line
Current conversation session ID: ses_AAAA....
- Resend it unchanged: the provider reports the full prefix as cached.
- Resend it with only the session ID changed: cached tokens drop to 0.
The probe above shows all three steps. For an Anthropic provider, compare cache_read_input_tokens on the first call of two sessions with identical config and no history.
Suggested fix
Keep the ID visible to the model but out of the cacheable prefix:
- Render the session ID as the last instruction block, a dedicated source ordered after skills and MCP, or
- Inject it into the first user message as a synthetic part, the way plan reminders work, or
- Drop it from the env block entirely.
Plugins
None.
OpenCode version
2.0.15 (tag v2.0.15; verified against tag source, the installed binary, and a live request capture)
Screenshot and/or share link
n/a; captured request data is quoted above.
Operating System
macOS 26.6.2 (Darwin 25.6.0)
Terminal
n/a (headless opencode run)
Related
#48778 covers the within-session variant: a mid-session instruction edit or the midnight date rollover rebuilds the prompt and forces a full re-prefill. This issue is the cross-session variant: even with no edits and no rollover, a fresh session can never inherit the previous session's cached system prefix.
Description
OpenCode v2 renders
Current conversation session ID: ses_...as the second line of the<env>block, and the env block is the first instruction block in the assembled system prompt. The session ID is unique per session, so any two sessions' system prompts diverge roughly 425 tokens in. Provider prefix caching matches from the start of the request, so everything after that line is uncachable across sessions: the rest of the env block,core/date, the Code Mode catalog, loaded AGENTS.md instructions, the skills catalog, MCP instructions, and (for Anthropic-style caching) everything from the breakpoint onward.Within one session the ID is stable, so per-turn caching works fine and the problem stays invisible until you compare first-call stats across sessions. The cost lands on the first request of every session and scales with instruction size (AGENTS.md plus the skills catalog) and with parallel sessions and subagent fan-out.
This line is v2-specific. The installed v1.18.32 binary contains no such string, and the string appears nowhere on the
devbranch: the 2.0.x release line is 3,731 commits ahead of and 1,284 behinddev(diverged), so the fix belongs on the release line rather than a dev backport.Where the line comes from
packages/core/src/instructions/builtins.tsat tagv2.0.15:Render order in
packages/core/src/session/context.ts:Instructions.renderInitialrenders sources in list order, so the per-session line sits near the front of the system message.Measured evidence
Request capture (
opencode runon 2.0.15, request body logged through a local proxy):Probe against a vLLM-backed OpenAI-compatible endpoint, same request body sent twice with only the session-ID line changed:
One differing line defeats the entire 2,176-token prefix. Same-ID resend hits the full prefix.
Local session DB, last 7 days, fresh session = session created within 60s of its first call:
cache read = 0on their first call. The 15 others showed large first-call reads (172K to 183K tokens on tiny uncached inputs) whose exact match content I could not verify; full system-prompt reuse is impossible by construction either way, because the divergence point is the session ID itself.With Anthropic-style caching the same math applies: opencode marks the system message with
cache_control: ephemeral, and the cached prefix stops at the first differing token, which is the session ID.Steps to reproduce
Current conversation session ID: ses_AAAA....The probe above shows all three steps. For an Anthropic provider, compare
cache_read_input_tokenson the first call of two sessions with identical config and no history.Suggested fix
Keep the ID visible to the model but out of the cacheable prefix:
Plugins
None.
OpenCode version
2.0.15 (tag
v2.0.15; verified against tag source, the installed binary, and a live request capture)Screenshot and/or share link
n/a; captured request data is quoted above.
Operating System
macOS 26.6.2 (Darwin 25.6.0)
Terminal
n/a (headless
opencode run)Related
#48778 covers the within-session variant: a mid-session instruction edit or the midnight date rollover rebuilds the prompt and forces a full re-prefill. This issue is the cross-session variant: even with no edits and no rollover, a fresh session can never inherit the previous session's cached system prefix.