Skip to content

feat(sdk): seed host plugins before recovery and harden workerd defaults - #54219

Merged
kitlangton merged 2 commits into
v2from
workerd-sdk-defaults
Oct 10, 2026
Merged

kitlangton merged 2 commits into
v2from
workerd-sdk-defaults

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Why

Embedded workerd consumers using @opencode/sdk/workerd/effect had to register model plugins twice (in Layer.tap and instances.configure) because EmbeddedHost.CreateOptions and OpenCodeWorkerd.CreateOptions lacked plugins and started resumeSuspendedSessions before Layer.tap ran. In addition, ModelHandler and GenerateHandler did not wait for Plugin.awaitActivation, forcing callers to poll model.list or plugin.list on cold Location boots, and the workerd profile still required manual config overrides to disable 127.0.0.1 local-provider polling, enable the portable shell scanner, and scrub host process.env keys from remote workspace shells.

What Changes

Surface Before After
OpenCode.CreateOptions / OpenCodeWorkerd.CreateOptions (@opencode/sdk/effect, @opencode/sdk/workerd/effect) No plugins option; host plugins registered in Layer.tap ran after resumeSuspendedSessions and triggered a debounced PubSub reload. plugins?: ReadonlyArray<Plugin> seeds SdkPlugins.Service inside EmbeddedHost.create before resumeSuspendedSessions or any Location boots.
ModelHandler (model.list, model.default) & GenerateHandler (generate.text) Read Model.Service / Generate.Service without waiting for Location plugin activation, returning incomplete snapshots or failing on cold boots. Await Plugin.awaitActivation at the HTTP handler boundary with a 5-second timeout (ServiceUnavailableError({ service: "plugin" })).
Shell.create (@opencode/core/shell) Fell back to host process.env when location.workspaceID !== undefined (sessionEnvironment was undefined). Omits host process.env fallback when location.workspaceID !== undefined, isolating remote workspace shells by default.
ShellTool & ShellParse.scan (@opencode/core) Defaulted portable to false on channel: "unknown" / "latest" even when #shell-parser-wasm resolved to empty workerd paths. Default portable to true when shellParserWasm.bash === "" (parser-wasm.workerd.ts).
createLocalProviderPlugin (ollama, vllm, lmstudio) Polled 127.0.0.1 every 30s unless baseURL: "disabled:" was set in config. Skips unconfigured 127.0.0.1 discovery when shellParserWasm.bash === "", location.workspaceID !== undefined, or Environment.Service uses EnvironmentUnavailable.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 OK
Loading

Plugin Readiness Boundary

Plugin.awaitActivation is awaited at the HTTP handler boundary (ModelHandler and GenerateHandler), never inside Model.Service or Generate.Service. During Plugin.Service.activate, PluginHost wires ctx.model.list(), ctx.model.default(), and ctx.generate.text() directly to Model.Service and Generate.Service while holding the ready Latch; 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 during setup(ctx) / effect(ctx).

Scope

Covers plugins seeding in @opencode/sdk, bounded Plugin.awaitActivation in ModelHandler and GenerateHandler, remote workspace shell environment isolation in Shell.create, portable shell scanner fallback when WASM assets are empty, and skipping unconfigured 127.0.0.1 local-provider discovery when no local execution plane is present.

Verification

cd packages/core && bun typecheck && bun test test/shell-parse.test.ts test/shell-retention.test.ts test/plugin/provider-ollama.test.ts
cd packages/server && bun typecheck && bun run probe:workerd && bun test test/model.test.ts test/generate.test.ts test/workerd.test.ts
cd packages/sdk && bun typecheck && bun test test/embedded.test.ts -t "generate.text|seeds|OpenCodeWorkerd"
bun run check
  • packages/sdk/test/embedded.test.ts: verifies a delayed model plugin passed via OpenCode.create({ plugins: [plugin] }) and OpenCodeWorkerd.create({ plugins: [plugin] }) that calls ctx.model.list() inside its own effect(ctx) resolves on the first cold opencode.model.default and opencode.generate.text calls without polling or self-deadlock.
  • packages/core/test/shell-retention.test.ts: verifies Shell.create in a workspace-backed Location (location.workspaceID !== undefined) does not inherit host process.env keys.
  • packages/core/test/shell-parse.test.ts: verifies ShellParse.scan defaults to portable scanning when WASM assets are empty (parser-wasm.workerd.ts).
  • packages/core/test/plugin/provider-ollama.test.ts: verifies OllamaPlugin skips unconfigured default 127.0.0.1 discovery when the Location uses EnvironmentUnavailable.spawner.

Added by the opencode bot (AI-generated).

Public API: adds plugins to OpenCode/OpenCodeWorkerd effect CreateOptions; model.list/model.default/generate.text may now return 503 ServiceUnavailableError while plugins activate (backwards compatible types, behaviour change)

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 5-second plugin wait: a slow plugin activation now makes model.list / model.default fail 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).
  2. 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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in tool/plugin/shell.ts). WorkerdProfile already carries replacements and 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant