Skip to content

feat(sdk): configure session-selected instances - #46496

Merged
kitlangton merged 1 commit into
v2from
sdk-instances
Sep 2, 2026
Merged

feat(sdk): configure session-selected instances#46496
kitlangton merged 1 commit into
v2from
sdk-instances

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Why

An embedded application can already replace Core's Session-aware instance selector, but doing so requires Effect graph wiring. Host-wide plugin registration cannot give two application threads in the same directory different plugin stacks, and post-construction registration is too late to configure automatic restart recovery.

What Changes

Add instances: { key, configure } to the Promise, Effect, and Workerd SDK constructors:

const opencode = await OpenCode.create({
  database: { path: "./sessions.db" },
  instances: {
    key(session) {
      const threadID = session.metadata?.threadID
      if (typeof threadID !== "string") throw new Error("Session has no thread ID")
      return threadID
    },
    configure: async (threadID) => ({
      plugins: [slackPlugin(await threads.get(threadID))],
    }),
  },
})

threads and slackPlugin are application code. Effect callers return an Effect from configure instead.

Situation Behavior
Same application key and canonical Location Reuse one live instance, including concurrent acquisition.
Different key, directory, or workspace identity Build separate Location-bound instances.
Session metadata, message, inbox, and context reads Do not run configuration or plugin setup. Permission and form lists read instance services and acquire the instance, following Core's current handlers.
Persistent host restart Reconstruct callbacks before recovered execution uses them.
Configuration fails, an instance plugin ID collides with a host plugins entry, or a supplied plugin's setup fails Fail acquisition without admitting a new prompt or falling back; a later request can reconstruct.
No instances option Preserve default Location sharing.

Instance Lifetimes

The selector is a global Instance.node replacement over a scoped LayerMap. Successful instances remain cached until the host closes; configure runs on cache misses, not on every prompt. Server supplies its final replacement set so selected graphs share the correct host Database, Bus, and runtime-profile overrides rather than creating another host. Both instance caches use the existing canonical Location normalization.

Inside each selected instance, Instance.node and LocationServiceMap.node are bound to cheap borrowers of the selector and the host Location map, mirroring buildLocationServiceMap. Retaining the selector's own Layer from its entries would make host shutdown wait on those entries to release it first, and plugin cleanups would never run.

After construction, the selector waits on Plugin.awaitActivation and rejects supplied plugins reported failed. A host/instance plugin ID collision fails the whole generation before anything reaches the inventory, so it is rejected before construction instead. Failed lookup eviction runs in the selector's host Scope so cancelling the last borrower cannot leave the lookup waiting for itself.

Scope

This PR targets v2 directly and contains only the keyed SDK adapter, its Server wiring, the LocationServiceMap.canonical extraction, documentation, and SDK regressions. The former Core prerequisite #46615 was closed: #46639 removed PluginSupervisor.Service, and the collision it guarded is now handled here without new Core surface.

Independent ACP startup repair is #46613. The former SDK fixture maintenance #46614 was closed as superseded by #46639.

This does not share plugin activations across Locations. Selection is not an authorization or plugin-storage isolation boundary. No Protocol or HTTP resource contracts change.

Existing HTTP prompt middleware remains eager: a cold idempotent retry can reconstruct its instance before Core returns the original admission. Preparation and prompt hooks do not repeat. This PR preserves that eager capability acquisition on idempotent retries.

Verification

Using Bun 1.4.0:

# packages/sdk
bun typecheck
bun run ../core/script/test.ts test/instances.test.ts test/instances-effect.test.ts test/instances-lifecycle.test.ts --rerun-each 5
bun run ../core/script/test.ts
bun run verify:package

# packages/server
bun run ../core/script/test.ts

Rebased onto v2 at c8f81c8b83, after #46639 removed PluginSupervisor.Service, Instance.provideIfLoaded, and Instance.byLocationNode. SDK, Server, and Core typechecking passed. All eight instance-selection cases passed 40/40 repeated runs, including host/instance collision rejection and cleanup on host close. The full Server suite passed 48 tests with 3 skipped.

Full SDK run: 26 passed, 2 failed. Both failures reproduce 3/3 on a pristine v2 checkout at the same commit without this branch: embedded client exposes plugin-backed web search (Web search provider not found) and the cancellation assertion at test/promise.test.ts:73. Neither file is changed here.

Packed-consumer validation: all four entrypoints load, the Workerd bundle passes the no-Bun checks, and a real Miniflare consumer exercises selected-instance reuse and prompt hooks against injected Durable Object SQLite. Wrangler ran in dry-run mode; nothing was deployed.

@kitlangton
kitlangton changed the base branch from strict-plugin-readiness to v2 September 2, 2026 01:37
@kitlangton kitlangton closed this Sep 2, 2026
@kitlangton kitlangton reopened this Sep 2, 2026
@kitlangton
kitlangton merged commit 4d0eb97 into v2 Sep 2, 2026
12 checks passed
@kitlangton
kitlangton deleted the sdk-instances branch September 2, 2026 02:07
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