Skip to content

v2: session ID in the first instruction block defeats cross-session prompt caching #51007

Description

@a692570

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

  1. Send a request whose system prompt contains a long stable preamble and the line Current conversation session ID: ses_AAAA....
  2. Resend it unchanged: the provider reports the full prefix as cached.
  3. 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:

  1. Render the session ID as the last instruction block, a dedicated source ordered after skills and MCP, or
  2. Inject it into the first user message as a synthetic part, the way plan reminders work, or
  3. 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.

Activity

holny commented on Sep 24, 2026

@holny

I'll take this. Confirmed on current v2: packages/core/src/instructions/builtins.ts:31 renders Current conversation session ID: ${sessionID} as the second line of the core/environment block, which is the first instruction in the assembled prompt. Everything after that line is uncachable across sessions — env tail, core/date, Code Mode catalog, AGENTS.md, skills.

Plan: split the session ID into its own instruction block emitted after core/date, so the shared prefix (env without the ID + date) stays cacheable across sessions. The session line keeps its per-session value; within one session nothing changes. Covered by the builtins test asserting the ordering.

Fixes #51007

added a commit that references this issue on Sep 26, 2026
5650694

rekram1-node commented on Sep 29, 2026

@rekram1-node
Collaborator

removed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions