From 12b44cb7cb09fdfa175804897f59716cc51f8141 Mon Sep 17 00:00:00 2001 From: Anil Kulkarni Date: Thu, 10 Sep 2026 09:24:05 -0700 Subject: [PATCH] Pass x-opencode-session for memory, roll-call, and expand-prompt calls --- .../opencode-session-header-followup.md | 5 ++ packages/kilo-memory/src/effect/capture.ts | 2 + packages/kilo-memory/src/effect/ports.ts | 1 + .../src/kilocode/cli/cmd/roll-call.ts | 7 ++- .../opencode/src/kilocode/enhance-prompt.ts | 5 ++ .../opencode/src/kilocode/memory/ports.ts | 6 ++- .../provider/opencode-session-headers.ts | 16 +++++++ .../test/kilocode/memory/memory-ports.test.ts | 46 +++++++++++++++++-- .../provider/opencode-session-headers.test.ts | 24 ++++++++++ 9 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 .changeset/opencode-session-header-followup.md create mode 100644 packages/opencode/src/kilocode/provider/opencode-session-headers.ts create mode 100644 packages/opencode/test/kilocode/provider/opencode-session-headers.test.ts diff --git a/.changeset/opencode-session-header-followup.md b/.changeset/opencode-session-header-followup.md new file mode 100644 index 000000000000..ec21b473410f --- /dev/null +++ b/.changeset/opencode-session-header-followup.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix "missing x-opencode-session" errors from memory consolidation, prompt enhancement, and the roll-call diagnostic command when using an OpenCode-managed model. diff --git a/packages/kilo-memory/src/effect/capture.ts b/packages/kilo-memory/src/effect/capture.ts index cd0e1308d201..783bc0030e68 100644 --- a/packages/kilo-memory/src/effect/capture.ts +++ b/packages/kilo-memory/src/effect/capture.ts @@ -241,6 +241,7 @@ export namespace MemoryCapture { try: () => input.model.run({ handle: model!, + sessionID: input.sessionID, system: digestPrompt, prompt: body, timeoutMs: state.capture.timeoutMs, @@ -360,6 +361,7 @@ export namespace MemoryCapture { try: () => input.model.run({ handle: model!, + sessionID: input.sessionID, system: typedPrompt, prompt: body, timeoutMs: state.capture.timeoutMs, diff --git a/packages/kilo-memory/src/effect/ports.ts b/packages/kilo-memory/src/effect/ports.ts index 927c41f022c7..af20605993fe 100644 --- a/packages/kilo-memory/src/effect/ports.ts +++ b/packages/kilo-memory/src/effect/ports.ts @@ -39,6 +39,7 @@ export namespace MemoryPorts { readonly resolve: (input: { configured?: string; session: ModelRef }) => Effect.Effect readonly run: (input: { handle: ModelHandle + sessionID: string system: string prompt: string timeoutMs: number diff --git a/packages/opencode/src/kilocode/cli/cmd/roll-call.ts b/packages/opencode/src/kilocode/cli/cmd/roll-call.ts index 37b18716530e..cef6b7b8e50f 100644 --- a/packages/opencode/src/kilocode/cli/cmd/roll-call.ts +++ b/packages/opencode/src/kilocode/cli/cmd/roll-call.ts @@ -18,11 +18,13 @@ function loadDeps() { import("../../../provider/provider"), import("../../../effect/runtime-flags"), import("ai"), - ]).then(([runtime, provider, flags, ai]) => ({ + import("../../provider/opencode-session-headers"), + ]).then(([runtime, provider, flags, ai, headers]) => ({ AppRuntime: runtime.AppRuntime, Provider: provider.Provider, RuntimeFlags: flags.RuntimeFlags, generateText: ai.generateText, + opencodeSessionHeaders: headers.opencodeSessionHeaders, })) } function deps() { @@ -310,7 +312,7 @@ async function call( start: number, ): Promise> { try { - const { AppRuntime, RuntimeFlags, generateText } = await deps() + const { AppRuntime, RuntimeFlags, generateText, opencodeSessionHeaders } = await deps() const language = await lang(model) const sessionID = randomUUID() const options = ProviderTransform.options({ model, sessionID }) @@ -332,6 +334,7 @@ async function call( topP, topK, providerOptions, + headers: opencodeSessionHeaders({ providerID: model.providerID, sessionID }), }) return { diff --git a/packages/opencode/src/kilocode/enhance-prompt.ts b/packages/opencode/src/kilocode/enhance-prompt.ts index 3924e073398e..30f7beb52f98 100644 --- a/packages/opencode/src/kilocode/enhance-prompt.ts +++ b/packages/opencode/src/kilocode/enhance-prompt.ts @@ -1,10 +1,12 @@ import { generateText } from "ai" +import { randomUUID } from "crypto" import { mergeDeep } from "remeda" import { Provider } from "@/provider/provider" import { ProviderTransform } from "@/provider/transform" import { AppRuntime } from "@/effect/app-runtime" import { Effect } from "effect" import * as Log from "@opencode-ai/core/util/log" +import { opencodeSessionHeaders } from "@/kilocode/provider/opencode-session-headers" const log = Log.create({ service: "enhance-prompt" }) @@ -50,6 +52,9 @@ export async function enhancePrompt(text: string): Promise { ), maxRetries: 3, system: INSTRUCTION, + // Each call is a standalone rewrite, not part of a multi-turn conversation; a fresh ID + // per call still satisfies the opencode API's "stable per-conversation ID" requirement. + headers: opencodeSessionHeaders({ providerID: resolved.model.providerID, sessionID: randomUUID() }), messages: [{ role: "user" as const, content: `Draft prompt to enhance, not answer:\n\n${text}` }], }) diff --git a/packages/opencode/src/kilocode/memory/ports.ts b/packages/opencode/src/kilocode/memory/ports.ts index 9f6fc42d884e..afee8f6f690d 100644 --- a/packages/opencode/src/kilocode/memory/ports.ts +++ b/packages/opencode/src/kilocode/memory/ports.ts @@ -16,6 +16,7 @@ import type { Snapshot } from "@/snapshot" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { SessionID } from "@/session/schema" +import { opencodeSessionHeaders } from "@/kilocode/provider/opencode-session-headers" const log = Log.create({ service: "memory.ports" }) @@ -174,6 +175,7 @@ async function memoryText(input: { system: string prompt: string timeoutMs: number + sessionID: string temperature?: number topP?: number topK?: number @@ -193,6 +195,7 @@ async function memoryText(input: { topP: input.topP, topK: input.topK, maxRetries: 1, + headers: opencodeSessionHeaders({ providerID: input.source.providerID, sessionID: input.sessionID }), } const work = async () => { if (!openai) return generateText(common) @@ -314,7 +317,7 @@ export namespace MemoryModel { const language = yield* input.provider.getLanguage(source) return { handle: modelOptions(source, language), ...(reason ? { fallback: { reason } } : {}) } }).pipe(Effect.mapError(MemoryError.from)), - run: ({ handle, system, prompt, timeoutMs, signal }) => { + run: ({ handle, sessionID, system, prompt, timeoutMs, signal }) => { const resolved = handle as ModelHandle return memoryText({ source: resolved.source, @@ -323,6 +326,7 @@ export namespace MemoryModel { system, prompt, timeoutMs, + sessionID, temperature: resolved.temperature, topP: resolved.topP, topK: resolved.topK, diff --git a/packages/opencode/src/kilocode/provider/opencode-session-headers.ts b/packages/opencode/src/kilocode/provider/opencode-session-headers.ts new file mode 100644 index 000000000000..9f09eefaa563 --- /dev/null +++ b/packages/opencode/src/kilocode/provider/opencode-session-headers.ts @@ -0,0 +1,16 @@ +import { USER_AGENT } from "@/installation" + +/** + * The OpenCode Go managed-inference API requires a stable `x-opencode-session` header on + * every request routed through an `opencode*` provider (anaconda/kilocode#13723). The main + * chat path gets this from `LLMRequestPrep.prepare` (session/llm/request.ts); any other + * caller that resolves a language model and invokes the `ai` SDK directly must attach it here. + */ +export function opencodeSessionHeaders(input: { providerID: string; sessionID: string }): Record { + return input.providerID.startsWith("opencode") + ? { + "x-opencode-session": input.sessionID, + "User-Agent": USER_AGENT, + } + : {} +} diff --git a/packages/opencode/test/kilocode/memory/memory-ports.test.ts b/packages/opencode/test/kilocode/memory/memory-ports.test.ts index c316dee12f49..95e82e9169b2 100644 --- a/packages/opencode/test/kilocode/memory/memory-ports.test.ts +++ b/packages/opencode/test/kilocode/memory/memory-ports.test.ts @@ -299,12 +299,48 @@ describe("memory ports", () => { expect(seen).toEqual(["memory-config-model", "fake-memory-model"]) }) + test("model port sends x-opencode-session for opencode-managed memory models", async () => { + const calls: unknown[] = [] + const port = MemoryModel.port({ + provider: provider({ providerID: ProviderV2.ID.make("opencode"), calls }), + }) + const resolved = await Effect.runPromise(port.resolve({ session: ref })) + + await port.run({ + handle: resolved.handle, + sessionID: "ses_memory_headers", + system: "system", + prompt: "prompt", + timeoutMs: 30_000, + }) + + const opts = calls[0] as { headers?: Record } + expect(opts.headers?.["x-opencode-session"]).toBe("ses_memory_headers") + }) + + test("model port omits opencode headers for non-opencode memory models", async () => { + const calls: unknown[] = [] + const port = MemoryModel.port({ provider: provider({ calls }) }) + const resolved = await Effect.runPromise(port.resolve({ session: ref })) + + await port.run({ + handle: resolved.handle, + sessionID: "ses_memory_headers", + system: "system", + prompt: "prompt", + timeoutMs: 30_000, + }) + + const opts = calls[0] as { headers?: Record } + expect(opts.headers?.["x-opencode-session"]).toBeUndefined() + }) + test("model port asks OpenAI-compatible providers for a non-streaming JSON response", async () => { const calls: unknown[] = [] const port = MemoryModel.port({ provider: provider({ npm: "@ai-sdk/openai-compatible", calls }) }) const resolved = await Effect.runPromise(port.resolve({ session: ref })) - await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 }) + await port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000 }) const opts = calls[0] as { providerOptions?: Record } expect(opts.providerOptions?.test?.stream).toBe(false) @@ -321,7 +357,7 @@ describe("memory ports", () => { }) const resolved = await Effect.runPromise(port.resolve({ session: ref })) - await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 }) + await port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000 }) const opts = calls[0] as { providerOptions?: Record } expect(opts.providerOptions?.openai?.stream).toBe(false) @@ -374,6 +410,7 @@ describe("memory ports", () => { const resolved = await Effect.runPromise(port.resolve({ session: ref })) const result = await port.run({ handle: resolved.handle, + sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000, @@ -400,7 +437,7 @@ describe("memory ports", () => { const port = MemoryModel.port({ provider: provider({ outputs: [err, "{}"], calls }) }) const resolved = await Effect.runPromise(port.resolve({ session: ref })) - await port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 30_000 }) + await port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000 }) expect(calls).toHaveLength(2) const opts = calls[0] as { providerOptions?: Record } @@ -412,7 +449,7 @@ describe("memory ports", () => { const resolved = await Effect.runPromise(port.resolve({ session: ref })) await expect( - port.run({ handle: resolved.handle, system: "system", prompt: "prompt", timeoutMs: 1 }), + port.run({ handle: resolved.handle, sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 1 }), ).rejects.toMatchObject({ name: "TimeoutError", message: "memory model timed out" }) }) @@ -442,6 +479,7 @@ describe("memory ports", () => { await port.run({ handle: resolved.handle, + sessionID: "ses_test", system: "system", prompt: "prompt", timeoutMs: 30_000, diff --git a/packages/opencode/test/kilocode/provider/opencode-session-headers.test.ts b/packages/opencode/test/kilocode/provider/opencode-session-headers.test.ts new file mode 100644 index 000000000000..8e741c8d8ba6 --- /dev/null +++ b/packages/opencode/test/kilocode/provider/opencode-session-headers.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test" +import { opencodeSessionHeaders } from "@/kilocode/provider/opencode-session-headers" + +describe("opencodeSessionHeaders", () => { + test("attaches x-opencode-session for opencode-managed providers", () => { + const headers = opencodeSessionHeaders({ providerID: "opencode", sessionID: "ses_abc" }) + expect(headers["x-opencode-session"]).toBe("ses_abc") + }) + + test("matches any opencode-prefixed provider id (e.g. opencode-go)", () => { + const headers = opencodeSessionHeaders({ providerID: "opencode-go", sessionID: "ses_abc" }) + expect(headers["x-opencode-session"]).toBe("ses_abc") + }) + + test("returns no headers for non-opencode providers", () => { + const headers = opencodeSessionHeaders({ providerID: "anthropic", sessionID: "ses_abc" }) + expect(headers).toEqual({}) + }) + + test("returns no headers for providers that merely contain the word opencode", () => { + const headers = opencodeSessionHeaders({ providerID: "my-opencode-proxy", sessionID: "ses_abc" }) + expect(headers).toEqual({}) + }) +})