fix(opencode): synthesize x-opencode-session when client sends none — 2026-09-06 upstream enforcement - #12719
Moseyuh333 wants to merge 2 commits into
Conversation
… 2026-09-06 upstream enforcement Starting 2026-09-06 opencode.ai errors on requests missing x-opencode-session. Requests sent through OmniRoute without any session-ish client header produced NO session header at all (only x-session-affinity/x-session-id were mapped), which trips the new upstream check and loses upstream prompt caching. forwardOpencodeClientHeaders() step-3 now synthesizes, for OpencodeExecutor only, when no session was provided: - a conversation-stable fingerprint via generateSessionId(sessionBody) (model, system prompt, first user message, tools) so consecutive agent turns share one session -> upstream prompt cache hits; - a random UUID when no body is available (same behavior as the diegosouzapw#5997 CLI-identity synth). Explicit client sessions still win (forwarded untouched); the OPENCODE_SYNTHESIZE_CLI_HEADERS opt-in path is unchanged. Plumbing (buildHeaders body param, Muse responses UUID guard) already exists in this release; this closes the last missing branch. Tests: unit (opencode-executor, refactor-buildHeaders, 5997 synthesis) + new audit suite (10) + end-to-end execute() suite (3) through a real local fetch target: 97/97 pass. Client session is forwarded unchanged; same conversation prefix keeps one synthesized session across turns.
There was a problem hiding this comment.
🟡 Changes recommended
A newly added unit test currently passes the request body in the wrong buildHeaders() parameter position, so it can “pass” without actually exercising the fingerprint-based session synthesis logic.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Ensures the OpencodeExecutor path always sends x-opencode-session upstream (synthesizing a deterministic conversation fingerprint when the client provides no session-ish header), to comply with the announced 2026-09-06 opencode.ai enforcement and preserve prompt-cache affinity.
Changes:
- Update
forwardOpencodeClientHeaders()to synthesizex-opencode-sessionwhensynthesizeRequestIdis enabled and neither a direct session header nor an affinity/session-id fallback is present. - Add new unit + execute()-level tests to pin the new outbound header contract (synthesized session/request IDs, stability across turns, and Muse Responses UUID guard).
- Update existing OpencodeExecutor tests to reflect the new “always send session” behavior when the client provides none.
File summaries
| File | Description |
|---|---|
| open-sse/utils/opencodeHeaders.ts | Add synthesis branch to guarantee x-opencode-session (fingerprint w/ body, UUID fallback w/o body). |
| tests/unit/runtime/opencode-session-headers.audit.test.ts | New audit-style unit tests for header forwarding/mapping/synthesis + stability across turns. |
| tests/unit/runtime/opencode-session-e2e.test.ts | New local-echo execute()-level tests verifying outbound wire headers. |
| tests/unit/opencode-executor.test.ts | Update existing suite expectations to match the new synthesis contract. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const h1 = executor.buildHeaders({ accessToken: "k" } as any, true, {}, "m", { | ||
| model: "m", | ||
| messages: [{ role: "user", content: "conversation A" }], | ||
| }) as Record<string, string>; | ||
| const h2 = executor.buildHeaders({ accessToken: "k" } as any, true, {}, "m", { | ||
| model: "m", | ||
| messages: [{ role: "user", content: "conversation B" }], | ||
| }) as Record<string, string>; | ||
| assert.notEqual(h1["x-opencode-session"], h2["x-opencode-session"]); |
|
Thanks for the thorough test coverage here — 13 well-written tests. But I need to flag The one place your patch does change behavior is when an operator sets Triage note: this is the review recommendation — the close itself happens only after the maintainer's per-PR sign-off (and, where a superseding PR is named, after it has landed). Nothing is being closed by this comment. |
…e only filler Review follow-up (PR diegosouzapw#12719): under the default configuration applyCliDefaults (step 4, diegosouzapw#10571) backfills x-opencode-session, so the step-3 gap this PR fixes is only observable when an operator sets OPENCODE_SYNTHESIZE_CLI_HEADERS=false — step 4 never runs there and step 3 is the only filler left. The new test isolates exactly that scenario and fails on the unpatched tip (verified: 10/11 pass there, only this test red), proving it pins the fix.
|
Took option (a) — thanks for the precise audit; you're right that step 4's Changes since your review (commit 46eb14b):
Isolation proof, run on the current tip 152d951 with the new test files dropped on unpatched vs patched:
Ready for re-review. |
Summary
Rescoped after review (commit
46eb14bd4). Under the default configuration,applyCliDefaults()(step 4, #10571) already backfillsx-opencode-sessionwith||=, so the default path is covered — thanks for the careful audit. The reproducible gap this PR actually closes is the opt-out path: withOPENCODE_SYNTHESIZE_CLI_HEADERS=false, step 4 never runs and, before this patch, step 3 did not synthesize either — an OpencodeExecutor request whose client sent no session-ish header went outbound withoutx-opencode-sessionat all.forwardOpencodeClientHeaders()step 3 (OpencodeExecutor path only) now synthesizesx-opencode-sessionwhen the client provides none ofx-opencode-session/x-session-affinity/x-session-id: a conversation-stable fingerprint viagenerateSessionId(sessionBody)(model, system prompt, first user message, tools), falling back to a random UUID when no body is available. +8/-3; explicit client sessions still win and forward untouched.x-opencode-sessionsince 2026-09-06 — the opt-out path trips exactly that check. (2) The opt-out is a documented escape hatch (fix(opencode): session stability, free-tier routing, and CLI defaults #10571 flipped the default on;OPENCODE_SYNTHESIZE_CLI_HEADERS=falselets operators who don't want fabricated CLI identity still customize env defaults), and those operators still need a session header for the enforcement and upstream prompt caching. (3) It future-proofs step 3 for any path wherecliDefaultsis absent.bodyplumbing (buildHeaders(..., body),sessionBody) and the Muse responses UUID guard already exist on the base — this closes the last missing branch.Related Issues
generateSessionIdfingerprint mechanism)Validation
Choose the change type and focused loop from the
Contribution Golden Path. The full unit suite,
Vitest, the 60% coverage gate, and the production build all run in CI on this PR (#8329):
npm run lint— not runnable in this contributor environment (localnode_modules/eslintfails to load, pre-existing, unrelated to this diff; CI runs the real lint gate)Isolation proof — current tip
152d95108, unpatched vs patchedc3945a724(identicalopencodeHeaders.tsto the tip): unpatched 10/11 — only the new test red — patched 11/11.tests/unit/opencode-executor.test.ts+tests/unit/opencode-cli-headers-synthesis-5997.test.tson the branch: 66/66 pass.npx prettier --check tests/unit/runtime/opencode-session-headers.audit.test.ts: clean.Tests Added Or Updated
tests/unit/runtime/opencode-session-headers.audit.test.ts— new file, 11 tests, including the opt-out isolation test that fails on the unpatched tip (verified) and passes with the patch.tests/unit/runtime/opencode-session-e2e.test.ts— new file, 3 tests (echo-server e2e throughOpencodeExecutor.execute()).tests/unit/opencode-executor.test.ts— 3 subtests updated to pin the new behavior (synthesize when the client sends no matching keys).Coverage Notes
open-sse/utils/opencodeHeaders.tsstep-3 behavior is covered by the audit file; the outbound wiring by the e2e file; the executor-level path by the updated subtests. No coverage movement expected.Reviewer Notes
opt-out (OPENCODE_SYNTHESIZE_CLI_HEADERS=false): step 3 alone still fills x-opencode-sessionisolates it — red on the unpatched tip (10/11), green with the patch (11/11; 14/14 with e2e). Happy to re-review.