Skip to content

feat(server): workerd runtime profile and SDK workerd entrypoint - #41918

Merged
kitlangton merged 12 commits into
v2from
workerd-profile-sdk
Aug 12, 2026
Merged

feat(server): workerd runtime profile and SDK workerd entrypoint#41918
kitlangton merged 12 commits into
v2from
workerd-profile-sdk

Conversation

@kitlangton

@kitlangtonkitlangton commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What

Adds a workerd runtime profile so an OpenCode server can boot inside a Cloudflare Durable Object, plus the SDK entrypoint and a CI guard that proves it in a real isolate.

The destination is one OpenCode server per Durable Object — a Slack bot where each thread is a DO hosting a full server, with OpenCode's database being the DO's own SQLite and no request leaving the isolate. That app lives on a local branch; this PR is the upstream half it needs.

Four pieces:

PieceWhat it is
packages/server/src/workerd.tsThe profile: ServerWorkerd.create(options) returns the fetch handler for a DO's fetch(), with every intentionally-local service replaced
overrides threadingcreateRoutes / createEmbeddedRoutes / ServerFetch.make take runtime-profile replacements applied after the standard set, so later entries win
@opencode-ai/sdk-next/workerdThe embedded typed client over the workerd profile — same OpenCode.Interface, in-isolate transport
packages/workerd-spikeThe purity guard: boots core inside real workerd via @cloudflare/vitest-pool-workers

