fix(cli): bridge Instance.current ALS in effectCmd handlers (regression from #25522)#25546
Merged
kitlangton merged 3 commits intodevfrom May 3, 2026
Merged
fix(cli): bridge Instance.current ALS in effectCmd handlers (regression from #25522)#25546kitlangton merged 3 commits intodevfrom
kitlangton merged 3 commits intodevfrom
Conversation
PR #25522 (the original effectCmd conversion of github.ts) introduced a latent breakage: any handler that yields `Effect.promise(async () => { ... await AppRuntime.runPromise(svc.method()) ... })` would lose the outer `InstanceRef` for the inner Effect run. Effect's fiber context propagates only through Effect's own scheduling. Inside an `async` callback after the first `await`, `Fiber.getCurrent()` returns undefined, so `attach()` in run-service.ts can't recover the outer fiber's `InstanceRef`. The inner Effect runs with `InstanceRef` defaulting to undefined, then `InstanceState.context` falls through to `Instance.current` ALS — which the new `effectCmd` was no longer setting (the legacy `bootstrap()` path used to via `WithInstance.provide`). End result: services that yield `InstanceState.context` (e.g. `Session.create`, `SessionShare.share`, `SessionPrompt.prompt`, `Agent.get`) would die with `LocalContext.NotFound` when invoked from inside an `Effect.promise(async)` block in any effectCmd handler. This broke `opencode github run`, which exercises exactly that pattern. Fix: in effectCmd, split load and body into two AppRuntime.runPromise calls so the body can run inside `Instance.restore(ctx, ...)`. Node ALS persists across awaits, so any nested AppRuntime.runPromise inside an async closure now recovers the instance via `attach()`'s ALS fallback. This restores the pre-#25522 `bootstrap()` behavior without giving up the Effect-context InstanceRef on the synchronous path. Adds a regression test that reproduces the bug (fails without the restore wrap, passes with it).
- Wrap the body run in try/finally so store.dispose(ctx) still fires if the JS-side `Instance.restore`/`pipe` chain throws synchronously before the inner runPromise constructs the effect. - Rewrite the regression test to use the existing `provideTestInstance` fixture (which already encodes the load → restore → dispose triple) instead of hand-rolling the noopBootstrap layer + InstanceStore plumbing.
66 tasks
oleksii-honchar
pushed a commit
to oleksii-honchar/better-opencode
that referenced
this pull request
May 6, 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 free
to 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.
Summary
Fixes a latent breakage introduced in PR #25522 (the original effectCmd conversion of github.ts). Any handler that does
yield* Effect.promise(async () => { ... await AppRuntime.runPromise(svc.method()) ... })loses the outerInstanceReffor the inner Effect run.Root cause
Effect's fiber context propagates only through Effect's own scheduling. Inside an
asynccallback after the firstawait,Fiber.getCurrent()returns undefined, soattach()inrun-service.tscan't recover the outer fiber'sInstanceRef. The inner Effect then runs withInstanceRefdefaulting toundefined, andInstanceState.contextfalls through toInstance.currentALS — which the neweffectCmdwas no longer setting (the legacybootstrap()path used to viaWithInstance.provide).End result: services that yield
InstanceState.context(e.g.Session.create,SessionShare.share,SessionPrompt.prompt,Agent.get) die withLocalContext.NotFoundwhen invoked from inside anEffect.promise(async)block in any effectCmd handler. This brokeopencode github run, which exercises exactly that pattern.The bug was caught by a review-agent audit of an unrelated stage-4 PR (#25539). Empirically confirmed with a focused test (see below).
Fix
In
effectCmd, split load and body into twoAppRuntime.runPromisecalls so the body can run insideInstance.restore(ctx, ...). Node'sAsyncLocalStoragepersists across awaits, so any nestedAppRuntime.runPromiseinside an async closure now recovers the instance viaattach()'s ALS fallback. Restores pre-#25522 behavior without giving up the Effect-contextInstanceRefon the synchronous path.Regression test
test/cli/effect-cmd-instance-als.test.tsreproduces the exact pattern: store.load → Effect.promise(async => { runPromise inside }) and asserts thatInstance.currentis reachable from the inner async closure. Replacing theInstance.restore(...)wrap with a bareEffect.runPromise(inner)causes the test to fail (manually verified) — proves the fix is load-bearing.Test plan
bun run typecheckcleanbun run test test/cli/effect-cmd-instance-als.test.ts— 1 pass; verified the test fails without the restore wrapbun run test test/project/instance.test.ts test/cli/— 148 pass