Compare commits

...

3 Commits

Author SHA1 Message Date
Aiden Cline e9f8a210b9 fix(opencode): add namespaced session identity headers (#52370) 2026-09-30 15:39:43 -05:00
Ryan 9b4882db54 fix(opencode): ad-hoc re-sign darwin binaries after local compile (#52183)
Co-authored-by: Ryan Wyler <ryan.wyler@gmail.com>
2026-09-30 12:53:09 -05:00
Metal Heart 97a86b7677 fix(tui): use path.sep for plugin name extraction in /status dialog (#52328) 2026-09-30 11:22:47 -05:00
6 changed files with 105 additions and 68 deletions
+2
View File
@@ -206,6 +206,8 @@ const layer = Layer.effect(
model,
http: {
headers: {
"x-opencode-session-id": session.id,
...(session.parentID ? { "x-opencode-parent-session-id": session.parentID } : {}),
"x-session-affinity": session.id,
"X-Session-Id": session.id,
...(session.parentID ? { "x-parent-session-id": session.parentID } : {}),
+10 -1
View File
@@ -1103,10 +1103,12 @@ describe("SessionRunnerLLM", () => {
expect(requests).toHaveLength(2)
expect(requests.map((request) => request.http?.headers)).toEqual([
{
"x-opencode-session-id": sessionID,
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
},
{
"x-opencode-session-id": sessionID,
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
},
@@ -2528,6 +2530,7 @@ describe("SessionRunnerLLM", () => {
yield* session.resume(sessionID)
expect(requests[0]?.http?.headers).toEqual({
"x-opencode-session-id": sessionID,
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
})
@@ -2551,7 +2554,13 @@ describe("SessionRunnerLLM", () => {
requests.length = 0
yield* session.resume(sessionID)
expect(requests[0]?.http?.headers?.["x-parent-session-id"]).toBe(parentID)
expect(requests[0]?.http?.headers).toEqual({
"x-opencode-session-id": sessionID,
"x-opencode-parent-session-id": parentID,
"x-session-affinity": sessionID,
"X-Session-Id": sessionID,
"x-parent-session-id": parentID,
})
}),
)
+6
View File
@@ -201,6 +201,12 @@ for (const item of targets) {
},
})
// Embedding the bundle invalidates the linker's ad-hoc signature, and macOS 27+
// SIGKILLs binaries with invalid pages. Re-sign ad-hoc; release CI re-signs with Developer ID.
if (item.os === "darwin" && process.platform === "darwin") {
await $`codesign --force --sign - dist/${name}/bin/opencode`
}
// Smoke test: only run if binary is for current platform
if (item.os === process.platform && item.arch === process.arch && !item.abi) {
const binaryPath = `dist/${name}/bin/opencode`
@@ -185,6 +185,8 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
params,
messageTransformOptions: options,
headers: {
"x-opencode-session-id": input.sessionID,
...(input.parentSessionID ? { "x-opencode-parent-session-id": input.parentSessionID } : {}),
...(input.model.providerID.startsWith("opencode")
? {
...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}),
+81 -64
View File
@@ -756,72 +756,89 @@ describe("session.llm.stream", () => {
const vivgridFixture = { providerID: "vivgrid", modelID: "gemini-3.1-pro-preview" }
const opencodeFixture = { providerID: "opencode-test", modelID: vivgridFixture.modelID }
it.instance(
"sends the parent session header for opencode providers",
() =>
Effect.gen(function* () {
const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
const request = waitRequest(
"/chat/completions",
new Response(createChatStream("Hello"), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
}),
)
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make(opencodeFixture.providerID),
ModelV2.ID.make(opencodeFixture.modelID),
)
const sessionID = SessionID.make("session-child")
const parentSessionID = SessionID.make("session-parent")
const agent = {
name: "test",
mode: "primary",
options: {},
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
const user = {
id: MessageID.make("msg_user-parent-header"),
sessionID,
role: "user",
time: { created: Date.now() },
agent: agent.name,
model: {
providerID: ProviderV2.ID.make(opencodeFixture.providerID),
modelID: resolved.id,
},
} satisfies SessionV1.User
yield* drain({
user,
sessionID,
parentSessionID,
model: resolved,
agent,
system: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Hello" }],
tools: {},
})
expect((yield* Effect.promise(() => request)).headers.get("x-parent-session-id")).toBe(parentSessionID)
}),
{
config: () => {
const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
return {
enabled_providers: [opencodeFixture.providerID],
provider: {
[opencodeFixture.providerID]: {
name: "OpenCode Test",
npm: "@ai-sdk/openai-compatible",
models: { [fixture.model.id]: configModel(fixture.model) as ConfigModel },
options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
const headerCases = [
{ providerID: opencodeFixture.providerID, child: false },
{ providerID: opencodeFixture.providerID, child: true },
{ providerID: "custom-test", child: false },
{ providerID: "custom-test", child: true },
]
headerCases.forEach((input) => {
it.instance(
`sends session identity headers for ${input.child ? "child" : "root"} sessions on ${input.providerID}`,
() =>
Effect.gen(function* () {
const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
const request = waitRequest(
"/chat/completions",
new Response(createChatStream("Hello"), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
}),
)
const resolved = yield* Provider.use.getModel(
ProviderV2.ID.make(input.providerID),
ModelV2.ID.make(opencodeFixture.modelID),
)
const sessionID = SessionID.make("session-child")
const parentSessionID = input.child ? SessionID.make("session-parent") : undefined
const agent = {
name: "test",
mode: "primary",
options: {},
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info
const user = {
id: MessageID.make("msg_user-parent-header"),
sessionID,
role: "user",
time: { created: Date.now() },
agent: agent.name,
model: {
providerID: ProviderV2.ID.make(input.providerID),
modelID: resolved.id,
},
},
}
} satisfies SessionV1.User
yield* drain({
user,
sessionID,
parentSessionID,
model: resolved,
agent,
system: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Hello" }],
tools: {},
})
const headers = (yield* Effect.promise(() => request)).headers
expect(headers.get("x-opencode-session-id")).toBe(sessionID)
expect(headers.get("x-opencode-parent-session-id")).toBe(parentSessionID ?? null)
expect(headers.get("x-parent-session-id")).toBe(parentSessionID ?? null)
if (input.providerID.startsWith("opencode")) {
expect(headers.get("x-opencode-session")).toBe(sessionID)
return
}
expect(headers.get("x-session-affinity")).toBe(sessionID)
expect(headers.get("X-Session-Id")).toBe(sessionID)
}),
{
config: () => {
const fixture = loadFixture(vivgridFixture.providerID, vivgridFixture.modelID)
return {
enabled_providers: [input.providerID],
provider: {
[input.providerID]: {
name: "OpenCode Test",
npm: "@ai-sdk/openai-compatible",
models: { [fixture.model.id]: configModel(fixture.model) as ConfigModel },
options: { apiKey: "test-key", baseURL: `${state.server!.url.origin}/v1` },
},
},
}
},
},
},
)
)
})
it.instance(
"sends temperature, tokens, and reasoning options for openai-compatible models",
+4 -3
View File
@@ -1,5 +1,6 @@
import { TextAttributes } from "@opentui/core"
import { fileURLToPath } from "bun"
import path from "path"
import { useTheme } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { useSync } from "../context/sync"
@@ -19,9 +20,9 @@ export function DialogStatus() {
const result = list.map((item) => {
const value = typeof item === "string" ? item : item[0]
if (value.startsWith("file://")) {
const path = fileURLToPath(value)
const parts = path.split("/")
const filename = parts.pop() || path
const filePath = fileURLToPath(value)
const parts = filePath.split(path.sep)
const filename = parts.pop() || filePath
if (!filename.includes(".")) return { name: filename }
const basename = filename.split(".")[0]
if (basename === "index") {