Uh oh!
There was an error while loading. Please reload this page.
feat(server): workerd runtime profile and SDK workerd entrypoint - #41918
Merged
Conversation
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.
kitlangtonforce-pushed
the
workerd-profile-sdk
branch
from
August 12, 2026 15:35
a92e658 to
bda0a25Comparekitlangton
marked this pull request as ready for review
August 12, 2026 18:07
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
packages/server/src/workerd.tsServerWorkerd.create(options)returns the fetch handler for a DO'sfetch(), with every intentionally-local service replacedcreateRoutes/createEmbeddedRoutes/ServerFetch.maketake runtime-profile replacements applied after the standard set, so later entries win@opencode-ai/sdk-next/workerdOpenCode.Interface, in-isolate transportpackages/workerd-spike@cloudflare/vitest-pool-workersThe profile
DurableObjectStorageSQLite (Database.layerFromClientover the merged workerd adapter).#pty,#fff,#photon-wasm,#shell-parser-wasm,#process-lock-ffi) resolve to inert stubs under theworkerdbundle condition; loopback OAuth servers importnode:httplazily;@effect/platform-nodeis deep-imported so the barrel's eagerundici/node:sqliteside imports never load.ConfigPluginSource.empty); MCP is remote-only.Globalroots every path under one writable directory (tmp on workerd), and health reportspid: 0where the runtime has no OS process identity.The spike (the heart of this PR)
packages/workerd-spikeruns the full stack inside a real Durable Object, all five tests green:fetchMock'd OpenAI-compatible provider, read back through the durable session log cursor route.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:
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
overridesparameter.ServerFetch.makestays 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.createreturns anEffectrequiring aScope. 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:
Database.layerFromClient.suspendOnStart→ merged (feat(core): discover AGENTS.md up to the home directory #41652, core: write-ahead execution claims replace shutdown-hook suspension #41800); the write-ahead claim is now unconditional, so the profile only opts into the boot-time resume.PluginSupervisor.configured({ dynamic: false })→ superseded byConfigPluginSource.empty.ModelsDevcontent/cacheoptions → superseded by the embedded snapshot + KV cache (c254ba8); head's models-dev is already workerd-safe (no flock, no disk).supportsForeignKeyToggle, migrationdefer_foreign_keys→ already in v2.toWebHandlerLayerWithfetch entry → superseded by the eagerServerFetch.makein feat(server): web-standard fetch handler entry #41896.One deliberate deviation:
sdk-nextkeeps building its own routes rather than consumingServerFetch.make. The embedded path needs the built context (forSdkPluginsregistration and the logging context added since the seam) and uses password-less embedded auth, so routing it throughServerFetch.makewould regress log capture and change auth semantics. It gains the sameoverrides+resumeSuspendedSessionshooks instead.Testing
packages/workerd-spike: 5/5 pass in a real workerd isolate (vitest-pool-workers).packages/server: 22 pass — theServerFetchtests from feat(server): web-standard fetch handler entry #41896 stay green, plus a profile test over abun:sqlite-backed fake DO storage.packages/sdk-next: 13 pass.packages/core: 1681 pass.bun typecheck: 32/32 green.check:generatedclean for both client and www.script/workerd-probe.ts(bun run probe:workerd): the graph bundles under theworkerdcondition with no bun builtins statically imported. The probe now distinguishes static from dynamic imports, so a lazily-loadedbun:sqlitebehind 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 (threedirectoryAutocompleteSearchpath-separator tests); a fix is in flight separately.testtask so it actually runs in CI on both matrix legs —turbo testonly runs the tasks declared inturbo.json, so without that entry the guard would never fire. It pins@cloudflare/vitest-pool-workers0.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,/@fsids, 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.