The profile

  • Database runs on the injected DurableObjectStorage SQLite (Database.layerFromClient over the merged workerd adapter).
  • Native modules (#pty, #fff, #photon-wasm, #shell-parser-wasm, #process-lock-ffi) resolve to inert stubs under the workerd bundle condition; loopback OAuth servers import node:http lazily; @effect/platform-node is deep-imported so the barrel's eager undici / node:sqlite side imports never load.
  • Watcher and fff are disabled through existing option flags; plugin discovery is precompiled-only (ConfigPluginSource.empty); MCP is remote-only.
  • Shell, FileSystem, FileSystemSearch, Pty fail with a clear defect until a remote sandbox backs them; Snapshot and Vcs degrade to empty results, matching their behavior for locations without a supported VCS.
  • Durable events are persisted unconditionally — that history is exactly what an evicted turn is recovered from, so a runtime that dies without teardown cannot opt out.
  • Global roots every path under one writable directory (tmp on workerd), and health reports pid: 0 where the runtime has no OS process identity.

The spike (the heart of this PR)

packages/workerd-spike runs the full stack inside a real Durable Object, all five tests green:

  1. Cold boot with all 42 migrations journaled on real DO SQLite, authed health 200 / unauthed 401.
  2. Session create over the HTTP API, row persisted.
  3. A complete prompt turn against a fetchMock'd OpenAI-compatible provider, read back through the durable session log cursor route.
  4. A turn that completes with no request in flight (prompt, return, sleep, read) — the ack-then-continue shape the Slack flow needs.
  5. Eviction mid-turn: DurableObjectState.abort() between prompt-accepted and turn-complete, then a fresh instance boots over the same storage, resumes the claimed execution, and replays the drain.

Test 5 is the interesting one. The durable log stays gapless across the isolate's death:

[1 session.input.admitted] [2 session.execution.started] [3 session.instructions.updated]
[4 session.input.promoted] ✗ EVICTED ✗ [5 session.synthetic] [6 session.execution.started]
[7 session.step.started] [8 session.text.started] [9 session.text.ended]
[10 session.step.ended] [11 session.execution.succeeded]

A consumer that checkpointed a cursor at seq 4 pre-eviction resumes at exactly 5 — no gaps, no duplicates. The second session.execution.started (seq 6) with no terminal event between is the replay signature a projection can key on.

How

The only new seam in shared code is the overrides parameter. ServerFetch.make stays eager — the layer builds in the caller's scope before the handler is returned, as merged in #41896; the profile does not reintroduce a lazy first-request build.

ServerWorkerd.create returns an Effect requiring a Scope. A Durable Object holds that scope for the instance's lifetime and never closes it, which is correct: a DO is evicted without teardown, and storage is durable.

Scope

Ported from a local seam branch validated months ago, re-adapted to current head. Several seam pieces turned out to be superseded by upstream work and were dropped:

One deliberate deviation: sdk-next keeps building its own routes rather than consuming ServerFetch.make. The embedded path needs the built context (for SdkPlugins registration and the logging context added since the seam) and uses password-less embedded auth, so routing it through ServerFetch.make would regress log capture and change auth semantics. It gains the same overrides + resumeSuspendedSessions hooks instead.

Testing

  • packages/workerd-spike: 5/5 pass in a real workerd isolate (vitest-pool-workers).
  • packages/server: 22 pass — the ServerFetch tests from feat(server): web-standard fetch handler entry #41896 stay green, plus a profile test over a bun:sqlite-backed fake DO storage.
  • packages/sdk-next: 13 pass. packages/core: 1681 pass.
  • Repo-wide bun typecheck: 32/32 green. check:generated clean for both client and www.
  • script/workerd-probe.ts (bun run probe:workerd): the graph bundles under the workerd condition with no bun builtins statically imported. The probe now distinguishes static from dynamic imports, so a lazily-loaded bun:sqlite behind a runtime guard (the v1 legacy-database reader) is reported as (lazy) rather than failing.

Two CI caveats, neither from this branch:

  • unit (windows) is broken at v2 head itself (three directoryAutocompleteSearch path-separator tests); a fix is in flight separately.
  • The spike is registered as a turbo test task so it actually runs in CI on both matrix legs — turbo test only runs the tasks declared in turbo.json, so without that entry the guard would never fire. It pins @cloudflare/vitest-pool-workers 0.12.6 (newer pool/workerd pairings segfault on macOS) with a patch extending the stale workerd builtin allowlist and fixing fallback-service handling of case-insensitive filesystems, /@fs ids, JSON requires, and unanchored module-rule globs. Worth watching its first run on the Linux and Windows runners; if the pool proves unreliable on Windows, the task can be scoped to Linux.

Resolve the native-module import conditions (#pty, #fff, #photon-wasm,
#shell-parser-wasm, #process-lock-ffi) to inert workerd stubs, so the module
graph loads in a runtime with no subprocesses, FFI, or filesystem artifacts.
Loopback OAuth servers import node:http lazily for the same reason, MCP gains
an stdio flag for runtimes that cannot spawn local servers, and Global roots
every path under one writable directory (tmp on workerd, OPENCODE_GLOBAL_ROOT
anywhere).
ServerWorkerd.create builds the fetch handler for a Durable Object's fetch(),
with every intentionally-local service replaced: the database runs on the
injected DO SQLite, plugin discovery is precompiled-only, MCP is remote-only,
Snapshot and Vcs degrade to empty results, and Shell/FileSystem/Pty fail with
a clear defect until a remote sandbox backs them.
Threading it through needs one seam: createRoutes and ServerFetch.make take
runtime-profile replacements applied after the standard set, so later entries
win. script/workerd-probe.ts pins that the graph bundles under the workerd
condition without bun builtins.
createEmbeddedRoutes accepts runtime-profile service replacements, and the
embedded SDK exposes them through EmbedOptions: overrides applied after the
standard set, plus an opt-in boot-time resume of Sessions whose execution
claim was never released, for runtimes that die without teardown.
@opencode-ai/sdk-next/workerd composes the workerd profile
(ServerWorkerd.serverOptions + replacements) with the embedded SDK, so a
Durable Object host gets the same typed client and event streams as any other
sdk-next consumer, over Durable Object SQLite, with no network hop.
Health reports pid 0 where the runtime has no OS process identity, and the
drizzle session delegates to the client's native withTransaction when the
client rejects BEGIN/SAVEPOINT (Durable Object SQLite). Node platform modules
are deep-imported so the barrel's eager undici and node:sqlite side imports
never load.
packages/workerd-spike runs the full opencode core + server stack inside a
real Durable Object via @cloudflare/vitest-pool-workers: boot with all 42
migrations journaled on real DO SQLite, session create over the HTTP API, a
complete prompt turn against a fetchMock'd OpenAI-compatible provider read
back through the durable session log cursor route, a turn that completes with
no request in flight, and recovery of a session evicted mid-turn.
Eviction is simulated with DurableObjectState.abort() between prompt-accepted
and turn-complete; a fresh instance boots over the same storage, resumes the
claimed execution, and replays the drain. The durable log stays gapless
across the death, so a consumer resuming from a pre-eviction cursor sees no
gaps and no duplicates.
The profile persists durable events because that history is what recovery
replays. Harness notes: pins pool 0.12.6 (newer pool/workerd pairings segfault
on macOS) with a patch extending the stale workerd builtin allowlist and
fixing fallback-service handling of case-insensitive filesystems, /@fs ids,
JSON requires, and unanchored module-rule globs; missing node builtins resolve
to unenv polyfills via vite aliases.
turbo only runs the test tasks declared here, so the spike would never guard
anything in CI without an entry. Registering it makes the purity check — core
booting inside a real workerd isolate — run on every push.
The workspace also contains vitest 4.x, and hoisting differs by platform: on
windows the pool loaded @vitest/utils 4.x against @vitest/pretty-format 3.2.7
and died on a missing export before any test ran. Declaring the 3.2.7 set
directly on this package makes resolution the same under either layout.
The patched pool's module fallback service handles /@fs ids with posix
assumptions, so Windows drive-letter paths (/@fs/C:/...) fall through and
raw-text modules fail to resolve before any test runs. The purity guard is
platform-independent — the bundle graph proven inside a Linux isolate is the
same graph everywhere — so the suite skips on win32 rather than teaching the
pinned pool about windows paths.
Review pass over the profile. fff.workerd.ts becomes the standard bind()
shim over the shared fff module instead of a hand-copied 119-line type
surface that had already drifted. The dead Options.paths and its
redundant Global override are deleted. The copy-pasted MCP clientInfo
block becomes a ServerOptions mcp.stdio capability flag handled by the
standard routes replacement. Database.configuredClient joins configured()
so the profile stops hand-assembling the node's Global dependency.
ServerFetch.make folds overrides into BootOptions so the embed seam has
one shape, and sdk-next's EmbedOptions aliases it. The drizzle session's
duck-typed transactionStatements check becomes the named
NativeTransactionSqlClient contract that sqlite.workerd satisfies. The
bundled models.dev snapshot is decoded and normalized once per isolate
instead of per runtime, which matters when one isolate hosts many
Durable Objects. Spike-test interceptor and log-read boilerplate
collapse into the existing helpers, and the vitest 3.2.7 pins get their
rationale in the config.
The workerd tmp-rooting is what the profile needs; an env override with
no consumer can return when something wants it.
…condition
Replaces the navigator.userAgent sniff in global.ts with a #global-roots
conditional import, the same mechanism the native-module stubs and
#runtime-import already use: the workerd bundle statically resolves the
tmp-rooted variant and every other runtime keeps the XDG computation,
with no runtime detection at module scope.
The rebase brings in the removal of the resumeSuspendedSessions flag
(the fetch entry resumes unconditionally now), so the profile and the
embedded SDK stop passing it and the SDK's conditional fork becomes
unconditional, matching every other runtime.
@kitlangton
kitlangton marked this pull request as ready for review August 12, 2026 18:07
@kitlangton
kitlangton merged commit a841fc2 into v2Aug 12, 2026
8 checks passed
@kitlangton
kitlangton deleted the workerd-profile-sdk branch August 12, 2026 18:07
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@kitlangton