Skip to content

fix(httpapi): install Instance ALS for adapter Promise bridge#25417

Merged
kitlangton merged 1 commit intodevfrom
kit/fix-workspace-create-adapter-als
May 2, 2026
Merged

fix(httpapi): install Instance ALS for adapter Promise bridge#25417
kitlangton merged 1 commit intodevfrom
kit/fix-workspace-create-adapter-als

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

Summary

The TUI's "New Workspace → Worktree" creates a workspace via POST /experimental/workspace. On the new Effect HttpApi backend the request returned a 500 with the toast `Failed to create workspace: unknown error`. Schema validation passed (PR #25412), but the inner handler tripped on `InstanceState.context` failing with `No context found for instance`.

This PR fixes the underlying bridge-context issue and adds a regression test that reproduces the live failure path against the real built-in `WorktreeAdapter`.

Reproduction

```
POST /experimental/workspace
backend = effect-httpapi
http.response.status_code = 500
exception.type = instance
exception.message = No context found for instance
at use (src/util/local-context.ts:15)
at instance-state.ts:29
at Workspace.create (control-plane/workspace.ts:463)
```

Root Cause

The HttpApi instance-context middleware sets `InstanceRef` on the request fiber via `InstanceStore.provide`. That works for any code that stays inside Effect.

`Workspace.create` calls into the `WorkspaceAdapter` via `Effect.promise(() => adapter.configure(...))`. `WorktreeAdapter.configure` is async JS that does `await loadWorktree()` then `await AppRuntime.runPromise(Worktree.makeWorktreeInfo())`. By the time the nested `AppRuntime.runPromise` reaches `attach()`, the parent Effect fiber is suspended in JS-promise-land:

  • `Fiber.getCurrent()` returns `undefined` (no synchronous Effect fiber)
  • `Instance.current` (legacy ALS) was never installed for HttpApi requests
  • `InstanceRef` is unreachable across the async boundary

So `attach()` finds no instance context, the new fiber starts with `InstanceRef = undefined`, and `InstanceState.context` (which falls back to `Instance.current`) throws.

Node's `AsyncLocalStorage` does propagate across `async`/`await`. The fix is to install legacy `Instance.context`/`WorkspaceContext` ALS exactly at the Effect→JS bridge so any nested runtime call inherits it.

Fix

  • Add `EffectBridge.adapterPromise(fn)` and `EffectBridge.adapterTryPromise(fn)` in `packages/opencode/src/effect/bridge.ts`. Each reads the current `InstanceRef` and `WorkspaceRef`, then runs the JS callback inside the existing `restore(instance, workspace, fn)` so both `Instance.context` and `WorkspaceContext` ALS are set for the duration of the callback.
  • Replace every adapter Promise call site in `control-plane/workspace.ts` (`adapter.target`, `adapter.configure`, `adapter.create`, `adapter.remove`) with `EffectBridge.adapterPromise` / `adapterTryPromise`.
  • Replace the same inline pattern in `server/routes/instance/httpapi/middleware/workspace-routing.ts` `resolveTarget`.

This keeps all bridging in one helper alongside the existing `EffectBridge.make` plumbing and avoids the `WorkspaceRef` ALS gap that would have appeared if I had open-coded the helper twice.

Tests

  • New `packages/opencode/test/server/httpapi-workspace.test.ts > creates a real git worktree workspace via the builtin adapter` — drives the live failure path through the built-in `WorktreeAdapter`. Without this fix it returns 500 `No context found for instance`. With the fix it returns 200.
  • `bun typecheck` passes
  • `bun run test test/server/httpapi-workspace.test.ts test/control-plane/workspace.test.ts test/project/instance.test.ts test/effect/run-service.test.ts` — 57/57 pass
  • Full `bun test` — only the two pre-existing baseline failures (`project.initGit endpoint`, `session.created event`) remain

Why this approach (and not the alternatives)

  • I first tried wiring ALS install into `InstanceStore.provide` itself (so all HttpApi paths automatically install ALS). That reran the inner Effect on a detached fiber via `Effect.runPromiseExit`, which broke fiber scoping and timed out unrelated tests. Reverted.
  • Refactoring the `WorkspaceAdapter` interface to be Effect-native is a cleaner long-term fix but a larger surgery. Plugin-registered adapters expect the current Promise-based shape.
  • The surgical bridge installs ALS only at the existing Effect→JS boundary and keeps the adapter contract unchanged.

Followups

  • Consider Effectifying the `WorkspaceAdapter` contract so this bridge can disappear.
  • Consider moving more JS adapter call sites (worktree adapter create flow, workspace proxy) onto `EffectBridge.adapterPromise` so they all share the same ALS guarantees.

@kitlangton kitlangton force-pushed the kit/fix-workspace-create-adapter-als branch from 4b94fc2 to 51734c1 Compare May 2, 2026 14:42
@kitlangton kitlangton merged commit 5242a1c into dev May 2, 2026
9 checks passed
@kitlangton kitlangton deleted the kit/fix-workspace-create-adapter-als branch May 2, 2026 14:49
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