From 5d709807542436be1b93b0908ef06de34a9f2b4e Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 11:17:49 +0100 Subject: [PATCH 1/7] fix(opencode): list a project's own agents in its picker Read agents with the workspace's own location on both runtimes and publish them as that workspace's agent option descriptor. A failed or empty read publishes nothing, and the descriptor is only published when it has visible options, so an all-hidden project cannot override the machine agent control. resolveProvidersForCwd now applies a workspace snapshot's option descriptors to that cwd's models, which is where the web and mobile pickers already look. --- .../client-runtime/src/providerSkills.test.ts | 39 +++++++++++ packages/client-runtime/src/providerSkills.ts | 23 ++++++- .../src/server/OpenCodeRuntime.ts | 2 +- .../src/server/driver.test.ts | 56 ++++++++++++++++ .../provider-opencode/src/server/driver.ts | 67 ++++++++++++++----- .../src/server/status.test.ts | 5 +- .../provider-opencode/src/server/status.ts | 50 ++++++++------ 7 files changed, 198 insertions(+), 44 deletions(-) diff --git a/packages/client-runtime/src/providerSkills.test.ts b/packages/client-runtime/src/providerSkills.test.ts index acd4a4904c1d4..1cd4829e29cd2 100644 --- a/packages/client-runtime/src/providerSkills.test.ts +++ b/packages/client-runtime/src/providerSkills.test.ts @@ -145,6 +145,45 @@ describe("workspace provider snapshots", () => { expect(resolveProvidersForCwd(providers, "/workspace/project-a")[0]).toBe(provider); expect(resolveProvidersForCwd(providers, "/workspace/project-b")).toBe(providers); }); + + it("applies a workspace agent descriptor to that cwd's models", () => { + const agentDescriptor = { + id: "agent", + label: "Agent", + type: "select" as const, + options: [ + { id: "build", label: "Build", isDefault: true }, + { id: "reviewer", label: "Reviewer" }, + ], + currentValue: "build", + }; + const projectModel = { + slug: "uni/medium-gpt", + name: "Medium GPT", + isCustom: false, + capabilities: { + optionDescriptors: [ + { id: "variant", label: "Reasoning", type: "select" as const, options: [] }, + ], + }, + } satisfies ServerProvider["models"][number]; + const withDescriptor = { + ...provider, + workspaceSnapshots: provider.workspaceSnapshots.map((snapshot) => ({ + ...snapshot, + models: [projectModel], + optionDescriptors: [agentDescriptor], + })), + } satisfies ServerProvider; + + const providers: ReadonlyArray = [provider, withDescriptor]; + const resolved = resolveProvidersForCwd(providers, "/workspace/project-a"); + expect(resolved[0]).toBe(provider); + expect(resolved[1]?.models[0]?.capabilities?.optionDescriptors).toEqual([ + ...(projectModel.capabilities?.optionDescriptors ?? []), + agentDescriptor, + ]); + }); }); describe("provider workspace snapshot refresh atom", () => { diff --git a/packages/client-runtime/src/providerSkills.ts b/packages/client-runtime/src/providerSkills.ts index 0f91fc29f74e5..44331ec0a8bdb 100644 --- a/packages/client-runtime/src/providerSkills.ts +++ b/packages/client-runtime/src/providerSkills.ts @@ -133,12 +133,31 @@ export function resolveProviderSlashCommandsForCwd( return resolveProviderWorkspaceSnapshot(provider, cwd)?.slashCommands ?? provider.slashCommands; } -/** A project's own provider config can add models that only its workspace snapshot lists. */ +/** + * A project's own provider config can add models that only its workspace + * snapshot lists, and its agents are published as option descriptors. + */ function resolveProviderModelsForCwd( provider: ServerProvider, cwd: string | null | undefined, ): ServerProvider["models"] { - return resolveProviderWorkspaceSnapshot(provider, cwd)?.models ?? provider.models; + const snapshot = resolveProviderWorkspaceSnapshot(provider, cwd); + const models = snapshot?.models ?? provider.models; + const descriptors = snapshot?.optionDescriptors; + if (descriptors === undefined) return models; + const overriddenIds = new Set(descriptors.map((descriptor) => descriptor.id)); + return models.map((model) => ({ + ...model, + capabilities: { + ...model.capabilities, + optionDescriptors: [ + ...(model.capabilities?.optionDescriptors ?? []).filter( + (descriptor) => !overriddenIds.has(descriptor.id), + ), + ...descriptors, + ], + }, + })); } /** Providers keep their identity unless the cwd has its own catalog. */ diff --git a/packages/provider-opencode/src/server/OpenCodeRuntime.ts b/packages/provider-opencode/src/server/OpenCodeRuntime.ts index a99e8cd7daece..4d2249a53584f 100644 --- a/packages/provider-opencode/src/server/OpenCodeRuntime.ts +++ b/packages/provider-opencode/src/server/OpenCodeRuntime.ts @@ -242,7 +242,7 @@ const loadOpenCodeProviders = (client: OpencodeClient) => ), ); -const loadOpenCodeAgents = (client: OpencodeClient) => +export const loadOpenCodeAgents = (client: OpencodeClient) => runOpenCodeSdk("app.agents", (signal) => client.app.agents(undefined, { signal })).pipe( Effect.map((result): ReadonlyArray => result.data ?? []), ); diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 6d74dc8dd2542..cd857916cacfa 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -267,6 +267,62 @@ const lazyMiseShim = { } as unknown as OpenCodeRuntime.OpenCodeRuntime["Service"]; it.layer(layer)("OpenCodeDriver runtime selection", (it) => { + it.effect("lists each OpenCode 2 workspace's own agents", () => + Effect.gen(function* () { + serverStarts.length = 0; + const requested: Array = []; + const server = HttpClient.make((request, url) => { + const directory = url.searchParams.get("location[directory]"); + const responses = { + ...OPENCODE_2_RESPONSES, + ...OPENCODE_2_WORKSPACE_RESPONSES, + "/api/agent": { + status: 200, + contentType: "application/json", + body: JSON.stringify({ + location: { directory }, + data: [ + { id: "build", mode: "primary", hidden: false }, + { + id: directory === "/work" ? "reviewer" : "writer", + mode: "all", + hidden: false, + }, + { id: "internal", mode: "primary", hidden: true }, + ].map((agent) => ({ + ...agent, + name: agent.id, + request: { settings: {}, headers: {}, body: {} }, + permissions: [], + })), + }), + }, + }; + return replayOpenCodeServer(responses, "secret", requested).execute(request); + }); + const instance = yield* create( + { serverUrl: "http://127.0.0.1:4096", serverPassword: "secret" }, + server, + ); + + const workAgent = (yield* instance.snapshotForCwd!("/work")).optionDescriptors?.find( + (option) => option.id === "agent", + ); + assert.deepStrictEqual( + workAgent?.type === "select" ? workAgent.options.map((option) => option.id) : undefined, + ["build", "reviewer"], + ); + const otherAgent = (yield* instance.snapshotForCwd!("/other")).optionDescriptors?.find( + (option) => option.id === "agent", + ); + assert.deepStrictEqual( + otherAgent?.type === "select" ? otherAgent.options.map((option) => option.id) : undefined, + ["build", "writer"], + ); + assert.deepStrictEqual(serverStarts, []); + }).pipe(Effect.scoped), + ); + it.effect("never starts a 1.x server for an OpenCode 2 instance", () => Effect.gen(function* () { serverStarts.length = 0; diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 2686de885b3f0..8a148a8d7ba97 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -36,6 +36,7 @@ import { loadOpenCode2Catalog, loadOpenCode2DirectoryCatalog } from "./openCode2 import { checkOpenCodeProviderStatus, loadOpenCode2Workspace, + openCodeAgentOptionDescriptor, makeOpenCode2ModelLoader, makePendingOpenCodeProvider, openCode2CommandsToServerProviderSlashCommands, @@ -424,6 +425,11 @@ export const OpenCodeDriver: ProviderDriver< return yield* loadOpenCode2Workspace( Effect.all( { + agents: client.agent.list({ location }).pipe( + Effect.map((list) => list.data), + Effect.timeout("10 seconds"), + Effect.catchCause(() => Effect.undefined), + ), skills: client.skill.list({ location }).pipe(Effect.map((list) => list.data)), commands: client.command.list({ location }).pipe(Effect.map((list) => list.data)), }, @@ -513,6 +519,9 @@ export const OpenCodeDriver: ProviderDriver< ) => Effect.all( { + agents: OpenCodeRuntime.loadOpenCodeAgents(client).pipe( + Effect.catchCause(() => Effect.undefined), + ), skills: openCodeRuntime.loadOpenCodeSkills(client), commandInventory: OpenCodeRuntime.loadOpenCodeCommands(client).pipe( Effect.timeoutOption("10 seconds"), @@ -539,7 +548,8 @@ export const OpenCodeDriver: ProviderDriver< }, { concurrency: "unbounded" }, ).pipe( - Effect.map(({ skills, commandInventory, models }) => ({ + Effect.map(({ agents, skills, commandInventory, models }) => ({ + agents, skills, models, ...commandInventory, @@ -701,28 +711,51 @@ export const OpenCodeDriver: ProviderDriver< ], { concurrency: "unbounded" }, ).pipe( - Effect.map(([machineSnapshot, cwdModels, { skills, commands }]) => ({ - ...machineSnapshot, - ...workspaceModelsFor(machineSnapshot.models, cwdModels), - skills: openCode2SkillsToServerProviderSkills(skills), - slashCommands: openCode2CommandsToServerProviderSlashCommands(commands), - // A finished scan lists at least the built-ins. None means it - // timed out, so the registry decides from the provider's health. - slashCommandsPending: commands.length > 0 ? false : undefined, - })), + Effect.map(([machineSnapshot, cwdModels, { agents, skills, commands }]) => { + const agentDescriptor = + agents === undefined || agents.length === 0 + ? undefined + : openCodeAgentOptionDescriptor( + agents.map((agent) => ({ ...agent, name: agent.id })), + ); + return { + ...machineSnapshot, + ...workspaceModelsFor(machineSnapshot.models, cwdModels), + ...(agentDescriptor === undefined || agentDescriptor.options.length === 0 + ? {} + : { optionDescriptors: [agentDescriptor] }), + skills: openCode2SkillsToServerProviderSkills(skills), + slashCommands: openCode2CommandsToServerProviderSlashCommands(commands), + // A finished scan lists at least the built-ins. None means it + // timed out, so the registry decides from the provider's health. + slashCommandsPending: commands.length > 0 ? false : undefined, + }; + }), ), v1: Effect.all([ snapshot.getSnapshot, loadWorkspaceForCwd(cwd).pipe(Effect.timeout("20 seconds")), ]).pipe( Effect.map( - ([machineSnapshot, { skills, models, commands, slashCommandsPending }]) => ({ - ...machineSnapshot, - ...workspaceModelsFor(machineSnapshot.models, models), - skills: openCodeSkillsToServerProviderSkills(skills), - slashCommands: openCodeCommandsToServerProviderSlashCommands(commands), - slashCommandsPending, - }), + ([ + machineSnapshot, + { agents, skills, models, commands, slashCommandsPending }, + ]) => { + const agentDescriptor = + agents === undefined || agents.length === 0 + ? undefined + : openCodeAgentOptionDescriptor(agents); + return { + ...machineSnapshot, + ...workspaceModelsFor(machineSnapshot.models, models), + ...(agentDescriptor === undefined || agentDescriptor.options.length === 0 + ? {} + : { optionDescriptors: [agentDescriptor] }), + skills: openCodeSkillsToServerProviderSkills(skills), + slashCommands: openCodeCommandsToServerProviderSlashCommands(commands), + slashCommandsPending, + }; + }, ), Effect.mapError( (cause) => diff --git a/packages/provider-opencode/src/server/status.test.ts b/packages/provider-opencode/src/server/status.test.ts index f691337257a34..d3411022ad02c 100644 --- a/packages/provider-opencode/src/server/status.test.ts +++ b/packages/provider-opencode/src/server/status.test.ts @@ -1101,6 +1101,7 @@ const builtinSkills = [ { id: "report", name: "Report", path: "/builtin/report.md" }, ]; const scanned: OpenCode2Workspace = { + agents: [], commands: [...builtinCommands, { name: "hello", description: "Say hello to the workspace" }], skills: [ ...builtinSkills, @@ -1117,7 +1118,7 @@ it.effect( "reads a loaded OpenCode 2 directory listing no commands again once its scan has ended", () => Effect.gen(function* () { - const reads: Array = [{ commands: [], skills: [] }, scanned]; + const reads: Array = [{ agents: [], commands: [], skills: [] }, scanned]; const scanEnded = yield* Deferred.make(); const load = yield* loadOpenCode2Workspace( Effect.sync(() => reads.shift()!), @@ -1135,7 +1136,7 @@ it.effect("waits for the scan of a cold OpenCode 2 directory that first lists on Effect.gen(function* () { const calls: Array = []; const reads: Array = [ - { commands: builtinCommands, skills: builtinSkills }, + { agents: [], commands: builtinCommands, skills: builtinSkills }, scanned, ]; const workspace = yield* loadOpenCode2Workspace( diff --git a/packages/provider-opencode/src/server/status.ts b/packages/provider-opencode/src/server/status.ts index 233a9bc516c5c..1a14e7c0b13fc 100644 --- a/packages/provider-opencode/src/server/status.ts +++ b/packages/provider-opencode/src/server/status.ts @@ -1,5 +1,6 @@ import { type ModelCapabilities, + type SelectProviderOptionDescriptor, type ServerProviderModel, type ServerProviderSkill, type ServerProviderSlashCommand, @@ -199,8 +200,24 @@ function inferDefaultVariant( return undefined; } -function inferDefaultAgent(agents: ReadonlyArray): string | undefined { - return agents.find((agent) => agent.name === "build")?.name ?? agents[0]?.name ?? undefined; +export function openCodeAgentOptionDescriptor( + agents: ReadonlyArray>, +): SelectProviderOptionDescriptor { + const visible = agents.filter( + (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"), + ); + const defaultAgent = visible.find((agent) => agent.name === "build")?.name ?? visible[0]?.name; + return { + id: "agent", + label: "Agent", + type: "select", + options: visible.map((agent) => ({ + id: agent.name, + label: titleCaseSlug(agent.name), + ...(agent.name === defaultAgent ? { isDefault: true } : {}), + })), + ...(defaultAgent ? { currentValue: defaultAgent } : {}), + }; } const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ @@ -242,15 +259,7 @@ function openCodeCapabilitiesForModel(input: { ? { id: value, label: titleCaseSlug(value), isDefault: true as const } : { id: value, label: titleCaseSlug(value) }, ); - const primaryAgents = input.agents.filter( - (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"), - ); - const defaultAgent = inferDefaultAgent(primaryAgents); - const agentOptions = primaryAgents.map((agent) => - defaultAgent === agent.name - ? { id: agent.name, label: titleCaseSlug(agent.name), isDefault: true as const } - : { id: agent.name, label: titleCaseSlug(agent.name) }, - ); + const agentDescriptor = openCodeAgentOptionDescriptor(input.agents); return createModelCapabilities({ optionDescriptors: [ ...(variantOptions.length > 0 @@ -264,17 +273,7 @@ function openCodeCapabilitiesForModel(input: { }, ] : []), - ...(agentOptions.length > 0 - ? [ - { - id: "agent", - label: "Agent", - type: "select" as const, - options: agentOptions, - ...(defaultAgent ? { currentValue: defaultAgent } : {}), - }, - ] - : []), + ...(agentDescriptor.options.length > 0 ? [agentDescriptor] : []), ], }); } @@ -554,6 +553,13 @@ export const readSettledOpenCode2Catalog = ( ); export interface OpenCode2Workspace { + readonly agents: + | ReadonlyArray<{ + readonly id: string; + readonly mode: Agent["mode"]; + readonly hidden: boolean; + }> + | undefined; readonly skills: ReadonlyArray<{ readonly id: string; readonly name: string; From 0d90985b40ac483eb49f5e507cc2c8126113b1f2 Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 11:52:22 +0100 Subject: [PATCH 2/7] fix(opencode): bound workspace agent reads A stalled agent endpoint must not consume the workspace scan deadline. Both runtimes bound the agent read at three seconds and fall back to no descriptor, and a stalled v2 endpoint now has coverage. --- .../src/server/driver.test.ts | 44 +++++++++++++++++++ .../provider-opencode/src/server/driver.ts | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index cd857916cacfa..20a01212bf882 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -593,6 +593,50 @@ it.layer(layer)("OpenCodeDriver runtime selection", (it) => { }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), ); + it.effect("keeps workspace skills and commands when the agent endpoint stalls", () => + Effect.gen(function* () { + const answers: Record = { + "/api/info": OPENCODE_2_RESPONSES["/api/info"].body, + "/api/debug/location": "[]", + "/api/model": '{"location":{"directory":"/work"},"data":[]}', + "/api/provider": '{"location":{"directory":"/work"},"data":[]}', + "/api/command": OPENCODE_2_WORKSPACE_RESPONSES["/api/command"].body, + "/api/skill": OPENCODE_2_WORKSPACE_RESPONSES["/api/skill"].body, + }; + const http = HttpClient.make((request) => { + const path = new URL(request.url).pathname; + if (path === "/api/agent") return Effect.never; + if (path === "/api/event") + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(new ReadableStream(), { + headers: { "content-type": "text/event-stream" }, + }), + ), + ); + const body = answers[path]; + return body === undefined + ? Effect.die(`Unexpected request: ${path}`) + : Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(JSON.parse(body)))); + }); + const instance = yield* create({ serverUrl: "http://127.0.0.1:4096" }, http); + + const snapshot = yield* instance.snapshotForCwd!("/work").pipe(Effect.forkChild); + yield* TestClock.adjust("30 seconds"); + const { skills, slashCommands, optionDescriptors } = yield* Fiber.join(snapshot); + assert.include( + skills.map((skill) => skill.name), + "plum", + ); + assert.include( + slashCommands.map((command) => command.name), + "review", + ); + assert.isUndefined(optionDescriptors?.find((option) => option.id === "agent")); + }).pipe(Effect.scoped, Effect.provide(TestClock.layer())), + ); + it.effect.each(["probe", "error", "empty"] as const)( "recovers discovery without a manual refresh after %s", (initialCatalog) => diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index 8a148a8d7ba97..3f6ca24e256db 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -427,7 +427,7 @@ export const OpenCodeDriver: ProviderDriver< { agents: client.agent.list({ location }).pipe( Effect.map((list) => list.data), - Effect.timeout("10 seconds"), + Effect.timeout("3 seconds"), Effect.catchCause(() => Effect.undefined), ), skills: client.skill.list({ location }).pipe(Effect.map((list) => list.data)), @@ -520,6 +520,7 @@ export const OpenCodeDriver: ProviderDriver< Effect.all( { agents: OpenCodeRuntime.loadOpenCodeAgents(client).pipe( + Effect.timeout("3 seconds"), Effect.catchCause(() => Effect.undefined), ), skills: openCodeRuntime.loadOpenCodeSkills(client), From a978974eea8951c20bc7cc5980657c15100c6757 Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 12:07:47 +0100 Subject: [PATCH 3/7] test(opencode): cover stalled OpenCode 1 agent discovery The v1 fixture now answers app.agents for every project except one, where it stalls until the request is aborted, so the snapshot path is exercised with commands and skills present. --- .../src/server/driver.test.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 20a01212bf882..6cd163c3b2a70 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -1122,6 +1122,7 @@ const openCode1Provider = (id: string, models: ReadonlyArray) => ({ }); const openCode1Project = "/work/project-with-config"; const openCode1UnreadableProject = "/work/unreadable-project"; +const openCode1StalledAgents = "/work/stalled-agents"; const openCode1Providers = (directory: string) => { const all = [ openCode1Provider("opencode", ["big-pickle"]), @@ -1145,7 +1146,14 @@ const openCode1Runtime = { ? Promise.reject(new Error("provider list failed")) : Promise.resolve({ data: openCode1Providers(directory) }), }, - app: { agents: () => Promise.resolve({ data: [] }) }, + app: { + agents: (_input: unknown, options?: { readonly signal?: AbortSignal }) => + directory === openCode1StalledAgents + ? new Promise((_resolve, reject) => { + options?.signal?.addEventListener("abort", () => reject(new Error("aborted"))); + }) + : Promise.resolve({ data: [] }), + }, command: { list: () => Promise.resolve({ data: [] }) }, }), loadOpenCodeInventory: () => @@ -1183,6 +1191,24 @@ it.layer(Layer.mergeAll(layer, Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, op assert.strictEqual(unreadable.workspaceModelsPending, true); }).pipe(Effect.scoped), ); + + it.effect("keeps commands and skills when OpenCode 1 agent discovery stalls", () => + Effect.gen(function* () { + const instance = yield* create({}, noHttp); + const ready = yield* instance.snapshot.streamChanges.pipe( + Stream.filter((snapshot) => snapshot.status === "ready"), + Stream.take(1), + Stream.runCollect, + Effect.forkChild, + ); + yield* Fiber.join(ready); + + const snapshot = yield* instance.snapshotForCwd!(openCode1StalledAgents); + assert.deepStrictEqual(snapshot.skills, []); + assert.deepStrictEqual(snapshot.slashCommands, []); + assert.isUndefined(snapshot.optionDescriptors); + }).pipe(Effect.scoped), + ); }, ); From 25b71bab20b68438d0479bed3d3bff8539cd1c19 Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 12:19:44 +0100 Subject: [PATCH 4/7] Revert "test(opencode): cover stalled OpenCode 1 agent discovery" This reverts commit a978974eea8951c20bc7cc5980657c15100c6757. --- .../src/server/driver.test.ts | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 6cd163c3b2a70..20a01212bf882 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -1122,7 +1122,6 @@ const openCode1Provider = (id: string, models: ReadonlyArray) => ({ }); const openCode1Project = "/work/project-with-config"; const openCode1UnreadableProject = "/work/unreadable-project"; -const openCode1StalledAgents = "/work/stalled-agents"; const openCode1Providers = (directory: string) => { const all = [ openCode1Provider("opencode", ["big-pickle"]), @@ -1146,14 +1145,7 @@ const openCode1Runtime = { ? Promise.reject(new Error("provider list failed")) : Promise.resolve({ data: openCode1Providers(directory) }), }, - app: { - agents: (_input: unknown, options?: { readonly signal?: AbortSignal }) => - directory === openCode1StalledAgents - ? new Promise((_resolve, reject) => { - options?.signal?.addEventListener("abort", () => reject(new Error("aborted"))); - }) - : Promise.resolve({ data: [] }), - }, + app: { agents: () => Promise.resolve({ data: [] }) }, command: { list: () => Promise.resolve({ data: [] }) }, }), loadOpenCodeInventory: () => @@ -1191,24 +1183,6 @@ it.layer(Layer.mergeAll(layer, Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, op assert.strictEqual(unreadable.workspaceModelsPending, true); }).pipe(Effect.scoped), ); - - it.effect("keeps commands and skills when OpenCode 1 agent discovery stalls", () => - Effect.gen(function* () { - const instance = yield* create({}, noHttp); - const ready = yield* instance.snapshot.streamChanges.pipe( - Stream.filter((snapshot) => snapshot.status === "ready"), - Stream.take(1), - Stream.runCollect, - Effect.forkChild, - ); - yield* Fiber.join(ready); - - const snapshot = yield* instance.snapshotForCwd!(openCode1StalledAgents); - assert.deepStrictEqual(snapshot.skills, []); - assert.deepStrictEqual(snapshot.slashCommands, []); - assert.isUndefined(snapshot.optionDescriptors); - }).pipe(Effect.scoped), - ); }, ); From 88078d46596c8ac56d45697617b82c5f65495d56 Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 12:31:58 +0100 Subject: [PATCH 5/7] Reapply "test(opencode): cover stalled OpenCode 1 agent discovery" This reverts commit 25b71bab20b68438d0479bed3d3bff8539cd1c19. --- .../src/server/driver.test.ts | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 20a01212bf882..6cd163c3b2a70 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -1122,6 +1122,7 @@ const openCode1Provider = (id: string, models: ReadonlyArray) => ({ }); const openCode1Project = "/work/project-with-config"; const openCode1UnreadableProject = "/work/unreadable-project"; +const openCode1StalledAgents = "/work/stalled-agents"; const openCode1Providers = (directory: string) => { const all = [ openCode1Provider("opencode", ["big-pickle"]), @@ -1145,7 +1146,14 @@ const openCode1Runtime = { ? Promise.reject(new Error("provider list failed")) : Promise.resolve({ data: openCode1Providers(directory) }), }, - app: { agents: () => Promise.resolve({ data: [] }) }, + app: { + agents: (_input: unknown, options?: { readonly signal?: AbortSignal }) => + directory === openCode1StalledAgents + ? new Promise((_resolve, reject) => { + options?.signal?.addEventListener("abort", () => reject(new Error("aborted"))); + }) + : Promise.resolve({ data: [] }), + }, command: { list: () => Promise.resolve({ data: [] }) }, }), loadOpenCodeInventory: () => @@ -1183,6 +1191,24 @@ it.layer(Layer.mergeAll(layer, Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, op assert.strictEqual(unreadable.workspaceModelsPending, true); }).pipe(Effect.scoped), ); + + it.effect("keeps commands and skills when OpenCode 1 agent discovery stalls", () => + Effect.gen(function* () { + const instance = yield* create({}, noHttp); + const ready = yield* instance.snapshot.streamChanges.pipe( + Stream.filter((snapshot) => snapshot.status === "ready"), + Stream.take(1), + Stream.runCollect, + Effect.forkChild, + ); + yield* Fiber.join(ready); + + const snapshot = yield* instance.snapshotForCwd!(openCode1StalledAgents); + assert.deepStrictEqual(snapshot.skills, []); + assert.deepStrictEqual(snapshot.slashCommands, []); + assert.isUndefined(snapshot.optionDescriptors); + }).pipe(Effect.scoped), + ); }, ); From faa041a649218bcf7c0d59941248ab901a97a131 Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 12:33:19 +0100 Subject: [PATCH 6/7] test(opencode): advance TestClock for the stalled OpenCode 1 agent read it.effect runs on TestClock, so the three-second bound only fires after an explicit adjustment. Fork the snapshot request, advance the clock, then join. --- packages/provider-opencode/src/server/driver.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 6cd163c3b2a70..9cb14235d4d33 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -1203,7 +1203,11 @@ it.layer(Layer.mergeAll(layer, Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, op ); yield* Fiber.join(ready); - const snapshot = yield* instance.snapshotForCwd!(openCode1StalledAgents); + const snapshotFiber = yield* instance.snapshotForCwd!(openCode1StalledAgents).pipe( + Effect.forkChild, + ); + yield* TestClock.adjust("10 seconds"); + const snapshot = yield* Fiber.join(snapshotFiber); assert.deepStrictEqual(snapshot.skills, []); assert.deepStrictEqual(snapshot.slashCommands, []); assert.isUndefined(snapshot.optionDescriptors); From 1ba02eb7cfd132d614950d3dc967c6b80822ca43 Mon Sep 17 00:00:00 2001 From: Daniel SOARES Date: Thu, 12 Nov 2026 12:42:15 +0100 Subject: [PATCH 7/7] test(opencode): assert the built-in command survives a stalled v1 agent read --- packages/provider-opencode/src/server/driver.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/provider-opencode/src/server/driver.test.ts b/packages/provider-opencode/src/server/driver.test.ts index 9cb14235d4d33..78b46fc083309 100644 --- a/packages/provider-opencode/src/server/driver.test.ts +++ b/packages/provider-opencode/src/server/driver.test.ts @@ -1209,7 +1209,10 @@ it.layer(Layer.mergeAll(layer, Layer.succeed(OpenCodeRuntime.OpenCodeRuntime, op yield* TestClock.adjust("10 seconds"); const snapshot = yield* Fiber.join(snapshotFiber); assert.deepStrictEqual(snapshot.skills, []); - assert.deepStrictEqual(snapshot.slashCommands, []); + assert.include( + snapshot.slashCommands.map((command) => command.name), + "compact", + ); assert.isUndefined(snapshot.optionDescriptors); }).pipe(Effect.scoped), );