Repository navigation
feat(sdk): seed host plugins before recovery and harden workerd defaults - #54219
Conversation
There was a problem hiding this comment.
The cold-boot problem is fixed. I ran the PR's new SDK tests and its server model test against the base: they fail there ("No model specified and no supported model is available") and pass with this change. The core, server and SDK tests nearest the change pass, and typecheck passes in all three packages.
Public API: adds plugins?: ReadonlyArray<Plugin> to OpenCode.CreateOptions (effect) and OpenCodeWorkerd.CreateOptions. The promise OpenCode.create({ plugins }) now registers plugins before session recovery instead of after. model.list, model.default and generate.text can now return ServiceUnavailableError (service: "plugin"), which their error schemas already declare, so the OpenAPI spec is unchanged. Other default behaviour changes: shells in workspace Locations no longer get the host process.env, and unconfigured Ollama/vLLM/LM Studio discovery on 127.0.0.1 is off in workspace Locations and on workerd. All of this is backwards compatible at the type level; the behaviour changes are covered below.
Concerns:
- 5-second plugin wait: a slow plugin activation now makes
model.list/model.defaultfail with a 503. I confirmed this with a plugin whose setup takes 7s: on the base the list comes back immediately; with this PR it fails after about 5s. That covers first-time plugin package installs and the reload after every config change, and clients like the TUI load the model list through this route. The base test was named "lists models without blocking on plugin initialization", so this reverses an earlier choice (inline comment). - Local provider discovery: turning discovery off in every workspace Location, and detecting workerd from an empty shell-parser asset, could break setups outside workerd (inline comment).
The new Ollama test sleeps 20ms and then checks that the provider is absent. That check would also pass if discovery simply hadn't finished yet, so it doesn't really prove the skip; checking that no request reaches 127.0.0.1 would.
| ) | ||
| } | ||
|
|
||
| export const awaitPlugins = Plugin.awaitActivation.pipe( |
There was a problem hiding this comment.
This timeout means any plugin activation slower than 5s makes model.list / model.default fail with a 503. Slow cases include a first-time npm plugin install, a reload after a config or SdkPlugins.Updated change, or a plugin doing network work in effect. Before, these calls returned straight away. I checked with a plugin that sleeps 7s: model.list now fails after about 5s with "Location plugins did not finish activating". The handlers that already wait for activation (vcs, integration, rpc, session prompt) wait with no time limit. Could this wait the same way, or return the current snapshot on timeout instead of failing? Clients like the TUI load the model list through this route.
| const source = { current: configured(yield* config.entries(), input.providerID, origin, suffix) } | ||
| const environment = yield* Effect.serviceOption(Environment.Service) | ||
| const location = yield* Effect.serviceOption(Location.Service) | ||
| const defaultOrigin = |
There was a problem hiding this comment.
Two concerns with this default:
workspaceID !== undefined: discovery from the host still works for remote workspaces, because the model calls are made from the host. So users who run Ollama/LM Studio next to the server and use a workspace silently lose their local models.shellParserWasm.bash === ""as the workerd check: this ties provider discovery to how the shell parser assets resolve (the same check is added intool/plugin/shell.ts).WorkerdProfilealready carriesreplacementsand config defaults.
Could the workerd-specific defaults (no local discovery, portable scanner) live in the workerd profile, and the workspace case be dropped or limited to environments that can't reach the host?
Why
Embedded workerd consumers using
@opencode/sdk/workerd/effecthad to register model plugins twice (inLayer.tapandinstances.configure) becauseEmbeddedHost.CreateOptionsandOpenCodeWorkerd.CreateOptionslackedpluginsand startedresumeSuspendedSessionsbeforeLayer.tapran. In addition,ModelHandlerandGenerateHandlerdid not wait forPlugin.awaitActivation, forcing callers to pollmodel.listorplugin.liston cold Location boots, and the workerd profile still required manual config overrides to disable127.0.0.1local-provider polling, enable the portable shell scanner, and scrub hostprocess.envkeys from remote workspace shells.What Changes
OpenCode.CreateOptions/OpenCodeWorkerd.CreateOptions(@opencode/sdk/effect,@opencode/sdk/workerd/effect)pluginsoption; host plugins registered inLayer.tapran afterresumeSuspendedSessionsand triggered a debouncedPubSubreload.plugins?: ReadonlyArray<Plugin>seedsSdkPlugins.ServiceinsideEmbeddedHost.createbeforeresumeSuspendedSessionsor any Location boots.ModelHandler(model.list,model.default) &GenerateHandler(generate.text)Model.Service/Generate.Servicewithout waiting for Location plugin activation, returning incomplete snapshots or failing on cold boots.Plugin.awaitActivationat the HTTP handler boundary with a 5-second timeout (ServiceUnavailableError({ service: "plugin" })).Shell.create(@opencode/core/shell)process.envwhenlocation.workspaceID !== undefined(sessionEnvironmentwasundefined).process.envfallback whenlocation.workspaceID !== undefined, isolating remote workspace shells by default.ShellTool&ShellParse.scan(@opencode/core)portabletofalseonchannel: "unknown"/"latest"even when#shell-parser-wasmresolved to empty workerd paths.portabletotruewhenshellParserWasm.bash === ""(parser-wasm.workerd.ts).createLocalProviderPlugin(ollama,vllm,lmstudio)127.0.0.1every 30s unlessbaseURL: "disabled:"was set in config.127.0.0.1discovery whenshellParserWasm.bash === "",location.workspaceID !== undefined, orEnvironment.ServiceusesEnvironmentUnavailable.spawner.sequenceDiagram participant App as Embedded Consumer participant Host as EmbeddedHost.create participant Sdk as SdkPlugins.Service participant Handler as ModelHandler / GenerateHandler participant Plugin as Plugin.Service participant Model as Model.Service App->>Host: create({ plugins: [modelPlugin] }) Host->>Sdk: register(modelPlugin) Host->>Host: fork resumeSuspendedSessions App->>Handler: model.default / generate.text (cold Location) Handler->>Plugin: awaitActivation (5s timeout) Note over Plugin,Model: modelPlugin setup calls ctx.model.list() Plugin->>Model: available() (direct, no self-deadlock) Plugin-->>Handler: ready Latch opens Handler->>Model: default() / generate.text() Handler-->>App: 200 OKPlugin Readiness Boundary
Plugin.awaitActivationis awaited at the HTTP handler boundary (ModelHandlerandGenerateHandler), never insideModel.ServiceorGenerate.Service. DuringPlugin.Service.activate,PluginHostwiresctx.model.list(),ctx.model.default(), andctx.generate.text()directly toModel.ServiceandGenerate.Servicewhile holding thereadyLatch; keeping the wait in the HTTP handlers ensures cold HTTP/SDK requests wait for plugin registration to settle without self-deadlocking plugins that inspect models duringsetup(ctx)/effect(ctx).Scope
Covers
pluginsseeding in@opencode/sdk, boundedPlugin.awaitActivationinModelHandlerandGenerateHandler, remote workspace shell environment isolation inShell.create, portable shell scanner fallback when WASM assets are empty, and skipping unconfigured127.0.0.1local-provider discovery when no local execution plane is present.Verification
packages/sdk/test/embedded.test.ts: verifies a delayed model plugin passed viaOpenCode.create({ plugins: [plugin] })andOpenCodeWorkerd.create({ plugins: [plugin] })that callsctx.model.list()inside its owneffect(ctx)resolves on the first coldopencode.model.defaultandopencode.generate.textcalls without polling or self-deadlock.packages/core/test/shell-retention.test.ts: verifiesShell.createin a workspace-backed Location (location.workspaceID !== undefined) does not inherit hostprocess.envkeys.packages/core/test/shell-parse.test.ts: verifiesShellParse.scandefaults to portable scanning when WASM assets are empty (parser-wasm.workerd.ts).packages/core/test/plugin/provider-ollama.test.ts: verifiesOllamaPluginskips unconfigured default127.0.0.1discovery when the Location usesEnvironmentUnavailable.spawner.Added by the opencode bot (AI-generated).
Public API: adds
pluginsto OpenCode/OpenCodeWorkerd effect CreateOptions; model.list/model.default/generate.text may now return 503 ServiceUnavailableError while plugins activate (backwards compatible types, behaviour change)