Skip to content

feat: fingerprint-guarded session reuse as default (session: "auto")#18

Merged
justin-carper merged 6 commits into
mainfrom
feat/auto-session-reuse
Jun 11, 2026
Merged

feat: fingerprint-guarded session reuse as default (session: "auto")#18
justin-carper merged 6 commits into
mainfrom
feat/auto-session-reuse

Conversation

@justin-carper

Copy link
Copy Markdown
Collaborator

Summary

Makes Cursor session reuse safe enough to be the default, fixing the cost/fidelity problems of the per-turn-fresh path without the robustness risks of blind session: true.

Before:

  • Default (session: false) created a fresh Cursor agent every turn and re-sent the whole transcript — robust but cache-hostile, increasingly costly as a conversation grows, and paying opencode's system prompt on top of Cursor's own.
  • Opt-in session: true resumed one agent per session but blindly: it could drift from opencode's history (edits, reverts, opencode-side compaction) and was polluted by non-chat side calls (title generation).

After — session: "auto" (new default): hashes only the parts opencode replays verbatim (system prompt + user-message sequence) and classifies each turn.

Situation Classification Action
First turn new fresh agent, full transcript, pool it
System prompt differs (title gen / side calls) side-call fresh ephemeral agent; pool untouched
Prior user seq is exact prefix + exactly one new user msg continuation resume pooled agent, send only the new message
Edit / revert / compaction / queued msgs / failed resume divergence fresh agent + full transcript, re-pool

Worst case on any misclassification is one self-healing full replay — never worse than the old default. session: true is now an alias for "auto"; session: false preserves the always-fresh behavior.

Why

opencode re-sends the entire transcript every turn. Cursor caching is automatic and provider-side (no SDK knob); the only lever is prompt-prefix stability across turns — exactly Cursor's documented dispatcher/resume pattern. "auto" keeps the prefix stable on clean continuations (cache reads dominate, input stays flat) and safely degrades otherwise.

Fidelity fix

The flattened-transcript paths (new / side-call / divergence / session: false) previously dropped Cursor tool results to bare [result of X] placeholders, so a fresh agent re-read a transcript with prior tool outputs missing. Tool outputs are now included, truncated (2,000 chars/result, 500/args) — faithful without unbounded bloat.

Implementation

  • src/provider/transcript-fingerprint.ts (new): hashing + classifyTurn. Hashes system + user messages only — never assistant output (opencode re-serializes our reply unpredictably, which would spuriously mismatch every turn).
  • session-pool.ts: pool stores { agentId, systemHash, userHashes }; acquireAgent is decision-driven (resumeAgentId / poolKey / record); failed resume degrades to fresh-create.
  • language-model.ts: session?: boolean | "auto" default "auto"; classifies each turn and drives resume-vs-fresh + which message to send.
  • message-map.ts: truncated tool-output inclusion.
  • delegate.ts: updated to the new acquireAgent shape.

Side-call detection is handled by the system-prompt hash plus the fact that calls without a sessionID already skip pooling; the provider also honors an explicit providerOptions.cursor.ephemeral flag if a host sets one.

Verification

  • ✅ typecheck, build, 158 unit tests (new transcript-fingerprint classification matrix; message-map truncation; rewritten session-pool decision-driven tests).
  • 🧪 Live smoke: CURSOR_API_KEY=… node scripts/session-reuse-smoke.mjs — expect classifications fresh:new, resume, resume, fresh:divergence; on continuation turns input stays flat while cacheRead dominates.

Observability

OPENCODE_CURSOR_DEBUG=1 logs per-turn classification and cacheRead/cacheWrite usage.

Docs

README session section rewritten (classification table + cache implications: prefix cache, ~5 min TTL, what re-seeds it). CHANGELOG entry under Unreleased.

Previously the provider created a fresh Cursor agent every turn and
re-sent the whole transcript: robust but cache-hostile, increasingly
costly as a conversation grows, and it paid opencode's system prompt on
top of Cursor's own. Opt-in `session: true` resumed one agent per session
but blindly: it could drift from opencode's view of history (message
edits, reverts, opencode-side compaction) and was polluted by non-chat
side calls (e.g. title generation).

Add `session: "auto"` (new default) which hashes only the parts opencode
replays verbatim — the system prompt and the user-message sequence — and
classifies each turn:

- new          -> fresh agent, full transcript, pool it
- side-call    -> system prompt differs; fresh ephemeral agent, pool untouched
- continuation -> prior user seq is an exact prefix + exactly one new user
                  message; resume the pooled agent, send only the new message
- divergence   -> edit/revert/compaction/queued msgs (or failed resume);
                  fresh agent + full transcript, re-pool

Worst case on any misclassification is one self-healing full replay —
never worse than the old default. `session: true` is now an alias for
"auto"; `session: false` keeps the always-fresh behavior.

Also include tool outputs (truncated: 2000 chars/result, 500/args) in the
flattened transcript so fresh/divergence/`session: false` replays stay
faithful instead of dropping prior tool results to `[result of X]`
placeholders.

OPENCODE_CURSOR_DEBUG=1 logs per-turn classification and cache usage.
Adds transcript-fingerprint unit tests, rewrites session-pool tests for
the decision-driven acquireAgent contract, and adds a live
session-reuse smoke script.
@justin-carper justin-carper force-pushed the feat/auto-session-reuse branch from 6f71d05 to 361f152 Compare June 11, 2026 20:22
The config hook only snapshots opencode's MCP set once at startup, so
mid-session enable/disable never reached the Cursor agent. Re-forward the
live set from chat.params using client.mcp.status() (runtime truth) +
client.config.get() (launch specs), and force a fresh Agent.create when the
forwarded set changes between turns (a resumed agent keeps its original
servers).

Map remote OAuth client registration (clientId/clientSecret/scope) onto the
Cursor SDK's auth block so the agent runs its own OAuth flow. opencode's
access token never lands in config.mcp, so servers needing OAuth without a
shareable clientId (dynamic registration / needs_auth) are skipped and the
user is notified via a one-time toast instead of forwarding a spec that 401s.
The pool was in-memory only: an opencode restart lost the fingerprint
records, so the first turn of every resumed conversation classified as
"new" and paid a cache-cold full-transcript replay even though the
Cursor agent (and its conversation, in Cursor's checkpoint store) was
still resumable.

Persist the records best-effort to
~/.cache/opencode-cursor/session-pool.json following the model-cache
pattern (never throws, optimization-only). Records carry updatedAt and
are pruned to a 7-day TTL and a 200-entry most-recently-used cap. The
in-memory pool lazily hydrates from disk (memory wins on conflict);
concurrent opencode processes are last-write-wins on the whole file,
where a lost record costs exactly one self-healing full replay.

clearAgentPool() now wipes the disk store too; a new
resetSessionPoolMemory() test hook simulates process restarts.
The earlier ordering fix (328aecc) closed parts on text<->reasoning
transitions, but blocks-mode tool parts were enqueued while the
narration part stayed open. Hosts position a part where it STARTED, so
text streamed after a tool call appended to the pre-tool part and
rendered ABOVE the tool block.

Close the open text/reasoning part before emitting tool parts — but
only when parts are actually emitted: edit calls buffer until their
result (no parts at call time), so the narration part stays alive
across that gap instead of splitting needlessly.

Adds three regression tests: text/tool/text ordering, reasoning/tool/
reasoning ordering, and the buffered-edit no-split case.
- MCP section: live per-turn forwarding via chat.params (mcp.status +
  config.get), OAuth client mapping to the Cursor auth block, the
  unshareable-OAuth skip + one-time toast, and the session-reuse
  interaction (changed set -> fresh agent; tools sit atop the
  cache-prefix hierarchy).
- Session reuse table: add the MCP-set-changed row.
- Cache implications: MCP changes listed among prefix re-seeders.
- CHANGELOG: entries for MCP re-forwarding and the tool-block part
  ordering fix.
@justin-carper justin-carper merged commit df220e8 into main Jun 11, 2026
6 checks passed
@justin-carper justin-carper deleted the feat/auto-session-reuse branch June 11, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant