Преглед изворни кода

refactor(instance): remove legacy runtime fallback (#27757)

Shoubhit Dash пре 3 месеци
родитељ
комит
0c9cfe923f
47 измењених фајлова са 928 додато и 1245 уклоњено
  1. 3 12
      packages/opencode/AGENTS.md
  2. 0 3
      packages/opencode/specs/effect/facades.md
  3. 3 7
      packages/opencode/specs/effect/guide.md
  4. 9 305
      packages/opencode/specs/effect/instance-context.md
  5. 0 4
      packages/opencode/specs/effect/loose-ends.md
  6. 10 73
      packages/opencode/specs/effect/todo.md
  7. 12 4
      packages/opencode/src/acp/agent.ts
  8. 1 10
      packages/opencode/src/cli/effect-cmd.ts
  9. 36 42
      packages/opencode/src/control-plane/adapters/worktree.ts
  10. 16 5
      packages/opencode/src/control-plane/types.ts
  11. 76 47
      packages/opencode/src/control-plane/workspace.ts
  12. 18 38
      packages/opencode/src/effect/bridge.ts
  13. 4 16
      packages/opencode/src/effect/instance-state.ts
  14. 1 11
      packages/opencode/src/effect/run-service.ts
  15. 26 7
      packages/opencode/src/lsp/client.ts
  16. 1 0
      packages/opencode/src/lsp/lsp.ts
  17. 0 36
      packages/opencode/src/project/instance.ts
  18. 3 3
      packages/opencode/src/project/with-instance.ts
  19. 5 4
      packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts
  20. 5 5
      packages/opencode/src/tool/shell.ts
  21. 1 1
      packages/opencode/test/AGENTS.md
  22. 8 27
      packages/opencode/test/cli/effect-cmd-instance-als.test.ts
  23. 163 133
      packages/opencode/test/config/config.test.ts
  24. 15 2
      packages/opencode/test/control-plane/workspace.test.ts
  25. 0 1
      packages/opencode/test/effect/instance-state.test.ts
  26. 8 6
      packages/opencode/test/fixture/fixture.ts
  27. 24 13
      packages/opencode/test/lsp/client.test.ts
  28. 3 2
      packages/opencode/test/plugin/workspace-adapter.test.ts
  29. 2 21
      packages/opencode/test/project/instance.test.ts
  30. 5 2
      packages/opencode/test/project/worktree.test.ts
  31. 64 44
      packages/opencode/test/provider/amazon-bedrock.test.ts
  32. 0 1
      packages/opencode/test/provider/gitlab-duo.test.ts
  33. 242 218
      packages/opencode/test/provider/provider.test.ts
  34. 5 2
      packages/opencode/test/question/question.test.ts
  35. 6 3
      packages/opencode/test/server/httpapi-event.test.ts
  36. 0 3
      packages/opencode/test/server/httpapi-exercise/runtime.ts
  37. 0 1
      packages/opencode/test/server/httpapi-file.test.ts
  38. 0 1
      packages/opencode/test/server/httpapi-instance-context.test.ts
  39. 0 1
      packages/opencode/test/server/httpapi-pty.test.ts
  40. 0 1
      packages/opencode/test/server/httpapi-raw-route-auth.test.ts
  41. 6 3
      packages/opencode/test/server/httpapi-workspace.test.ts
  42. 146 122
      packages/opencode/test/session/llm.test.ts
  43. 1 1
      packages/opencode/test/tool/external-directory.test.ts
  44. 0 1
      packages/opencode/test/tool/lsp.test.ts
  45. 0 1
      packages/opencode/test/tool/read.test.ts
  46. 0 1
      packages/opencode/test/tool/skill.test.ts
  47. 0 1
      packages/opencode/test/tool/write.test.ts

+ 3 - 12
packages/opencode/AGENTS.md

@@ -128,17 +128,8 @@ See `specs/effect/migration.md` for the compact pattern reference and examples.
 
 Use `Effect.cached` when multiple concurrent callers should share a single in-flight computation rather than storing `Fiber | undefined` or `Promise | undefined` manually. See `specs/effect/migration.md` for the full pattern.
 
-## Instance.bind — ALS for native callbacks
+## Callback boundaries
 
-`Instance.bind(fn)` captures the current Instance AsyncLocalStorage context and restores it synchronously when called.
+Use `EffectBridge` for native or external callbacks (`@parcel/watcher`, `node-pty`, native `fs.watch`, plugin callbacks, etc.) that need to re-enter Effect services with instance/workspace context.
 
-Use it for native addon callbacks (`@parcel/watcher`, `node-pty`, native `fs.watch`, etc.) that need to call `Bus.publish` or anything that reads `Instance.directory`.
-
-You do not need it for `setTimeout`, `Promise.then`, `EventEmitter.on`, or Effect fibers.
-
-```typescript
-const cb = Instance.bind((err, evts) => {
-  Bus.publish(MyEvent, { ... })
-})
-nativeAddon.subscribe(dir, cb)
-```
+Plain async code should pass explicit context or stay inside an Effect fiber; do not add ambient instance context shims.

+ 0 - 3
packages/opencode/specs/effect/facades.md

@@ -6,7 +6,6 @@ Current status on this branch:
 
 - `src/` has 5 `makeRuntime(...)` call sites total.
 - 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`.
-- 1 is tracked primarily by the instance-context migration rather than facade removal: `src/project/instance.ts`.
 - That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`.
 
 Recent progress:
@@ -18,7 +17,6 @@ Recent progress:
 
 - `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`.
 - `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`.
-- `src/project/instance.ts` still uses a dedicated runtime for project boot, but that file is really part of the broader legacy instance-context transition tracked in `instance-context.md`.
 
 ## Completed Batches
 
@@ -192,7 +190,6 @@ Most of the original facade-removal backlog is already done. The practical remai
 
 1. remove the `Npm` runtime-backed facade from `src/npm/index.ts`
 2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts`
-3. keep `src/project/instance.ts` in the separate instance-context migration, not this checklist
 
 ## Checklist
 

+ 3 - 7
packages/opencode/specs/effect/guide.md

@@ -197,13 +197,9 @@ For background loops, use `Effect.repeat` or `Effect.schedule` with
 
 [`EffectBridge`](../../src/effect/bridge.ts) is the sanctioned helper for
 Promise/callback interop that needs to preserve instance/workspace context.
-Keep it, but reduce its dependency on legacy `Instance.current` /
-`Instance.restore` over time.
-
-`Instance.bind` / `Instance.restore` are transitional legacy tools. Use
-them only for native callbacks that still require legacy ALS context. Do
-not use them for `setTimeout`, `Promise.then`, `EventEmitter.on`, or
-Effect fibers.
+It preserves explicit `InstanceRef` / `WorkspaceRef` context for effects run
+through the bridge. Plain JS callbacks that need instance data should receive
+that data explicitly.
 
 ## Testing
 

+ 9 - 305
packages/opencode/specs/effect/instance-context.md

@@ -1,309 +1,13 @@
-# Instance context migration
+# Instance Context
 
-Practical plan for retiring the promise-backed / ALS-backed `Instance` helper in `src/project/instance.ts` and moving instance selection fully into Effect-provided scope.
+Instance selection is now Effect-provided context.
 
-## Goal
+Use these APIs:
 
-End state:
+- `InstanceRef` for the current project context.
+- `WorkspaceRef` for the current workspace id.
+- `InstanceState.context` / `InstanceState.directory` inside Effect services that require an instance.
+- `InstanceStore` at entry boundaries that need to load, reload, or dispose project contexts.
+- `EffectBridge` for native, plugin, or plain JavaScript callback boundaries that need to re-enter Effect with captured refs.
 
-- request, CLI, TUI, and tool entrypoints shift into an instance through Effect, not `Instance.provide(...)`
-- Effect code reads the current instance from `InstanceRef` or its eventual replacement, not from ALS-backed sync getters
-- per-directory boot, caching, and disposal are scoped Effect resources, not a module-level `Map<string, Promise<InstanceContext>>`
-- ALS remains only as a temporary bridge for native callback APIs that fire outside the Effect fiber tree
-
-## Current split
-
-Today `src/project/instance.ts` still owns two separate concerns:
-
-- ambient current-instance context through `LocalContext` / `AsyncLocalStorage`
-- per-directory boot and deduplication through `cache: Map<string, Promise<InstanceContext>>`
-
-At the same time, the Effect side already exists:
-
-- `src/effect/instance-ref.ts` provides `InstanceRef` and `WorkspaceRef`
-- `src/effect/run-service.ts` already attaches those refs when a runtime starts inside an active instance ALS context
-- `src/effect/instance-state.ts` already prefers `InstanceRef` and only falls back to ALS when needed
-
-That means the migration is not "invent instance context in Effect". The migration is "stop relying on the legacy helper as the primary source of truth".
-
-## End state shape
-
-Near-term target shape:
-
-```ts
-InstanceScope.with({ directory, workspaceID }, effect)
-```
-
-Responsibilities of `InstanceScope.with(...)`:
-
-- resolve `directory`, `project`, and `worktree`
-- acquire or reuse the scoped per-directory instance environment
-- provide `InstanceRef` and `WorkspaceRef`
-- run the caller's Effect inside that environment
-
-Code inside the boundary should then do one of these:
-
-```ts
-const ctx = yield * InstanceState.context
-const dir = yield * InstanceState.directory
-```
-
-Long-term, once `InstanceState` itself is replaced by keyed layers / `LayerMap`, those reads can move to an `InstanceContext` service without changing the outer migration order.
-
-## Migration phases
-
-### Phase 1: stop expanding the legacy surface
-
-Rules for all new code:
-
-- do not add new `Instance.directory`, `Instance.worktree`, `Instance.project`, or `Instance.current` reads inside Effect code
-- do not add new `Instance.provide(...)` boundaries unless there is no Effect-native seam yet
-- use `InstanceState.context`, `InstanceState.directory`, or an explicit `ctx` parameter inside Effect code
-
-Success condition:
-
-- the file inventory below only shrinks from here
-
-### Phase 2: remove direct sync getter reads from Effect services
-
-Convert Effect services first, before replacing the top-level boundary. These modules already run inside Effect and mostly need `yield* InstanceState.context` or a yielded `ctx` instead of ambient sync access.
-
-Primary batch, highest payoff:
-
-- `src/file/index.ts`
-- `src/lsp/server.ts`
-- `src/worktree/index.ts`
-- `src/file/watcher.ts`
-- `src/format/formatter.ts`
-- `src/session/index.ts`
-- `src/project/vcs.ts`
-
-Mechanical replacement rule:
-
-- `Instance.directory` -> `ctx.directory` or `yield* InstanceState.directory`
-- `Instance.worktree` -> `ctx.worktree`
-- `Instance.project` -> `ctx.project`
-
-Do not thread strings manually through every public method if the service already has access to Effect context.
-
-### Phase 3: convert entry boundaries to provide instance refs directly
-
-After the service bodies stop assuming ALS, move the top-level boundaries to shift into Effect explicitly.
-
-Main boundaries:
-
-- HTTP server middleware and experimental `HttpApi` entrypoints
-- CLI commands
-- TUI worker / attach / thread entrypoints
-- tool execution entrypoints
-
-These boundaries should become Effect-native wrappers that:
-
-- decode directory / workspace inputs
-- resolve the instance context once
-- provide `InstanceRef` and `WorkspaceRef`
-- run the requested Effect
-
-At that point `Instance.provide(...)` becomes a legacy adapter instead of the normal code path.
-
-### Phase 4: replace promise boot cache with scoped instance runtime
-
-Once boundaries and services both rely on Effect context, replace the module-level promise cache in `src/project/instance.ts`.
-
-Target replacement:
-
-- keyed scoped runtime or keyed layer acquisition for each directory
-- reuse via `ScopedCache`, `LayerMap`, or another keyed Effect resource manager
-- cleanup performed by scope finalizers instead of `disposeAll()` iterating a Promise map
-
-This phase should absorb the current responsibilities of:
-
-- `cache` in `src/project/instance.ts`
-- `boot(...)`
-- most of `disposeInstance(...)`
-- manual `reload(...)` / `disposeAll()` fan-out logic
-
-### Phase 5: shrink ALS to callback bridges only
-
-Keep ALS only where a library invokes callbacks outside the Effect fiber tree and we still need to call code that reads instance context synchronously.
-
-Known bridge cases today:
-
-- `src/file/watcher.ts`
-- `src/session/llm.ts`
-- some LSP and plugin callback paths
-
-If those libraries become fully wrapped in Effect services, the remaining `Instance.bind(...)` uses can disappear too.
-
-### Phase 6: delete the legacy sync API
-
-Only after earlier phases land:
-
-- remove broad use of `Instance.current`, `Instance.directory`, `Instance.worktree`, `Instance.project`
-- reduce `src/project/instance.ts` to a thin compatibility shim or delete it entirely
-- remove the ALS fallback from `InstanceState.context`
-
-## Inventory of direct legacy usage
-
-Direct legacy usage means any source file that still calls one of:
-
-- `Instance.current`
-- `Instance.directory`
-- `Instance.worktree`
-- `Instance.project`
-- `Instance.provide(...)`
-- `Instance.bind(...)`
-- `Instance.restore(...)`
-- `Instance.reload(...)`
-- `Instance.dispose()` / `Instance.disposeAll()`
-
-Current total: `56` files in `packages/opencode/src`.
-
-### Core bridge and plumbing
-
-These files define or adapt the current bridge. They should change last, after callers have moved.
-
-- `src/project/instance.ts`
-- `src/effect/run-service.ts`
-- `src/effect/instance-state.ts`
-- `src/project/bootstrap.ts`
-- `src/config/config.ts`
-
-Migration rule:
-
-- keep these as compatibility glue until the outer boundaries and inner services stop depending on ALS
-
-### HTTP and server boundaries
-
-These are the current request-entry seams that still create or consume instance context through the legacy helper.
-
-- `src/server/routes/instance/middleware.ts`
-- `src/server/routes/instance/index.ts`
-- `src/server/routes/instance/project.ts`
-- `src/server/routes/control/workspace.ts`
-- `src/server/routes/instance/file.ts`
-- `src/server/routes/instance/experimental.ts`
-- `src/server/routes/global.ts`
-
-Migration rule:
-
-- move these to explicit Effect entrypoints that provide `InstanceRef` / `WorkspaceRef`
-- do not move these first; first reduce the number of downstream handlers and services that still expect ambient ALS
-
-### CLI and TUI boundaries
-
-These commands still enter an instance through `Instance.provide(...)` or read sync getters directly.
-
-- `src/cli/bootstrap.ts`
-- `src/cli/cmd/agent.ts`
-- `src/cli/cmd/debug/agent.ts`
-- `src/cli/cmd/debug/ripgrep.ts`
-- `src/cli/cmd/github.ts`
-- `src/cli/cmd/import.ts`
-- `src/cli/cmd/mcp.ts`
-- `src/cli/cmd/models.ts`
-- `src/cli/cmd/plug.ts`
-- `src/cli/cmd/pr.ts`
-- `src/cli/cmd/providers.ts`
-- `src/cli/cmd/stats.ts`
-- `src/cli/cmd/tui/attach.ts`
-- `src/cli/cmd/tui/plugin/runtime.ts`
-- `src/cli/cmd/tui/thread.ts`
-- `src/cli/cmd/tui/worker.ts`
-
-Migration rule:
-
-- converge these on one shared `withInstance(...)` Effect entry helper instead of open-coded `Instance.provide(...)`
-- after that helper is proven, inline the legacy implementation behind an Effect-native scope provider
-
-### Tool boundary code
-
-These tools mostly use direct getters for path resolution and repo-relative display logic.
-
-- `src/tool/apply_patch.ts`
-- `src/tool/bash.ts`
-- `src/tool/edit.ts`
-- `src/tool/lsp.ts`
-- `src/tool/plan.ts`
-- `src/tool/read.ts`
-- `src/tool/write.ts`
-
-Migration rule:
-
-- expose the current instance as an explicit Effect dependency for tool execution
-- keep path logic local; avoid introducing another global singleton for tool state
-
-### Effect services still reading ambient instance state
-
-These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper.
-
-- `src/agent/agent.ts`
-- `src/cli/cmd/tui/config/tui-migrate.ts`
-- `src/file/index.ts`
-- `src/file/watcher.ts`
-- `src/format/formatter.ts`
-- `src/lsp/client.ts`
-- `src/lsp/index.ts`
-- `src/lsp/server.ts`
-- `src/mcp/index.ts`
-- `src/project/vcs.ts`
-- `src/provider/provider.ts`
-- `src/pty/index.ts`
-- `src/session/session.ts`
-- `src/session/instruction.ts`
-- `src/session/llm.ts`
-- `src/session/system.ts`
-- `src/sync/index.ts`
-- `src/worktree/index.ts`
-
-Migration rule:
-
-- replace direct getter reads with `yield* InstanceState.context` or a yielded `ctx`
-- isolate `Instance.bind(...)` callers and convert only the truly callback-driven edges to bridge mode
-
-### Highest-churn hotspots
-
-Current highest direct-usage counts by file:
-
-- `src/file/index.ts` - `18`
-- `src/lsp/server.ts` - `14`
-- `src/worktree/index.ts` - `12`
-- `src/file/watcher.ts` - `9`
-- `src/cli/cmd/mcp.ts` - `8`
-- `src/format/formatter.ts` - `8`
-- `src/tool/apply_patch.ts` - `8`
-- `src/cli/cmd/github.ts` - `7`
-
-These files should drive the first measurable burn-down.
-
-## Recommended implementation order
-
-1. Migrate direct getter reads inside Effect services, starting with `file`, `lsp`, `worktree`, `format`, and `session`.
-2. Add one shared Effect-native boundary helper for CLI / tool / HTTP entrypoints so we stop open-coding `Instance.provide(...)`.
-3. Move experimental `HttpApi` entrypoints to that helper so the new server stack proves the pattern.
-4. Convert remaining CLI and tool boundaries.
-5. Replace the promise cache with a keyed scoped runtime or keyed layer map.
-6. Delete ALS fallback paths once only callback bridges still depend on them.
-
-## Definition of done
-
-This migration is done when all of the following are true:
-
-- new requests and commands enter an instance by providing Effect context, not ALS
-- Effect services no longer read `Instance.directory`, `Instance.worktree`, `Instance.project`, or `Instance.current`
-- `Instance.provide(...)` is gone from normal request / CLI / tool execution
-- per-directory boot and disposal are handled by scoped Effect resources
-- `Instance.bind(...)` is either gone or confined to a tiny set of native callback adapters
-
-## Tracker and worktree
-
-Active tracker items:
-
-- `lh7l73` - overall `HttpApi` migration
-- `yobwlk` - remove direct `Instance.*` reads inside Effect services
-- `7irl1e` - replace `InstanceState` / legacy instance caching with keyed Effect layers
-
-Dedicated worktree for this transition:
-
-- path: `/Users/kit/code/open-source/opencode-worktrees/instance-effect-shift`
-- branch: `kit/instance-effect-shift`
+Do not add new ambient instance globals. Promise and callback boundaries should either stay in Effect, use `EffectBridge`, or pass the required context explicitly.

+ 0 - 4
packages/opencode/specs/effect/loose-ends.md

@@ -24,10 +24,6 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
 - [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
 - [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
 
-## Instance cleanup
-
-- [ ] `project/instance.ts` - keep shrinking the legacy ALS / Promise cache after the remaining `Instance.*` callers move over.
-
 ## Notes
 
 - Prefer small, semantics-preserving config migrations. Config precedence, legacy key migration, and plugin origin tracking are easy to break accidentally.

+ 10 - 73
packages/opencode/specs/effect/todo.md

@@ -64,13 +64,11 @@ P6  OA
   explicit and testable instead of mutable module state.
   Shrinks: [`global.ts`](../../../core/src/global.ts) import-time side
   effects, mutable `Global.Path` overrides, and its `Flag` dependency.
-- `INST` Instance shim — remove ambient `Instance` usage and old ALS
-  access patterns.
-  Shrinks: [`src/project/instance.ts`](../../src/project/instance.ts).
+- `INST` Instance context — keep project context explicit through Effect refs
+  and bridge boundaries.
 - `BRIDGE` Promise/callback interop — keep bridge helpers, but reduce
   legacy ALS coupling.
-  Shrinks: [`src/effect/bridge.ts`](../../src/effect/bridge.ts)
-  dependency on [`project/instance.ts`](../../src/project/instance.ts).
+  Shrinks: ad hoc Promise/callback re-entry code.
 - `PROC` AppProcess migration — prefer `AppProcess.Service` over raw
   process wrappers.
   Shrinks: direct spawn callsites and legacy process helpers.
@@ -221,74 +219,13 @@ Next PR candidates:
 
 ## P4: Instance And Bridge
 
-[`project/instance.ts`](../../src/project/instance.ts) is the deletion
-target. [`effect/bridge.ts`](../../src/effect/bridge.ts) is not a near-term
-deletion target; Promise/callback interop will continue to exist.
-
-Goal:
-
-- Keep a sanctioned bridge for Promise/callback boundaries.
-- Reduce bridge dependence on legacy `Instance.restore` / `Instance.current`.
-- Move callers toward `InstanceRef`, `WorkspaceRef`, `InstanceState`, or
-  explicit context where practical.
-- Delete `project/instance.ts` only after ambient Instance coupling is gone.
-
-Important distinction:
-
-- `InstanceState.context`, `InstanceState.directory`, and
-  `InstanceState.workspaceID` are acceptable inside normal Effect service
-  code when `InstanceRef` / `WorkspaceRef` are provided by the runtime.
-- The deletion blockers are the fallback and callback paths that rely on
-  ambient ALS: direct `Instance.*` reads, `InstanceState.bind(...)`,
-  `AppRuntime.runPromise(...)` re-entry from plain JS, and bridge restore
-  code that installs legacy ALS before invoking callbacks.
-
-Current bottom-up inventory from `dev`:
-
-- Direct `Instance.*` value readers:
-  [`tool/repo_overview.ts`](../../src/tool/repo_overview.ts),
-  [`control-plane/adapters/worktree.ts`](../../src/control-plane/adapters/worktree.ts),
-  [`cli/bootstrap.ts`](../../src/cli/bootstrap.ts).
-- `InstanceState.bind(...)` callback boundaries:
-  [`file/watcher.ts`](../../src/file/watcher.ts) native watcher callback,
-  [`storage/db.ts`](../../src/storage/db.ts) transaction/effect callbacks,
-  [`session/llm.ts`](../../src/session/llm.ts) workflow approval callback.
-- `AppRuntime.runPromise(...)` / re-entry from plain JS:
-  [`project/with-instance.ts`](../../src/project/with-instance.ts),
-  [`project/instance-runtime.ts`](../../src/project/instance-runtime.ts),
-  [`control-plane/adapters/worktree.ts`](../../src/control-plane/adapters/worktree.ts),
-  [`cli/effect-cmd.ts`](../../src/cli/effect-cmd.ts), plus global/non-instance
-  callsites such as CLI upgrade and ACP agent defaults.
-- Intentional bridge users to classify, not delete blindly:
-  workspace adapters in [`control-plane/workspace.ts`](../../src/control-plane/workspace.ts),
-  MCP, command execution, plugins, pty lifecycle, bus scope cleanup, task
-  cancellation, and HTTP lifecycle reload/dispose paths.
-- Core fallback layer to shrink last:
-  [`effect/run-service.ts`](../../src/effect/run-service.ts),
-  [`effect/bridge.ts`](../../src/effect/bridge.ts), and
-  [`effect/instance-state.ts`](../../src/effect/instance-state.ts).
-
-Recommended PR order:
-
-- [ ] `INST-1` Remove direct `Instance.*` value readers. Start with
-      `repo_overview`, `worktree` adapter, and `cli/bootstrap`; pass context
-      explicitly or obtain it from an Effect boundary.
-- [ ] `INST-2` Move type-only `InstanceContext` imports from
-      [`project/instance.ts`](../../src/project/instance.ts) to
-      [`project/instance-context.ts`](../../src/project/instance-context.ts).
-- [ ] `INST-3` Audit each `InstanceState.bind(...)` callback from the inside
-      out: list what the callback calls (`Bus.publish`, database effects,
-      permission/session services), then replace ambient capture with explicit
-      `InstanceRef` / `WorkspaceRef` provision or an `EffectBridge` call.
-- [ ] `INST-4` Classify `AppRuntime.runPromise(...)` callsites as global,
-      instance-scoped with explicit refs, or bridge-required. Eliminate the
-      instance-scoped callsites that rely on `run-service.attach()` falling
-      back to `Instance.current`.
-- [ ] `INST-5` After consumers are explicit, remove `Instance.current` fallback
-      from `InstanceState.context` and `run-service.attach()`.
-- [ ] `INST-6` Move any remaining `restore` / `bind` compatibility helpers to
-      the boundary that still needs them, then delete
-      [`project/instance.ts`](../../src/project/instance.ts).
+Instance context migration is complete for the legacy sync shim. Promise and callback interop continues through [`effect/bridge.ts`](../../src/effect/bridge.ts).
+
+Current rules:
+
+- Effect services read instance data from `InstanceRef`, `WorkspaceRef`, `InstanceState`, or explicit arguments.
+- Plain JavaScript callback boundaries use `EffectBridge` or explicit context arguments.
+- Runtime entrypoints must provide refs explicitly when they are instance-scoped.
 
 ## Lower Priority Tracks
 

+ 12 - 4
packages/opencode/src/acp/agent.ts

@@ -43,16 +43,25 @@ import { Provider } from "@/provider/provider"
 import { ModelID, ProviderID } from "../provider/schema"
 import { Agent as AgentModule } from "../agent/agent"
 import { AppRuntime } from "@/effect/app-runtime"
+import { InstanceRef } from "@/effect/instance-ref"
+import { InstanceRuntime } from "@/project/instance-runtime"
 import { Installation } from "@/installation"
 import { MessageV2 } from "@/session/message-v2"
 import { Config } from "@/config/config"
 import { ConfigMCP } from "@/config/mcp"
 import { Todo } from "@/session/todo"
-import { Result, Schema } from "effect"
+import { Effect, Result, Schema } from "effect"
 import { LoadAPIKeyError } from "ai"
 import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
 import { applyPatch } from "diff"
 import { InstallationVersion } from "@opencode-ai/core/installation/version"
+
+const defaultAgentInfo = async (directory: string) => {
+  const ctx = await InstanceRuntime.load({ directory })
+  return AppRuntime.runPromise(
+    AgentModule.Service.use((svc) => svc.defaultInfo()).pipe(Effect.provideService(InstanceRef, ctx)),
+  )
+}
 import { ShellID } from "@/tool/shell/id"
 
 type ModeOption = { id: string; name: string; description?: string }
@@ -1094,7 +1103,7 @@ export class Agent implements ACPAgent {
 
     const currentModeId = await (async () => {
       if (!availableModes.length) return undefined
-      const defaultAgent = await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultInfo()))
+      const defaultAgent = await defaultAgentInfo(directory)
       const resolvedModeId = availableModes.find((mode) => mode.name === defaultAgent.name)?.id ?? availableModes[0].id
       this.sessionManager.setMode(sessionId, resolvedModeId)
       return resolvedModeId
@@ -1328,8 +1337,7 @@ export class Agent implements ACPAgent {
     if (!current) {
       this.sessionManager.setModel(session.id, model)
     }
-    const agent =
-      session.modeId ?? (await AppRuntime.runPromise(AgentModule.Service.use((svc) => svc.defaultInfo()))).name
+    const agent = session.modeId ?? (await defaultAgentInfo(directory)).name
 
     const parts: Array<
       | { type: "text"; text: string; synthetic?: boolean; ignored?: boolean }

+ 1 - 10
packages/opencode/src/cli/effect-cmd.ts

@@ -3,7 +3,6 @@ import { Effect, Schema } from "effect"
 import { AppRuntime, type AppServices } from "@/effect/app-runtime"
 import { InstanceStore } from "@/project/instance-store"
 import { InstanceRef } from "@/effect/instance-ref"
-import { Instance } from "@/project/instance"
 import { cmd, type WithDoubleDash } from "./cmd/cmd"
 
 /**
@@ -83,19 +82,11 @@ export const effectCmd = <Args, A>(opts: EffectCmdOpts<Args, A>) =>
         return
       }
       const directory = opts.directory?.(args) ?? process.cwd()
-      // Two-phase: load ctx, then run body inside Instance.current ALS.
-      // Effect's InstanceRef is provided via fiber context, but that context is
-      // lost across `await` inside `Effect.promise(async () => ...)` callbacks
-      // — when handlers re-enter Effect via `AppRuntime.runPromise(svc.method())`
-      // there, attach() falls back to Instance.current ALS, which Node preserves
-      // across awaits. Matches the pre-effectCmd `bootstrap()` behavior.
       const { store, ctx } = await AppRuntime.runPromise(
         InstanceStore.Service.use((store) => store.load({ directory }).pipe(Effect.map((ctx) => ({ store, ctx })))),
       )
       try {
-        await Instance.restore(ctx, () =>
-          AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx))),
-        )
+        await AppRuntime.runPromise(opts.handler(args).pipe(Effect.provideService(InstanceRef, ctx)))
       } finally {
         await AppRuntime.runPromise(store.dispose(ctx))
       }

+ 36 - 42
packages/opencode/src/control-plane/adapters/worktree.ts

@@ -1,7 +1,6 @@
 import { Effect, Schema } from "effect"
 import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
-import { WorkspaceContext } from "../workspace-context"
-import { type WorkspaceAdapter, WorkspaceInfo } from "../types"
+import { type WorkspaceAdapter, type WorkspaceAdapterContext, WorkspaceInfo } from "../types"
 
 const WorktreeConfig = Schema.Struct({
   name: WorkspaceInfo.fields.name,
@@ -11,26 +10,31 @@ const WorktreeConfig = Schema.Struct({
 const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig)
 
 async function loadWorktree() {
-  const [{ AppRuntime }, { Instance }, { Worktree }] = await Promise.all([
+  const [{ AppRuntime }, { Worktree }] = await Promise.all([
     import("@/effect/app-runtime"),
-    import("@/project/instance"),
     import("@/worktree"),
   ])
-  return { AppRuntime, Instance, Worktree }
+  return { AppRuntime, Worktree }
 }
 
+function requireInstance(context: WorkspaceAdapterContext | undefined) {
+  if (!context?.instance) throw new Error("Worktree adapter requires an instance context")
+  return context.instance
+}
+
+const provideContext = <A, E, R>(effect: Effect.Effect<A, E, R>, context: WorkspaceAdapterContext | undefined) =>
+  effect.pipe(
+    Effect.provideService(InstanceRef, requireInstance(context)),
+    Effect.provideService(WorkspaceRef, context?.workspaceID),
+  )
+
 export const WorktreeAdapter: WorkspaceAdapter = {
   name: "Worktree",
   description: "Create a git worktree",
-  async configure(info) {
-    const { AppRuntime, Instance, Worktree } = await loadWorktree()
-    const ctx = Instance.current
-    const workspaceID = WorkspaceContext.workspaceID
+  async configure(info, context) {
+    const { AppRuntime, Worktree } = await loadWorktree()
     const next = await AppRuntime.runPromise(
-      Worktree.Service.use((svc) => svc.makeWorktreeInfo({ detached: true })).pipe(
-        Effect.provideService(InstanceRef, ctx),
-        Effect.provideService(WorkspaceRef, workspaceID),
-      ),
+      provideContext(Worktree.Service.use((svc) => svc.makeWorktreeInfo({ detached: true })), context),
     )
     return {
       ...info,
@@ -38,32 +42,27 @@ export const WorktreeAdapter: WorkspaceAdapter = {
       directory: next.directory,
     }
   },
-  async create(info) {
-    const { AppRuntime, Instance, Worktree } = await loadWorktree()
-    const ctx = Instance.current
-    const workspaceID = WorkspaceContext.workspaceID
+  async create(info, _env, _from, context) {
+    const { AppRuntime, Worktree } = await loadWorktree()
     const config = decodeWorktreeConfig(info)
     await AppRuntime.runPromise(
-      Worktree.Service.use((svc) =>
-        svc.createFromInfo({
-          name: config.name,
-          directory: config.directory,
-          ...(config.branch ? { branch: config.branch } : {}),
-        }),
-      ).pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID)),
+      provideContext(
+        Worktree.Service.use((svc) =>
+          svc.createFromInfo({
+            name: config.name,
+            directory: config.directory,
+            ...(config.branch ? { branch: config.branch } : {}),
+          }),
+        ),
+        context,
+      ),
     )
   },
-  async list() {
-    const { AppRuntime, Instance, Worktree } = await loadWorktree()
-    const ctx = Instance.current
-    const workspaceID = WorkspaceContext.workspaceID
+  async list(context) {
+    const { AppRuntime, Worktree } = await loadWorktree()
+    const ctx = requireInstance(context)
     return (
-      await AppRuntime.runPromise(
-        Worktree.Service.use((svc) => svc.list()).pipe(
-          Effect.provideService(InstanceRef, ctx),
-          Effect.provideService(WorkspaceRef, workspaceID),
-        ),
-      )
+      await AppRuntime.runPromise(provideContext(Worktree.Service.use((svc) => svc.list()), context))
     ).map((info) => ({
       type: "worktree",
       name: info.name,
@@ -72,16 +71,11 @@ export const WorktreeAdapter: WorkspaceAdapter = {
       projectID: ctx.project.id,
     }))
   },
-  async remove(info) {
-    const { AppRuntime, Instance, Worktree } = await loadWorktree()
-    const ctx = Instance.current
-    const workspaceID = WorkspaceContext.workspaceID
+  async remove(info, context) {
+    const { AppRuntime, Worktree } = await loadWorktree()
     const config = decodeWorktreeConfig(info)
     await AppRuntime.runPromise(
-      Worktree.Service.use((svc) => svc.remove({ directory: config.directory })).pipe(
-        Effect.provideService(InstanceRef, ctx),
-        Effect.provideService(WorkspaceRef, workspaceID),
-      ),
+      provideContext(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })), context),
     )
   },
   target(info) {

+ 16 - 5
packages/opencode/src/control-plane/types.ts

@@ -1,5 +1,6 @@
 import { Schema, Struct } from "effect"
 import { ProjectID } from "@/project/schema"
+import type { InstanceContext } from "@/project/instance-context"
 import { WorkspaceID } from "./schema"
 import type { DeepMutable } from "@opencode-ai/core/schema"
 
@@ -37,12 +38,22 @@ export type Target =
       headers?: HeadersInit
     }
 
+export type WorkspaceAdapterContext = {
+  readonly instance?: InstanceContext
+  readonly workspaceID?: WorkspaceID
+}
+
 export type WorkspaceAdapter = {
   name: string
   description: string
-  configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
-  create(info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo): Promise<void>
-  list?(): WorkspaceListedInfo[] | Promise<WorkspaceListedInfo[]>
-  remove(info: WorkspaceInfo): Promise<void>
-  target(info: WorkspaceInfo): Target | Promise<Target>
+  configure(info: WorkspaceInfo, context?: WorkspaceAdapterContext): WorkspaceInfo | Promise<WorkspaceInfo>
+  create(
+    info: WorkspaceInfo,
+    env: Record<string, string | undefined>,
+    from?: WorkspaceInfo,
+    context?: WorkspaceAdapterContext,
+  ): Promise<void>
+  list?(context?: WorkspaceAdapterContext): WorkspaceListedInfo[] | Promise<WorkspaceListedInfo[]>
+  remove(info: WorkspaceInfo, context?: WorkspaceAdapterContext): Promise<void>
+  target(info: WorkspaceInfo, context?: WorkspaceAdapterContext): Target | Promise<Target>
 }

+ 76 - 47
packages/opencode/src/control-plane/workspace.ts

@@ -26,8 +26,8 @@ import { SessionID } from "@/session/schema"
 import { NotFoundError } from "@/storage/storage"
 import { errorData } from "@/util/error"
 import { waitEvent } from "./util"
-import { WorkspaceContext } from "./workspace-context"
 import { EffectBridge } from "@/effect/bridge"
+import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
 import { Vcs } from "@/project/vcs"
 import { InstanceStore } from "@/project/instance-store"
 import { InstanceBootstrap } from "@/project/bootstrap"
@@ -196,6 +196,50 @@ export const layer = Layer.effect(
       })
     }
 
+    const adapterContext = Effect.gen(function* () {
+      return {
+        instance: yield* InstanceRef,
+        workspaceID: yield* WorkspaceRef,
+      }
+    })
+
+    const adapterTarget = (workspace: Info) =>
+      Effect.gen(function* () {
+        const adapter = getAdapter(workspace.projectID, workspace.type)
+        const context = yield* adapterContext
+        return yield* EffectBridge.fromPromise(() => adapter.target(workspace, context))
+      })
+
+    const adapterConfigure = (adapter: ReturnType<typeof getAdapter>, info: WorkspaceInfo) =>
+      Effect.gen(function* () {
+        const context = yield* adapterContext
+        return yield* EffectBridge.fromPromise(() => adapter.configure(info, context))
+      })
+
+    const adapterCreate = (
+      adapter: ReturnType<typeof getAdapter>,
+      info: WorkspaceInfo,
+      env: Record<string, string | undefined>,
+      from?: WorkspaceInfo,
+    ) =>
+      Effect.gen(function* () {
+        const context = yield* adapterContext
+        return yield* EffectBridge.fromPromise(() => adapter.create(info, env, from, context))
+      })
+
+    const adapterList = (adapter: ReturnType<typeof getAdapter>) =>
+      Effect.gen(function* () {
+        const context = yield* adapterContext
+        return yield* EffectBridge.fromPromise(() => Promise.resolve(adapter.list?.(context) ?? []))
+      })
+
+    const adapterRemove = (info: Info, type: string) =>
+      Effect.gen(function* () {
+        const adapter = getAdapter(info.projectID, type)
+        const context = yield* adapterContext
+        return yield* EffectBridge.fromPromise(() => adapter.remove(info, context))
+      })
+
     const connectSSE = Effect.fn("Workspace.connectSSE")(function* (
       url: URL | string,
       headers: HeadersInit | undefined,
@@ -281,8 +325,7 @@ export const layer = Layer.effect(
         const workspace = yield* get(input.workspaceID)
         if (!workspace) return input.fallback
 
-        const adapter = getAdapter(workspace.projectID, workspace.type)
-        const target = yield* EffectBridge.fromPromise(() => adapter.target(workspace))
+        const target = yield* adapterTarget(workspace)
 
         if (target.type === "local") {
           const store = yield* InstanceStore.Service
@@ -375,35 +418,27 @@ export const layer = Layer.effect(
         events: events.length,
       })
 
-      yield* Effect.promise(async () => {
-        await WorkspaceContext.provide({
-          workspaceID: space.id,
-          async fn() {
-            await Effect.runPromise(
-              Effect.forEach(
-                events,
-                (event) =>
-                  sync.replay(
-                    {
-                      id: event.id,
-                      aggregateID: event.aggregate_id,
-                      seq: event.seq,
-                      type: event.type,
-                      data: event.data,
-                    },
-                    { publish: true },
-                  ),
-                { discard: true },
-              ),
+      yield* Effect.forEach(
+        events,
+        (event) =>
+          sync
+            .replay(
+              {
+                id: event.id,
+                aggregateID: event.aggregate_id,
+                seq: event.seq,
+                type: event.type,
+                data: event.data,
+              },
+              { publish: true },
             )
-          },
-        })
-      })
+            .pipe(Effect.provideService(WorkspaceRef, space.id)),
+        { discard: true },
+      )
     })
 
     const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
-      const adapter = getAdapter(space.projectID, space.type)
-      const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
+      const target = yield* adapterTarget(space)
 
       if (target.type === "local") return
 
@@ -486,8 +521,7 @@ export const layer = Layer.effect(
     const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
       if (!flags.experimentalWorkspaces) return
 
-      const adapter = getAdapter(space.projectID, space.type)
-      const target = yield* EffectBridge.fromPromise(() => adapter.target(space)).pipe(
+      const target = yield* adapterTarget(space).pipe(
         Effect.catch((error) =>
           Effect.sync(() => {
             setStatus(space.id, "error")
@@ -538,15 +572,13 @@ export const layer = Layer.effect(
     const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
       const id = WorkspaceID.ascending(input.id)
       const adapter = getAdapter(input.projectID, input.type)
-      const config = yield* EffectBridge.fromPromise(() =>
-        adapter.configure({
-          ...input,
-          id,
-          name: Slug.create(),
-          directory: null,
-          extra: input.extra ?? null,
-        }),
-      )
+      const config = yield* adapterConfigure(adapter, {
+        ...input,
+        id,
+        name: Slug.create(),
+        directory: null,
+        extra: input.extra ?? null,
+      })
 
       const info: Info = {
         id,
@@ -583,7 +615,7 @@ export const layer = Layer.effect(
         OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
       }
 
-      yield* EffectBridge.fromPromise(() => adapter.create(config, env))
+      yield* adapterCreate(adapter, config, env)
       yield* Effect.all(
         [
           waitEvent({
@@ -622,8 +654,7 @@ export const layer = Layer.effect(
         if (current?.workspaceID) {
           const previous = yield* get(current.workspaceID)
           if (previous) {
-            const adapter = getAdapter(previous.projectID, previous.type)
-            const target = yield* EffectBridge.fromPromise(() => adapter.target(previous))
+            const target = yield* adapterTarget(previous)
 
             if (target.type === "remote") {
               yield* syncHistory(previous, target.url, target.headers).pipe(
@@ -701,8 +732,7 @@ export const layer = Layer.effect(
             workspaceID,
           })
 
-        const adapter = getAdapter(space.projectID, space.type)
-        const target = yield* EffectBridge.fromPromise(() => adapter.target(space))
+        const target = yield* adapterTarget(space)
 
         if (target.type === "local") {
           yield* sync.run(Session.Event.Updated, {
@@ -856,7 +886,7 @@ export const layer = Layer.effect(
         registeredAdapters(project.id),
         ([type, adapter]) =>
           adapter.list
-            ? EffectBridge.fromPromise(() => Promise.resolve(adapter.list?.() ?? [])).pipe(
+            ? adapterList(adapter).pipe(
                 Effect.catchCause((error) =>
                   Effect.sync(() => {
                     log.warn("workspace adapter list failed", { type, error })
@@ -937,8 +967,7 @@ export const layer = Layer.effect(
       const info = fromRow(row)
       yield* Effect.catchCause(
         Effect.gen(function* () {
-          const adapter = getAdapter(info.projectID, row.type)
-          yield* EffectBridge.fromPromise(() => adapter.remove(info))
+          yield* adapterRemove(info, row.type)
         }),
         () =>
           Effect.sync(() => {

+ 18 - 38
packages/opencode/src/effect/bridge.ts

@@ -1,9 +1,6 @@
 import { Context, Effect, Exit, Fiber } from "effect"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
-import { Instance } from "@/project/instance"
-import type { InstanceContext } from "@/project/instance-context"
 import type { WorkspaceID } from "@/control-plane/schema"
-import { LocalContext } from "@/util/local-context"
 import { InstanceRef, WorkspaceRef } from "./instance-ref"
 import { attachWith } from "./run-service"
 
@@ -14,27 +11,14 @@ export interface Shape {
   readonly bind: <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => (...args: Args) => Result
 }
 
-function restore<R>(instance: InstanceContext | undefined, workspace: WorkspaceID | undefined, fn: () => R): R {
-  if (instance && workspace !== undefined) {
-    return WorkspaceContext.restore(workspace, () => Instance.restore(instance, fn))
-  }
-  if (instance) return Instance.restore(instance, fn)
+function restoreWorkspace<R>(workspace: WorkspaceID | undefined, fn: () => R): R {
   if (workspace !== undefined) return WorkspaceContext.restore(workspace, fn)
   return fn()
 }
 
 function captureSync() {
   const fiber = Fiber.getCurrent()
-  const value = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
-  const instance =
-    value ??
-    (() => {
-      try {
-        return Instance.current
-      } catch (err) {
-        if (!(err instanceof LocalContext.NotFound)) throw err
-      }
-    })()
+  const instance = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
   const workspace =
     (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined) ?? WorkspaceContext.workspaceID
   return { instance, workspace }
@@ -42,47 +26,43 @@ function captureSync() {
 
 export const bind = <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) => {
   const captured = captureSync()
-  return (...args: Args) => restore(captured.instance, captured.workspace, () => fn(...args))
+  return (...args: Args) =>
+    restoreWorkspace(captured.workspace, () =>
+      Effect.runSync(attachWith(Effect.sync(() => fn(...args)), captured)),
+    )
 }
 
 /**
- * Bridge from Effect into a Promise-returning JS callback while installing
- * legacy `Instance.context` and `WorkspaceContext` AsyncLocalStorage for
- * the duration of the callback. Effect's `InstanceRef`/`WorkspaceRef` do
- * not propagate across async/await boundaries inside `Effect.promise(() =>
- * async fn)` callbacks that re-enter Effect via `AppRuntime.runPromise`,
- * but Node's AsyncLocalStorage does. Use this whenever an Effect crosses
- * into JS that may itself spawn new Effect runtimes (workspace adapters,
- * legacy plugins, etc.).
+ * Bridge from Effect into a Promise-returning JS callback while preserving
+ * `WorkspaceContext` AsyncLocalStorage for callback code that still reads it.
+ * `InstanceRef` is captured for effects run through the returned bridge APIs;
+ * plain JS callbacks that need it should receive the ref explicitly.
  *
- * Mirrors `Effect.promise` but restores legacy ALS first.
+ * Mirrors `Effect.promise` but restores workspace ALS first.
  */
 export const fromPromise = <T>(fn: () => Promise<T> | T): Effect.Effect<T> =>
   Effect.gen(function* () {
-    const instance = yield* InstanceRef
     const workspace = yield* WorkspaceRef
-    return yield* Effect.promise(() => Promise.resolve(restore(instance, workspace, () => fn())))
+    return yield* Effect.promise(() => Promise.resolve(restoreWorkspace(workspace, () => fn())))
   })
 
 export function make(): Effect.Effect<Shape> {
   return Effect.gen(function* () {
     const ctx = yield* Effect.context()
-    const value = yield* InstanceRef
     const captured = captureSync()
-    const instance = value ?? captured.instance
+    const instance = (yield* InstanceRef) ?? captured.instance
     const workspace = (yield* WorkspaceRef) ?? captured.workspace
-    const attach = <A, E, R>(effect: Effect.Effect<A, E, R>) => attachWith(effect, { instance, workspace })
     const wrap = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
-      attach(effect).pipe(Effect.provide(ctx)) as Effect.Effect<A, E, never>
+      attachWith(effect.pipe(Effect.provide(ctx)) as Effect.Effect<A, E, never>, { instance, workspace })
 
     return {
       promise: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
-        restore(instance, workspace, () => Effect.runPromise(wrap(effect))),
+        restoreWorkspace(workspace, () => Effect.runPromise(wrap(effect))),
       fork: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
-        restore(instance, workspace, () => Effect.runFork(wrap(effect))),
+        restoreWorkspace(workspace, () => Effect.runFork(wrap(effect))),
       run: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
         Effect.callback<A, E>((resume) => {
-          restore(instance, workspace, () =>
+          restoreWorkspace(workspace, () =>
             Effect.runPromiseExit(wrap(effect)).then((exit) =>
               resume(Exit.isSuccess(exit) ? Effect.succeed(exit.value) : Effect.failCause(exit.cause)),
             ),
@@ -91,7 +71,7 @@ export function make(): Effect.Effect<Shape> {
       bind:
         <Args extends readonly unknown[], Result>(fn: (...args: Args) => Result) =>
         (...args: Args) =>
-          restore(instance, workspace, () => fn(...args)),
+          restoreWorkspace(workspace, () => Effect.runSync(wrap(Effect.sync(() => fn(...args))))),
     } satisfies Shape
   })
 }

+ 4 - 16
packages/opencode/src/effect/instance-state.ts

@@ -1,8 +1,6 @@
-import { Effect, Fiber, ScopedCache, Scope, Context } from "effect"
+import { Effect, ScopedCache, Scope } from "effect"
 import * as EffectLogger from "@opencode-ai/core/effect/logger"
-import { Instance } from "@/project/instance"
 import type { InstanceContext } from "@/project/instance-context"
-import { LocalContext } from "@/util/local-context"
 import { InstanceRef, WorkspaceRef } from "./instance-ref"
 import { registerDisposer } from "./instance-registry"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -14,20 +12,10 @@ export interface InstanceState<A, E = never, R = never> {
   readonly cache: ScopedCache.ScopedCache<string, A, E, R>
 }
 
-export const bind = <F extends (...args: any[]) => any>(fn: F): F => {
-  try {
-    return Instance.bind(fn)
-  } catch (err) {
-    if (!(err instanceof LocalContext.NotFound)) throw err
-  }
-  const fiber = Fiber.getCurrent()
-  const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
-  if (!ctx) return fn
-  return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
-}
-
 export const context = Effect.gen(function* () {
-  return (yield* InstanceRef) ?? Instance.current
+  const ctx = yield* InstanceRef
+  if (!ctx) return yield* Effect.die(new Error("InstanceRef not provided"))
+  return ctx
 })
 
 export const workspaceID = Effect.gen(function* () {

+ 1 - 11
packages/opencode/src/effect/run-service.ts

@@ -1,7 +1,5 @@
 import { Effect, Fiber, Layer, ManagedRuntime } from "effect"
 import * as Context from "effect/Context"
-import { Instance } from "@/project/instance"
-import { LocalContext } from "@/util/local-context"
 import { InstanceRef, WorkspaceRef } from "./instance-ref"
 import * as Observability from "@opencode-ai/core/effect/observability"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -25,17 +23,9 @@ export function attachWith<A, E, R>(effect: Effect.Effect<A, E, R>, refs: Refs):
 
 export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> {
   const workspace = WorkspaceContext.workspaceID
-  const instance = (() => {
-    try {
-      return Instance.current
-    } catch (err) {
-      if (!(err instanceof LocalContext.NotFound)) throw err
-    }
-  })()
-  if (instance && workspace !== undefined) return attachWith(effect, { instance, workspace })
   const fiber = Fiber.getCurrent()
   return attachWith(effect, {
-    instance: instance ?? (fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined),
+    instance: fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined,
     workspace: workspace ?? (fiber ? Context.getReferenceUnsafe(fiber.context, WorkspaceRef) : undefined),
   })
 }

+ 26 - 7
packages/opencode/src/lsp/client.ts

@@ -7,10 +7,13 @@ import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types
 import * as Log from "@opencode-ai/core/util/log"
 import { Process } from "@/util/process"
 import { LANGUAGE_EXTENSIONS } from "./language"
-import { Schema } from "effect"
+import { Effect, Schema } from "effect"
 import type * as LSPServer from "./server"
 import { withTimeout } from "../util/timeout"
 import { Filesystem } from "@/util/filesystem"
+import { InstanceRef } from "@/effect/instance-ref"
+import { makeRuntime } from "@/effect/run-service"
+import { context, type InstanceContext } from "@/project/instance-context"
 
 const DIAGNOSTICS_DEBOUNCE_MS = 150
 const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5_000
@@ -25,6 +28,7 @@ const FILE_CHANGE_CHANGED = 2
 const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2
 
 const log = Log.create({ service: "lsp.client" })
+const busRuntime = makeRuntime(Bus.Service, Bus.layer)
 
 export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
 
@@ -134,9 +138,16 @@ function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
   return serverID === "typescript"
 }
 
-export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; directory: string }) {
+export async function create(input: {
+  serverID: string
+  server: LSPServer.Handle
+  root: string
+  directory: string
+  instance?: InstanceContext
+}) {
   const logger = log.clone().tag("serverID", input.serverID)
   logger.info("starting client")
+  const instance = input.instance ?? context.use()
 
   const connection = createMessageConnection(
     new StreamMessageReader(input.server.process.stdout as any),
@@ -162,7 +173,11 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
     dedupeDiagnostics([...(pushDiagnostics.get(filePath) ?? []), ...(pullDiagnostics.get(filePath) ?? [])])
   const updatePushDiagnostics = (filePath: string, next: Diagnostic[]) => {
     pushDiagnostics.set(filePath, next)
-    Bus.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID })
+    void busRuntime.runPromise((svc) =>
+      svc.publish(Event.Diagnostics, { path: filePath, serverID: input.serverID }).pipe(
+        Effect.provideService(InstanceRef, instance),
+      ),
+    )
   }
   const updatePullDiagnostics = (filePath: string, next: Diagnostic[]) => {
     pullDiagnostics.set(filePath, next)
@@ -510,10 +525,14 @@ export async function create(input: { serverID: string; server: LSPServer.Handle
       }
 
       timeoutTimer = setTimeout(() => finish(false), request.timeout)
-      unsub = Bus.subscribe(Event.Diagnostics, (event) => {
-        if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
-        schedule()
-      })
+      unsub = busRuntime.runSync((svc) =>
+        svc
+          .subscribeCallback(Event.Diagnostics, (event) => {
+            if (event.properties.path !== request.path || event.properties.serverID !== input.serverID) return
+            schedule()
+          })
+          .pipe(Effect.provideService(InstanceRef, instance)),
+      )
       schedule()
     })
   }

+ 1 - 0
packages/opencode/src/lsp/lsp.ts

@@ -237,6 +237,7 @@ export const layer = Layer.effect(
             server: handle,
             root,
             directory: ctx.directory,
+            instance: ctx,
           }).catch(async (err) => {
             s.broken.add(key)
             await Process.stop(handle.process)

+ 0 - 36
packages/opencode/src/project/instance.ts

@@ -1,36 +0,0 @@
-import { context, type InstanceContext } from "./instance-context"
-
-export type { InstanceContext } from "./instance-context"
-
-export const Instance = {
-  get current() {
-    return context.use()
-  },
-  get directory() {
-    return context.use().directory
-  },
-  get worktree() {
-    return context.use().worktree
-  },
-  get project() {
-    return context.use().project
-  },
-
-  /**
-   * Captures the current instance ALS context and returns a wrapper that
-   * restores it when called. Use this for callbacks that fire outside the
-   * instance async context (native addons, event emitters, timers, etc.).
-   */
-  bind<F extends (...args: any[]) => any>(fn: F): F {
-    const ctx = context.use()
-    return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F
-  },
-  /**
-   * Run a synchronous function within the given instance context ALS.
-   * Use this to bridge from Effect (where InstanceRef carries context)
-   * back to sync code that reads Instance.directory from ALS.
-   */
-  restore<R>(ctx: InstanceContext, fn: () => R): R {
-    return context.provide(ctx, fn)
-  },
-}

+ 3 - 3
packages/opencode/src/project/with-instance.ts

@@ -1,12 +1,12 @@
 import { AppRuntime } from "@/effect/app-runtime"
-import { context } from "./instance-context"
+import type { InstanceContext } from "./instance-context"
 import { InstanceStore } from "./instance-store"
 
-export async function provide<R>(input: { directory: string; fn: () => R }): Promise<R> {
+export async function provide<R>(input: { directory: string; fn: (ctx: InstanceContext) => R }): Promise<R> {
   const ctx = await AppRuntime.runPromise(
     InstanceStore.Service.use((store) => store.load({ directory: input.directory })),
   )
-  return context.provide(ctx, () => input.fn())
+  return input.fn(ctx)
 }
 
 export * as WithInstance from "./with-instance"

+ 5 - 4
packages/opencode/src/server/routes/instance/httpapi/middleware/instance-context.ts

@@ -1,4 +1,4 @@
-import { WorkspaceRef } from "@/effect/instance-ref"
+import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
 import { InstanceStore } from "@/project/instance-store"
 import { Effect, Layer } from "effect"
 import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
@@ -26,9 +26,10 @@ function provideInstanceContext<E>(
 ): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
   return Effect.gen(function* () {
     const route = yield* WorkspaceRouteContext
-    return yield* store.provide(
-      { directory: decode(route.directory) },
-      effect.pipe(Effect.provideService(WorkspaceRef, route.workspaceID)),
+    const ctx = yield* store.load({ directory: decode(route.directory) })
+    return yield* effect.pipe(
+      Effect.provideService(InstanceRef, ctx),
+      Effect.provideService(WorkspaceRef, route.workspaceID),
     )
   })
 }

+ 5 - 5
packages/opencode/src/tool/shell.ts

@@ -609,10 +609,10 @@ export const ShellTool = Tool.define(
           parameters: prompt.parameters,
           execute: (params: Parameters, ctx: Tool.Context) =>
             Effect.gen(function* () {
-              const executeInstance = yield* InstanceState.context
+              const instanceCtx = yield* InstanceState.context
               const cwd = params.workdir
-                ? yield* resolvePath(params.workdir, executeInstance.directory, shell)
-                : executeInstance.directory
+                ? yield* resolvePath(params.workdir, instanceCtx.directory, shell)
+                : instanceCtx.directory
               if (params.timeout !== undefined && params.timeout < 0) {
                 throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`)
               }
@@ -623,8 +623,8 @@ export const ShellTool = Tool.define(
                   const tree = yield* Effect.acquireRelease(parse(params.command, ps), (tree) =>
                     Effect.sync(() => tree.delete()),
                   )
-                  const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
-                  if (!containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
+                  const scan = yield* collect(tree.rootNode, cwd, ps, shell, instanceCtx)
+                  if (!containsPath(cwd, instanceCtx)) scan.dirs.add(cwd)
                   yield* ask(ctx, scan)
                 }),
               )

+ 1 - 1
packages/opencode/test/AGENTS.md

@@ -116,7 +116,7 @@ describe("my service", () => {
 Prefer the Effect-aware helpers from `fixture/fixture.ts` instead of building a manual runtime in each test.
 
 - `tmpdirScoped(options?)` creates a scoped temp directory and cleans it up when the Effect scope closes.
-- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it just runs an Effect with `Instance.current` bound to `dir`.
+- `provideInstance(dir)(effect)` is the low-level helper. It does not create a directory; it runs an Effect with `InstanceRef` provided for `dir`.
 - `provideTmpdirInstance((dir) => effect, options?)` is the convenience helper. It creates a temp directory, binds it as the active instance, and disposes the instance on cleanup.
 - `provideTmpdirServer((input) => effect, options?)` does the same, but also provides the test LLM server.
 

+ 8 - 27
packages/opencode/test/cli/effect-cmd-instance-als.test.ts

@@ -3,7 +3,6 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { Effect } from "effect"
 import { fileURLToPath } from "url"
 import { InstanceRef } from "../../src/effect/instance-ref"
-import { Instance } from "../../src/project/instance"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
@@ -13,46 +12,28 @@ afterEach(async () => {
   await disposeAllInstances()
 })
 
-// Regression for PR #25522: when an effectCmd handler does
-// `yield* Effect.promise(async () => { ... await runPromise(svcMethod) ... })`,
-// the inner runPromise creates a fresh fiber after `await` whose Effect context
-// has lost the outer InstanceRef. Services that read `InstanceState.context`
-// then fall back to `Instance.current` ALS, which must be installed at the JS
-// callback boundary (Node ALS persists across awaits, Effect's fiber context
-// does not). `it.instance` provides the loaded InstanceRef; the explicit
-// Instance.restore mirrors effectCmd's load + ALS-restore wrap.
-// Pins effect-cmd.ts directly: the pattern test below exercises the load +
-// Instance.restore boundary via the shared `it.instance` fixture,
-// so a regression that removed `Instance.restore` from effect-cmd.ts wouldn't
-// fail it. This grep guards the actual production callsite.
-it.live("effect-cmd.ts wraps the handler body in Instance.restore", () =>
+it.live("effect-cmd.ts does not restore legacy instance ALS", () =>
   Effect.gen(function* () {
     const fs = yield* AppFileSystem.Service
     const source = yield* fs.readFileString(fileURLToPath(new URL("../../src/cli/effect-cmd.ts", import.meta.url)))
-    expect(source).toContain("Instance.restore(ctx")
+    expect(source).not.toContain("restore(ctx")
   }),
 )
 
 it.instance(
-  "Instance.current reachable after await inside restored Effect.promise(async)",
+  "InstanceRef remains the handler context across Effect promise awaits",
   () =>
     Effect.gen(function* () {
       const test = yield* TestInstance
       const ctx = yield* InstanceRef
       if (!ctx) throw new Error("InstanceRef not provided")
 
-      const current = yield* Effect.promise(() =>
-        Instance.restore(ctx, async () => {
-          await Promise.resolve()
-          try {
-            return Instance.current
-          } catch {
-            return undefined
-          }
-        }),
-      )
+      const directory = yield* Effect.promise(async () => {
+        await Promise.resolve()
+        return ctx.directory
+      })
 
-      expect(current?.directory).toBe(test.directory)
+      expect(directory).toBe(test.directory)
     }),
   { git: true },
 )

+ 163 - 133
packages/opencode/test/config/config.test.ts

@@ -6,7 +6,8 @@ import { ConfigManaged } from "@/config/managed"
 import { ConfigParse } from "../../src/config/parse"
 import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
 
-import { Instance } from "../../src/project/instance"
+import { InstanceRef } from "../../src/effect/instance-ref"
+import type { InstanceContext } from "../../src/project/instance-context"
 import { WithInstance } from "../../src/project/with-instance"
 import { Auth } from "../../src/auth"
 import { Account } from "../../src/account/account"
@@ -61,9 +62,20 @@ const layer = Config.layer.pipe(
 
 const it = testEffect(layer)
 
-const load = () => Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(layer)))
-const save = (config: Config.Info) =>
-  Effect.runPromise(Config.Service.use((svc) => svc.update(config)).pipe(Effect.scoped, Effect.provide(layer)))
+const provideCurrentInstance = <A, E, R>(effect: Effect.Effect<A, E, R>, ctx: InstanceContext) =>
+  effect.pipe(Effect.provideService(InstanceRef, ctx))
+
+const load = (ctx: InstanceContext) =>
+  Effect.runPromise(
+    Config.Service.use((svc) => provideCurrentInstance(svc.get(), ctx)).pipe(Effect.scoped, Effect.provide(layer)),
+  )
+const save = (config: Config.Info, ctx: InstanceContext) =>
+  Effect.runPromise(
+    Config.Service.use((svc) => provideCurrentInstance(svc.update(config), ctx)).pipe(
+      Effect.scoped,
+      Effect.provide(layer),
+    ),
+  )
 const saveGlobal = (config: Config.Info) =>
   Effect.runPromise(
     Config.Service.use((svc) => svc.updateGlobal(config)).pipe(
@@ -76,10 +88,20 @@ const clear = async (wait = false) => {
   await Effect.runPromise(Config.Service.use((svc) => svc.invalidate()).pipe(Effect.scoped, Effect.provide(layer)))
   if (wait) await InstanceRuntime.disposeAllInstances()
 }
-const listDirs = () =>
-  Effect.runPromise(Config.Service.use((svc) => svc.directories()).pipe(Effect.scoped, Effect.provide(layer)))
-const ready = () =>
-  Effect.runPromise(Config.Service.use((svc) => svc.waitForDependencies()).pipe(Effect.scoped, Effect.provide(layer)))
+const listDirs = (ctx: InstanceContext) =>
+  Effect.runPromise(
+    Config.Service.use((svc) => provideCurrentInstance(svc.directories(), ctx)).pipe(
+      Effect.scoped,
+      Effect.provide(layer),
+    ),
+  )
+const ready = (ctx: InstanceContext) =>
+  Effect.runPromise(
+    Config.Service.use((svc) => provideCurrentInstance(svc.waitForDependencies(), ctx)).pipe(
+      Effect.scoped,
+      Effect.provide(layer),
+    ),
+  )
 
 // Get managed config directory from environment (set in preload.ts)
 const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
@@ -116,11 +138,11 @@ async function check(map: (dir: string) => string) {
     })
     await WithInstance.provide({
       directory: map(tmp.path),
-      fn: async () => {
-        const cfg = await load()
+      fn: async (ctx) => {
+        const cfg = await load(ctx)
         expect(cfg.snapshot).toBe(true)
-        expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
-        expect(Instance.project.id).not.toBe(ProjectID.global)
+        expect(ctx.directory).toBe(Filesystem.resolve(tmp.path))
+        expect(ctx.project.id).not.toBe(ProjectID.global)
       },
     })
   } finally {
@@ -134,8 +156,8 @@ test("loads config with defaults when no files exist", async () => {
   await using tmp = await tmpdir()
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.username).toBeDefined()
     },
   })
@@ -150,8 +172,8 @@ test("creates global jsonc config with schema when no global configs exist", asy
   try {
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        await load()
+      fn: async (ctx) => {
+        await load(ctx)
       },
     })
 
@@ -175,8 +197,8 @@ test("does not create global config when OPENCODE_CONFIG_DIR is set", async () =
   try {
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        await load()
+      fn: async (ctx) => {
+        await load(ctx)
       },
     })
 
@@ -201,8 +223,8 @@ test("loads JSON config file", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.model).toBe("test/model")
       expect(config.username).toBe("testuser")
     },
@@ -220,8 +242,8 @@ test("loads shell config field", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.shell).toBe("bash")
     },
   })
@@ -242,8 +264,8 @@ test("updates config and preserves empty shell sentinel", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      await save({ shell: "" })
+    fn: async (ctx) => {
+      await save({ shell: "" }, ctx)
 
       const writtenConfig = await Filesystem.readJson<{ shell?: string }>(path.join(tmp.path, "config.json"))
       expect(writtenConfig.shell).toBe("")
@@ -320,8 +342,8 @@ test("loads formatter boolean config", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.formatter).toBe(true)
     },
   })
@@ -338,8 +360,8 @@ test("loads lsp boolean config", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.lsp).toBe(true)
     },
   })
@@ -375,8 +397,8 @@ test("ignores legacy tui keys in opencode config", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.model).toBe("test/model")
       expect((config as Record<string, unknown>).theme).toBeUndefined()
       expect((config as Record<string, unknown>).tui).toBeUndefined()
@@ -400,8 +422,8 @@ test("loads JSONC config file", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.model).toBe("test/model")
       expect(config.username).toBe("testuser")
     },
@@ -428,8 +450,8 @@ test("jsonc overrides json in the same directory", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.model).toBe("base")
       expect(config.username).toBe("base")
     },
@@ -451,8 +473,8 @@ test("handles environment variable substitution", async () => {
     })
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const config = await load()
+      fn: async (ctx) => {
+        const config = await load(ctx)
         expect(config.username).toBe("test-user")
       },
     })
@@ -483,8 +505,8 @@ test("preserves env variables when adding $schema to config", async () => {
     })
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const config = await load()
+      fn: async (ctx) => {
+        const config = await load(ctx)
         expect(config.username).toBe("secret_value")
 
         // Read the file to verify the env variable was preserved
@@ -580,8 +602,8 @@ test("handles file inclusion substitution", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.username).toBe("test-user")
     },
   })
@@ -599,8 +621,8 @@ test("handles file inclusion with replacement tokens", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.username).toBe("const out = await Bun.$`echo hi`")
     },
   })
@@ -617,9 +639,9 @@ test("validates config schema and throws on invalid fields", async () => {
   })
   await provideTestInstance({
     directory: tmp.path,
-    fn: async () => {
+    fn: async (ctx) => {
       // Strict schema should throw an error for invalid fields
-      await expect(load()).rejects.toThrow()
+      await expect(load(ctx)).rejects.toThrow()
     },
   })
 })
@@ -632,8 +654,8 @@ test("throws error for invalid JSON", async () => {
   })
   await provideTestInstance({
     directory: tmp.path,
-    fn: async () => {
-      await expect(load()).rejects.toThrow()
+    fn: async (ctx) => {
+      await expect(load(ctx)).rejects.toThrow()
     },
   })
 })
@@ -655,8 +677,8 @@ test("handles agent configuration", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test_agent"]).toEqual(
         expect.objectContaining({
           model: "test/model",
@@ -686,8 +708,8 @@ test("treats agent variant as model-scoped setting (not provider option)", async
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       const agent = config.agent?.["test_agent"]
 
       expect(agent?.variant).toBe("xhigh")
@@ -716,8 +738,8 @@ test("handles command configuration", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.command?.["test_command"]).toEqual({
         template: "test template",
         description: "test command",
@@ -741,8 +763,8 @@ test("migrates autoshare to share field", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.share).toBe("auto")
       expect(config.autoshare).toBe(true)
     },
@@ -768,8 +790,8 @@ test("migrates mode field to agent field", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test_mode"]).toEqual({
         model: "test/model",
         temperature: 0.5,
@@ -800,8 +822,8 @@ Test agent prompt`,
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]).toEqual(
         expect.objectContaining({
           name: "test",
@@ -833,8 +855,8 @@ Ordered permissions`,
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"])
     },
   })
@@ -871,8 +893,8 @@ Nested agent prompt`,
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
 
       expect(config.agent?.["helper"]).toMatchObject({
         name: "helper",
@@ -920,8 +942,8 @@ Nested command template`,
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
 
       expect(config.command?.["hello"]).toEqual({
         description: "Test command",
@@ -965,8 +987,8 @@ Nested command template`,
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
 
       expect(config.command?.["hello"]).toEqual({
         description: "Test command",
@@ -985,9 +1007,9 @@ test("updates config and writes to file", async () => {
   await using tmp = await tmpdir()
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
+    fn: async (ctx) => {
       const newConfig = { model: "updated/model" }
-      await save(newConfig as any)
+      await save(newConfig as any, ctx)
 
       const writtenConfig = await Filesystem.readJson<{ model: string }>(path.join(tmp.path, "config.json"))
       expect(writtenConfig.model).toBe("updated/model")
@@ -999,8 +1021,8 @@ test("gets config directories", async () => {
   await using tmp = await tmpdir()
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const dirs = await listDirs()
+    fn: async (ctx) => {
+      const dirs = await listDirs(ctx)
       expect(dirs.length).toBeGreaterThanOrEqual(1)
     },
   })
@@ -1029,8 +1051,8 @@ test("does not try to install dependencies in read-only OPENCODE_CONFIG_DIR", as
   try {
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        await load()
+      fn: async (ctx) => {
+        await load(ctx)
       },
     })
   } finally {
@@ -1064,10 +1086,18 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
   try {
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        await Effect.runPromise(Config.Service.use((svc) => svc.get()).pipe(Effect.scoped, Effect.provide(testLayer)))
+      fn: async (ctx) => {
+        await Effect.runPromise(
+          Config.Service.use((svc) => svc.get().pipe(Effect.provideService(InstanceRef, ctx))).pipe(
+            Effect.scoped,
+            Effect.provide(testLayer),
+          ),
+        )
         await Effect.runPromise(
-          Config.Service.use((svc) => svc.waitForDependencies()).pipe(Effect.scoped, Effect.provide(testLayer)),
+          Config.Service.use((svc) => svc.waitForDependencies().pipe(Effect.provideService(InstanceRef, ctx))).pipe(
+            Effect.scoped,
+            Effect.provide(testLayer),
+          ),
         )
       },
     })
@@ -1123,8 +1153,8 @@ test("resolves scoped npm plugins in config", async () => {
 
   await provideTestInstance({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       const pluginEntries = config.plugin ?? []
       expect(pluginEntries).toContain("@scope/plugin")
     },
@@ -1161,8 +1191,8 @@ test("merges plugin arrays from global and local configs", async () => {
 
   await provideTestInstance({
     directory: path.join(tmp.path, "project"),
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       const plugins = config.plugin ?? []
 
       // Should contain both global and local plugins
@@ -1197,8 +1227,8 @@ Helper subagent prompt`,
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["helper"]).toMatchObject({
         name: "helper",
         model: "test/model",
@@ -1236,8 +1266,8 @@ test("merges instructions arrays from global and local configs", async () => {
 
   await WithInstance.provide({
     directory: path.join(tmp.path, "project"),
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       const instructions = config.instructions ?? []
 
       expect(instructions).toContain("global-instructions.md")
@@ -1275,8 +1305,8 @@ test("deduplicates duplicate instructions from global and local configs", async
 
   await WithInstance.provide({
     directory: path.join(tmp.path, "project"),
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       const instructions = config.instructions ?? []
 
       expect(instructions).toContain("global-only.md")
@@ -1320,8 +1350,8 @@ test("deduplicates duplicate plugins from global and local configs", async () =>
 
   await provideTestInstance({
     directory: path.join(tmp.path, "project"),
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       const plugins = config.plugin ?? []
 
       // Should contain all unique plugins
@@ -1369,8 +1399,8 @@ test("keeps plugin origins aligned with merged plugin list", async () => {
 
   await provideTestInstance({
     directory: path.join(tmp.path, "project"),
-    fn: async () => {
-      const cfg = await load()
+    fn: async (ctx) => {
+      const cfg = await load(ctx)
       const plugins = cfg.plugin ?? []
       const origins = cfg.plugin_origins ?? []
       const names = plugins.map((item) => ConfigPlugin.pluginSpecifier(item))
@@ -1410,8 +1440,8 @@ test("migrates legacy tools config to permissions - allow", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         bash: "allow",
         read: "allow",
@@ -1441,8 +1471,8 @@ test("migrates legacy tools config to permissions - deny", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         bash: "deny",
         webfetch: "deny",
@@ -1471,8 +1501,8 @@ test("migrates legacy write tool to edit permission", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         edit: "allow",
       })
@@ -1503,8 +1533,8 @@ test("managed settings override user settings", async () => {
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.model).toBe("managed/model")
       expect(config.share).toBe("disabled")
       expect(config.username).toBe("testuser")
@@ -1531,8 +1561,8 @@ test("managed settings override project settings", async () => {
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.autoupdate).toBe(false)
       expect(config.disabled_providers).toEqual(["openai"])
     },
@@ -1551,8 +1581,8 @@ test("missing managed settings file is not an error", async () => {
 
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.model).toBe("user/model")
     },
   })
@@ -1578,8 +1608,8 @@ test("migrates legacy edit tool to edit permission", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         edit: "deny",
       })
@@ -1607,8 +1637,8 @@ test("migrates legacy patch tool to edit permission", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         edit: "allow",
       })
@@ -1639,8 +1669,8 @@ test("migrates mixed legacy tools config", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         bash: "allow",
         edit: "allow",
@@ -1674,8 +1704,8 @@ test("merges legacy tools with existing permission config", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.agent?.["test"]?.permission).toEqual({
         glob: "allow",
         bash: "allow",
@@ -1711,8 +1741,8 @@ test("permission config preserves user key order", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(Object.keys(config.permission!)).toEqual([
         "*",
         "edit",
@@ -1794,8 +1824,8 @@ test("project config can override MCP server enabled status", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       // jira should be enabled (overridden by project config)
       expect(config.mcp?.jira).toEqual({
         type: "remote",
@@ -1850,8 +1880,8 @@ test("MCP config deep merges preserving base config properties", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.mcp?.myserver).toEqual({
         type: "remote",
         url: "https://myserver.example.com/mcp",
@@ -1901,8 +1931,8 @@ test("local .opencode config can override MCP from project config", async () =>
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      const config = await load()
+    fn: async (ctx) => {
+      const config = await load(ctx)
       expect(config.mcp?.docs?.enabled).toBe(true)
     },
   })
@@ -2235,8 +2265,8 @@ describe("deduplicatePluginOrigins", () => {
 
     await provideTestInstance({
       directory: path.join(tmp.path, "project"),
-      fn: async () => {
-        const config = await load()
+      fn: async (ctx) => {
+        const config = await load(ctx)
         const plugins = config.plugin ?? []
 
         expect(plugins.some((p) => ConfigPlugin.pluginSpecifier(p) === "my-plugin@1.0.0")).toBe(true)
@@ -2267,8 +2297,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
       })
       await WithInstance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const config = await load()
+        fn: async (ctx) => {
+          const config = await load(ctx)
           // Project config should NOT be loaded - model should be default, not "project/model"
           expect(config.model).not.toBe("project/model")
           expect(config.username).not.toBe("project-user")
@@ -2298,8 +2328,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
       })
       await WithInstance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const directories = await listDirs()
+        fn: async (ctx) => {
+          const directories = await listDirs(ctx)
           // Project .opencode should NOT be in directories list
           const hasProjectOpencode = directories.some((d) => d.startsWith(tmp.path))
           expect(hasProjectOpencode).toBe(false)
@@ -2322,9 +2352,9 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
       await using tmp = await tmpdir()
       await WithInstance.provide({
         directory: tmp.path,
-        fn: async () => {
+        fn: async (ctx) => {
           // Should still get default config (from global or defaults)
-          const config = await load()
+          const config = await load(ctx)
           expect(config).toBeDefined()
           expect(config.username).toBeDefined()
         },
@@ -2364,10 +2394,10 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
 
       await WithInstance.provide({
         directory: tmp.path,
-        fn: async () => {
+        fn: async (ctx) => {
           // The relative instruction should be skipped without error
           // We're mainly verifying this doesn't throw and the config loads
-          const config = await load()
+          const config = await load(ctx)
           expect(config).toBeDefined()
           // The instruction should have been skipped (warning logged)
           // We can't easily test the warning was logged, but we verify
@@ -2424,8 +2454,8 @@ describe("OPENCODE_DISABLE_PROJECT_CONFIG", () => {
 
       await WithInstance.provide({
         directory: projectTmp.path,
-        fn: async () => {
-          const config = await load()
+        fn: async (ctx) => {
+          const config = await load(ctx)
           // Should load from OPENCODE_CONFIG_DIR, not project
           expect(config.model).toBe("configdir/model")
         },
@@ -2459,8 +2489,8 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
       await using tmp = await tmpdir()
       await WithInstance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const config = await load()
+        fn: async (ctx) => {
+          const config = await load(ctx)
           expect(config.username).toBe("test_api_key_12345")
         },
       })
@@ -2493,8 +2523,8 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => {
       })
       await WithInstance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const config = await load()
+        fn: async (ctx) => {
+          const config = await load(ctx)
           expect(config.username).toBe("secret_key_from_file")
         },
       })

+ 15 - 2
packages/opencode/test/control-plane/workspace.test.ts

@@ -139,7 +139,18 @@ async function initGitRepo(dir: string) {
   await $`git commit -m "base"`.cwd(dir).quiet()
 }
 
-const runWorkspace = <A, E>(effect: Effect.Effect<A, E, Workspace.Service>) => AppRuntime.runPromise(effect)
+function currentInstance() {
+  try {
+    return context.use()
+  } catch {
+    return undefined
+  }
+}
+
+const runWorkspace = <A, E>(effect: Effect.Effect<A, E, Workspace.Service>) => {
+  const ctx = currentInstance()
+  return AppRuntime.runPromise(ctx ? effect.pipe(Effect.provideService(InstanceRef, ctx)) : effect)
+}
 const createWorkspace = (input: Workspace.CreateInput) =>
   runWorkspace(Workspace.Service.use((workspace) => workspace.create(input)))
 const warpWorkspaceSession = (input: Workspace.SessionWarpInput) =>
@@ -917,7 +928,9 @@ describe("workspace CRUD", () => {
       const previous = workspaceInfo(projectID, previousType)
       insertWorkspace(previous)
       registerAdapter(projectID, previousType, localAdapter(workspaceTmp.path, { createDir: false }).adapter)
-      const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
+      const session = await AppRuntime.runPromise(
+        SessionNs.Service.use((svc) => svc.create({})).pipe(Effect.provideService(InstanceRef, instance)),
+      )
       attachSessionToWorkspace(session.id, previous.id)
 
       const workspaceCtx = await AppRuntime.runPromise(

+ 0 - 1
packages/opencode/test/effect/instance-state.test.ts

@@ -3,7 +3,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { $ } from "bun"
 import { Context, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
 import { InstanceState } from "@/effect/instance-state"
-import { Instance } from "../../src/project/instance"
 import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 

+ 8 - 6
packages/opencode/test/fixture/fixture.ts

@@ -11,9 +11,9 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import type { Config } from "@/config/config"
 import { InstanceRef } from "../../src/effect/instance-ref"
 import { InstanceBootstrap } from "../../src/project/bootstrap-service"
+import type { InstanceContext } from "../../src/project/instance-context"
 import { InstanceRuntime } from "../../src/project/instance-runtime"
 import { InstanceStore } from "../../src/project/instance-store"
-import { Instance } from "../../src/project/instance"
 import { TestLLMServer } from "../lib/llm-server"
 
 const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
@@ -24,11 +24,15 @@ const testInstanceRuntime = ManagedRuntime.make(
 const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.Effect<A>) =>
   testInstanceRuntime.runPromise(InstanceStore.Service.use(fn))
 
-export async function provideTestInstance<R>(input: { directory: string; init?: Effect.Effect<void>; fn: () => R }) {
+export async function provideTestInstance<R>(input: {
+  directory: string
+  init?: Effect.Effect<void>
+  fn: (ctx: InstanceContext) => R
+}) {
   const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory }))
   try {
     if (input.init) await testInstanceRuntime.runPromise(input.init.pipe(Effect.provideService(InstanceRef, ctx)))
-    return await Instance.restore(ctx, () => input.fn())
+    return await input.fn(ctx)
   } finally {
     await runTestInstanceStore((store) => store.dispose(ctx))
   }
@@ -157,9 +161,7 @@ export const provideInstance =
     Effect.contextWith((services: Context.Context<R>) =>
       Effect.promise<A>(async () => {
         const ctx = await runTestInstanceStore((store) => store.load({ directory }))
-        return Instance.restore(ctx, () =>
-          Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, ctx))),
-        )
+        return Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, ctx)))
       }),
     )
 

+ 24 - 13
packages/opencode/test/lsp/client.test.ts

@@ -4,7 +4,6 @@ import { pathToFileURL } from "url"
 import { tmpdir } from "../fixture/fixture"
 import { LSPClient } from "@/lsp/client"
 import * as LSPServer from "@/lsp/server"
-import { Instance } from "../../src/project/instance"
 import { WithInstance } from "../../src/project/with-instance"
 import * as Log from "@opencode-ai/core/util/log"
 
@@ -28,12 +27,13 @@ describe("LSPClient interop", () => {
 
     const client = await WithInstance.provide({
       directory: process.cwd(),
-      fn: () =>
+      fn: (ctx) =>
         LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: process.cwd(),
           directory: process.cwd(),
+          instance: ctx,
         }),
     })
 
@@ -51,12 +51,13 @@ describe("LSPClient interop", () => {
 
     const client = await WithInstance.provide({
       directory: process.cwd(),
-      fn: () =>
+      fn: (ctx) =>
         LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: process.cwd(),
           directory: process.cwd(),
+          instance: ctx,
         }),
     })
 
@@ -74,12 +75,13 @@ describe("LSPClient interop", () => {
 
     const client = await WithInstance.provide({
       directory: process.cwd(),
-      fn: () =>
+      fn: (ctx) =>
         LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: process.cwd(),
           directory: process.cwd(),
+          instance: ctx,
         }),
     })
 
@@ -97,12 +99,13 @@ describe("LSPClient interop", () => {
 
     const client = await WithInstance.provide({
       directory: process.cwd(),
-      fn: () =>
+      fn: (ctx) =>
         LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: process.cwd(),
           directory: process.cwd(),
+          instance: ctx,
         }),
     })
 
@@ -124,7 +127,7 @@ describe("LSPClient interop", () => {
 
     const client = await WithInstance.provide({
       directory: process.cwd(),
-      fn: () =>
+      fn: (ctx) =>
         LSPClient.create({
           serverID: "fake",
           server: {
@@ -133,6 +136,7 @@ describe("LSPClient interop", () => {
           },
           root: process.cwd(),
           directory: process.cwd(),
+          instance: ctx,
         }),
     })
 
@@ -153,12 +157,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         await client.notify.open({ path: file })
@@ -196,12 +201,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         const version = await client.notify.open({ path: file })
@@ -242,12 +248,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         const version = await client.notify.open({ path: file })
@@ -289,12 +296,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         await client.connection.sendRequest("test/configure-pull-diagnostics", {
@@ -337,12 +345,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         await client.connection.sendRequest("test/configure-pull-diagnostics", {
@@ -390,12 +399,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         await client.connection.sendRequest("test/configure-pull-diagnostics", {
@@ -454,12 +464,13 @@ describe("LSPClient interop", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
+      fn: async (ctx) => {
         const client = await LSPClient.create({
           serverID: "fake",
           server: handle as unknown as LSPServer.Handle,
           root: tmp.path,
           directory: tmp.path,
+          instance: ctx,
         })
 
         await client.connection.sendRequest("test/configure-pull-diagnostics", {

+ 3 - 2
packages/opencode/test/plugin/workspace-adapter.test.ts

@@ -15,10 +15,10 @@ import { RuntimeFlags } from "../../src/effect/runtime-flags"
 import { Workspace } from "../../src/control-plane/workspace"
 import { Plugin } from "../../src/plugin/index"
 import { InstanceBootstrap } from "../../src/project/bootstrap-service"
-import { Instance } from "../../src/project/instance"
 import { InstanceStore } from "../../src/project/instance-store"
 import { Project } from "../../src/project/project"
 import { Vcs } from "../../src/project/vcs"
+import { InstanceState } from "../../src/effect/instance-state"
 import { Session } from "../../src/session/session"
 import { SessionPrompt } from "../../src/session/prompt"
 import { SyncEvent } from "../../src/sync"
@@ -116,11 +116,12 @@ describe("plugin.workspace", () => {
         const plugin = yield* Plugin.Service
         yield* plugin.init()
         const workspace = yield* Workspace.Service
+        const ctx = yield* InstanceState.context
         const info = yield* workspace.create({
           type,
           branch: null,
           extra: { key: "value" },
-          projectID: Instance.project.id,
+          projectID: ctx.project.id,
         })
 
         expect(info.type).toBe(type)

+ 2 - 21
packages/opencode/test/project/instance.test.ts

@@ -4,9 +4,8 @@ import { Deferred, Effect, Fiber, Layer } from "effect"
 import { InstanceRef } from "../../src/effect/instance-ref"
 import { registerDisposer } from "../../src/effect/instance-registry"
 import { InstanceBootstrap } from "../../src/project/bootstrap-service"
-import { Instance } from "../../src/project/instance"
 import { InstanceStore } from "../../src/project/instance-store"
-import { TestInstance, tmpdirScoped } from "../fixture/fixture"
+import { tmpdirScoped } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
 
 let bootstrapRun: Effect.Effect<void> = Effect.void
@@ -37,7 +36,7 @@ const registerDisposerScoped = (disposer: (directory: string) => Promise<void>)
   )
 
 describe("InstanceStore", () => {
-  it.live("loads instance context without installing ALS for the caller", () =>
+  it.live("loads instance context", () =>
     Effect.gen(function* () {
       const dir = yield* tmpdirScoped({ git: true })
       const store = yield* InstanceStore.Service
@@ -45,7 +44,6 @@ describe("InstanceStore", () => {
 
       expect(ctx.directory).toBe(dir)
       expect(ctx.worktree).toBe(dir)
-      expect(() => Instance.current).toThrow()
     }),
   )
 
@@ -63,7 +61,6 @@ describe("InstanceStore", () => {
       yield* store.load({ directory: dir })
 
       expect(initializedDirectory).toBe(dir)
-      expect(() => Instance.current).toThrow()
     }),
   )
 
@@ -245,20 +242,4 @@ describe("InstanceStore", () => {
       expect(disposed).toEqual([dir1, dir2])
     }),
   )
-
-  it.instance(
-    "provides legacy Promise callers with instance ALS",
-    () =>
-      Effect.gen(function* () {
-        const test = yield* TestInstance
-        const ctx = yield* InstanceRef
-        if (!ctx) throw new Error("InstanceRef not provided")
-
-        const directory = yield* Effect.promise(() => Promise.resolve(Instance.restore(ctx, () => Instance.directory)))
-
-        expect(directory).toBe(test.directory)
-        expect(() => Instance.current).toThrow()
-      }),
-    { git: true },
-  )
 })

+ 5 - 2
packages/opencode/test/project/worktree.test.ts

@@ -5,7 +5,7 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
 import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
 import { Git } from "../../src/git"
-import { Instance } from "../../src/project/instance"
+import { InstanceRef } from "../../src/effect/instance-ref"
 import { InstanceRuntime } from "../../src/project/instance-runtime"
 import { Worktree } from "../../src/worktree"
 import { disposeAllInstances, provideInstance, TestInstance } from "../fixture/fixture"
@@ -41,7 +41,10 @@ const waitReady = Effect.fn("WorktreeTest.waitReady")(function* () {
 const removeCreatedWorktree = (directory: string) =>
   Effect.gen(function* () {
     const svc = yield* Worktree.Service
-    const ctx = yield* Effect.sync(() => Instance.current).pipe(provideInstance(directory))
+    const ctx = yield* Effect.gen(function* () {
+      return yield* InstanceRef
+    }).pipe(provideInstance(directory))
+    if (!ctx) return yield* Effect.die(new Error("missing test instance"))
     yield* Effect.promise(() => InstanceRuntime.disposeInstance(ctx))
     const ok = yield* svc.remove({ directory })
     if (!ok) return yield* Effect.fail(new Error(`failed to remove worktree ${directory}`))

+ 64 - 44
packages/opencode/test/provider/amazon-bedrock.test.ts

@@ -1,10 +1,10 @@
-import { test, expect, describe } from "bun:test"
+import { afterEach, test, expect, describe } from "bun:test"
 import path from "path"
 import { unlink } from "fs/promises"
 
 import { ProviderID } from "../../src/provider/schema"
-import { tmpdir } from "../fixture/fixture"
-import { Instance } from "../../src/project/instance"
+import { disposeAllInstances, tmpdir } from "../fixture/fixture"
+import type { InstanceContext } from "../../src/project/instance-context"
 import { WithInstance } from "../../src/project/with-instance"
 import { Provider } from "@/provider/provider"
 import { Env } from "../../src/env"
@@ -12,17 +12,37 @@ import { Global } from "@opencode-ai/core/global"
 import { Filesystem } from "@/util/filesystem"
 import { Effect } from "effect"
 import { AppRuntime } from "../../src/effect/app-runtime"
+import { InstanceRef } from "../../src/effect/instance-ref"
 import { makeRuntime } from "../../src/effect/run-service"
 
 const env = makeRuntime(Env.Service, Env.defaultLayer)
-const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
+const originalEnv = new Map<string, string | undefined>()
 
-async function list() {
+function rememberEnv(k: string) {
+  if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
+}
+
+const set = (ctx: InstanceContext, k: string, v: string) => {
+  rememberEnv(k)
+  process.env[k] = v
+  return env.runSync((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
+}
+
+afterEach(async () => {
+  for (const [key, value] of originalEnv) {
+    if (value === undefined) delete process.env[key]
+    else process.env[key] = value
+  }
+  originalEnv.clear()
+  await disposeAllInstances()
+})
+
+async function list(ctx: InstanceContext) {
   return AppRuntime.runPromise(
     Effect.gen(function* () {
       const provider = yield* Provider.Service
       return yield* provider.list()
-    }),
+    }).pipe(Effect.provideService(InstanceRef, ctx)),
   )
 }
 
@@ -46,10 +66,10 @@ test("Bedrock: config region takes precedence over AWS_REGION env var", async ()
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_REGION", "us-east-1")
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_REGION", "us-east-1")
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
     },
@@ -69,10 +89,10 @@ test("Bedrock: falls back to AWS_REGION env var when no config region", async ()
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_REGION", "eu-west-1")
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_REGION", "eu-west-1")
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
     },
@@ -122,11 +142,11 @@ test("Bedrock: loads when bearer token from auth.json is present", async () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        set("AWS_PROFILE", "")
-        set("AWS_ACCESS_KEY_ID", "")
-        set("AWS_BEARER_TOKEN_BEDROCK", "")
-        const providers = await list()
+      fn: async (ctx) => {
+        set(ctx, "AWS_PROFILE", "")
+        set(ctx, "AWS_ACCESS_KEY_ID", "")
+        set(ctx, "AWS_BEARER_TOKEN_BEDROCK", "")
+        const providers = await list(ctx)
         expect(providers[ProviderID.amazonBedrock]).toBeDefined()
         expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
       },
@@ -166,10 +186,10 @@ test("Bedrock: config profile takes precedence over AWS_PROFILE env var", async
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_PROFILE", "default")
-      set("AWS_ACCESS_KEY_ID", "test-key-id")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_PROFILE", "default")
+      set(ctx, "AWS_ACCESS_KEY_ID", "test-key-id")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
     },
@@ -196,9 +216,9 @@ test("Bedrock: includes custom endpoint in options when specified", async () =>
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe(
         "https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
@@ -227,12 +247,12 @@ test("Bedrock: autoloads when AWS_WEB_IDENTITY_TOKEN_FILE is present", async ()
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
-      set("AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
-      set("AWS_PROFILE", "")
-      set("AWS_ACCESS_KEY_ID", "")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_WEB_IDENTITY_TOKEN_FILE", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token")
+      set(ctx, "AWS_ROLE_ARN", "arn:aws:iam::123456789012:role/my-eks-role")
+      set(ctx, "AWS_PROFILE", "")
+      set(ctx, "AWS_ACCESS_KEY_ID", "")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
     },
@@ -268,9 +288,9 @@ test("Bedrock: model with us. prefix should not be double-prefixed", async () =>
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       // The model should exist with the us. prefix
       expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
@@ -303,9 +323,9 @@ test("Bedrock: model with global. prefix should not be prefixed", async () => {
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
     },
@@ -337,9 +357,9 @@ test("Bedrock: model with eu. prefix should not be double-prefixed", async () =>
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
     },
@@ -371,9 +391,9 @@ test("Bedrock: model without prefix in US region should get us. prefix added", a
   })
   await WithInstance.provide({
     directory: tmp.path,
-    fn: async () => {
-      set("AWS_PROFILE", "default")
-      const providers = await list()
+    fn: async (ctx) => {
+      set(ctx, "AWS_PROFILE", "default")
+      const providers = await list(ctx)
       expect(providers[ProviderID.amazonBedrock]).toBeDefined()
       // Non-prefixed model should still be registered
       expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()

+ 0 - 1
packages/opencode/test/provider/gitlab-duo.test.ts

@@ -8,7 +8,6 @@ export {}
 
 // import { ProviderID, ModelID } from "../../src/provider/schema"
 // import { tmpdir } from "../fixture/fixture"
-// import { Instance } from "../../src/project/instance"
 import { WithInstance } from "../../src/project/with-instance"
 // import { Provider } from "@/provider/provider"
 // import { Env } from "../../src/env"

Разлика између датотеке није приказан због своје велике величине
+ 242 - 218
packages/opencode/test/provider/provider.test.ts


+ 5 - 2
packages/opencode/test/question/question.test.ts

@@ -1,7 +1,7 @@
 import { afterEach, expect } from "bun:test"
 import { Cause, Effect, Exit, Fiber, Layer, Queue } from "effect"
 import { Question } from "../../src/question"
-import { Instance } from "../../src/project/instance"
+import { InstanceRef } from "../../src/effect/instance-ref"
 import { InstanceRuntime } from "../../src/project/instance-runtime"
 import { QuestionID } from "../../src/question/schema"
 import { disposeAllInstances, provideInstance, reloadTestInstance, tmpdirScoped } from "../fixture/fixture"
@@ -404,7 +404,10 @@ it.live("pending question rejects on instance dispose", () =>
     }).pipe(provideInstance(dir), Effect.forkScoped)
 
     expect(yield* waitForPending(1).pipe(provideInstance(dir))).toHaveLength(1)
-    const ctx = yield* Effect.sync(() => Instance.current).pipe(provideInstance(dir))
+    const ctx = yield* Effect.gen(function* () {
+      return yield* InstanceRef
+    }).pipe(provideInstance(dir))
+    if (!ctx) return yield* Effect.die(new Error("missing test instance"))
     yield* Effect.promise(() => InstanceRuntime.disposeInstance(ctx))
 
     const exit = yield* Fiber.await(fiber)

+ 6 - 3
packages/opencode/test/server/httpapi-event.test.ts

@@ -1,11 +1,12 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { Bus } from "../../src/bus"
-import { Instance } from "../../src/project/instance"
+import { AppRuntime } from "../../src/effect/app-runtime"
+import { InstanceRef } from "../../src/effect/instance-ref"
 import { Server } from "../../src/server/server"
 import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
 import { Event as ServerEvent } from "../../src/server/event"
 import * as Log from "@opencode-ai/core/util/log"
-import { Schema } from "effect"
+import { Effect, Schema } from "effect"
 import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, reloadTestInstance, tmpdir } from "../fixture/fixture"
 
@@ -108,7 +109,9 @@ describe("event HttpApi", () => {
 
       const next = readEvent(reader)
       const ctx = await reloadTestInstance({ directory: tmp.path })
-      await Instance.restore(ctx, () => Bus.publish(ServerEvent.Connected, {}))
+      await AppRuntime.runPromise(
+        Bus.Service.use((svc) => svc.publish(ServerEvent.Connected, {})).pipe(Effect.provideService(InstanceRef, ctx)),
+      )
 
       expect(await next).toMatchObject({ type: "server.connected", properties: {} })
     } finally {

+ 0 - 3
packages/opencode/test/server/httpapi-exercise/runtime.ts

@@ -3,7 +3,6 @@ export type Runtime = {
   HttpApiApp: (typeof import("../../../src/server/routes/instance/httpapi/server"))["HttpApiApp"]
   AppLayer: (typeof import("../../../src/effect/app-runtime"))["AppLayer"]
   InstanceRef: (typeof import("../../../src/effect/instance-ref"))["InstanceRef"]
-  Instance: (typeof import("../../../src/project/instance"))["Instance"]
   InstanceStore: (typeof import("../../../src/project/instance-store"))["InstanceStore"]
   Session: (typeof import("../../../src/session/session"))["Session"]
   Todo: (typeof import("../../../src/session/todo"))["Todo"]
@@ -23,7 +22,6 @@ export function runtime() {
     const httpApiServer = await import("../../../src/server/routes/instance/httpapi/server")
     const appRuntime = await import("../../../src/effect/app-runtime")
     const instanceRef = await import("../../../src/effect/instance-ref")
-    const instance = await import("../../../src/project/instance")
     const instanceStore = await import("../../../src/project/instance-store")
     const session = await import("../../../src/session/session")
     const todo = await import("../../../src/session/todo")
@@ -37,7 +35,6 @@ export function runtime() {
       HttpApiApp: httpApiServer.HttpApiApp,
       AppLayer: appRuntime.AppLayer,
       InstanceRef: instanceRef.InstanceRef,
-      Instance: instance.Instance,
       InstanceStore: instanceStore.InstanceStore,
       Session: session.Session,
       Todo: todo.Todo,

+ 0 - 1
packages/opencode/test/server/httpapi-file.test.ts

@@ -3,7 +3,6 @@ import { Context } from "effect"
 import path from "path"
 import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
 import { FilePaths } from "../../src/server/routes/instance/httpapi/groups/file"
-import { Instance } from "../../src/project/instance"
 import * as Log from "@opencode-ai/core/util/log"
 import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, tmpdir } from "../fixture/fixture"

+ 0 - 1
packages/opencode/test/server/httpapi-instance-context.test.ts

@@ -10,7 +10,6 @@ import { WorkspaceID } from "../../src/control-plane/schema"
 import type { WorkspaceAdapter } from "../../src/control-plane/types"
 import { Workspace } from "../../src/control-plane/workspace"
 import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
-import { Instance } from "../../src/project/instance"
 import { InstanceLayer } from "../../src/project/instance-layer"
 import { Project } from "../../src/project/project"
 import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"

+ 0 - 1
packages/opencode/test/server/httpapi-pty.test.ts

@@ -1,7 +1,6 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { NodeHttpServer, NodeServices } from "@effect/platform-node"
 import { PtyID } from "../../src/pty/schema"
-import { Instance } from "../../src/project/instance"
 import { Server } from "../../src/server/server"
 import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
 import * as Log from "@opencode-ai/core/util/log"

+ 0 - 1
packages/opencode/test/server/httpapi-raw-route-auth.test.ts

@@ -1,7 +1,6 @@
 import { afterEach, describe, expect, test } from "bun:test"
 import { ConfigProvider, Layer } from "effect"
 import { HttpRouter } from "effect/unstable/http"
-import { Instance } from "../../src/project/instance"
 import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
 import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
 import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"

+ 6 - 3
packages/opencode/test/server/httpapi-workspace.test.ts

@@ -13,7 +13,6 @@ import * as Log from "@opencode-ai/core/util/log"
 import { Server } from "../../src/server/server"
 import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
-import { Instance } from "../../src/project/instance"
 import { InstanceBootstrap } from "../../src/project/bootstrap"
 import { InstanceStore } from "../../src/project/instance-store"
 import { Project } from "../../src/project/project"
@@ -71,7 +70,7 @@ function listedAdapter(directory: string, type: string): WorkspaceAdapter {
     },
     async create() {},
     async remove() {},
-    list() {
+    list(context) {
       return [
         {
           type,
@@ -79,7 +78,7 @@ function listedAdapter(directory: string, type: string): WorkspaceAdapter {
           branch: "listed/main",
           directory,
           extra: { listed: true },
-          projectID: Instance.project.id,
+          projectID: context?.instance?.project.id ?? missingAdapterContext(),
         },
       ]
     },
@@ -92,6 +91,10 @@ function listedAdapter(directory: string, type: string): WorkspaceAdapter {
   }
 }
 
+function missingAdapterContext(): never {
+  throw new Error("missing workspace adapter context")
+}
+
 function remoteAdapter(directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter {
   return {
     name: "Remote Test",

+ 146 - 122
packages/opencode/test/session/llm.test.ts

@@ -4,8 +4,9 @@ import { tool, type ModelMessage } from "ai"
 import { Cause, Effect, Exit, Stream } from "effect"
 import z from "zod"
 import { makeRuntime } from "../../src/effect/run-service"
+import { InstanceRef } from "../../src/effect/instance-ref"
 import { LLM } from "../../src/session/llm"
-import { Instance } from "../../src/project/instance"
+import type { InstanceContext } from "../../src/project/instance-context"
 import { WithInstance } from "../../src/project/with-instance"
 import { Provider } from "@/provider/provider"
 import { ProviderTransform } from "@/provider/transform"
@@ -18,19 +19,21 @@ import { MessageV2 } from "../../src/session/message-v2"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { AppRuntime } from "../../src/effect/app-runtime"
 
-async function getModel(providerID: ProviderID, modelID: ModelID) {
-  return AppRuntime.runPromise(
-    Effect.gen(function* () {
-      const provider = yield* Provider.Service
-      return yield* provider.getModel(providerID, modelID)
-    }),
-  )
+async function getModel(providerID: ProviderID, modelID: ModelID, ctx: InstanceContext) {
+  const effect = Effect.gen(function* () {
+    const provider = yield* Provider.Service
+    return yield* provider.getModel(providerID, modelID)
+  })
+  return AppRuntime.runPromise(effect.pipe(Effect.provideService(InstanceRef, ctx)))
 }
 
 const llm = makeRuntime(LLM.Service, LLM.defaultLayer)
 
-async function drain(input: LLM.StreamInput) {
-  return llm.runPromise((svc) => svc.stream(input).pipe(Stream.runDrain))
+async function drain(input: LLM.StreamInput, ctx: InstanceContext) {
+  return llm.runPromise((svc) => {
+    const effect = svc.stream(input).pipe(Stream.runDrain)
+    return effect.pipe(Effect.provideService(InstanceRef, ctx))
+  })
 }
 
 describe("session.llm.hasToolCalls", () => {
@@ -360,8 +363,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-1")
         const agent = {
           name: "test",
@@ -381,15 +384,18 @@ describe("session.llm.stream", () => {
           model: { providerID: ProviderID.make(providerID), modelID: resolved.id, variant: "high" },
         } satisfies MessageV2.User
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          system: ["You are a helpful assistant."],
-          messages: [{ role: "user", content: "Hello" }],
-          tools: {},
-        })
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            system: ["You are a helpful assistant."],
+            messages: [{ role: "user", content: "Hello" }],
+            tools: {},
+          },
+          ctx,
+        )
 
         const capture = await request
         const body = capture.body
@@ -447,8 +453,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-service-abort")
         const agent = {
           name: "test",
@@ -478,7 +484,7 @@ describe("session.llm.stream", () => {
                 messages: [{ role: "user", content: "Hello" }],
                 tools: {},
               })
-              .pipe(Stream.runDrain),
+              .pipe(Stream.runDrain, Effect.provideService(InstanceRef, ctx)),
           { signal: ctrl.signal },
         )
 
@@ -537,8 +543,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-tools")
         const agent = {
           name: "test",
@@ -557,22 +563,25 @@ describe("session.llm.stream", () => {
           tools: { question: true },
         } satisfies MessageV2.User
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          permission: [{ permission: "question", pattern: "*", action: "allow" }],
-          system: ["You are a helpful assistant."],
-          messages: [{ role: "user", content: "Hello" }],
-          tools: {
-            question: tool({
-              description: "Ask a question",
-              inputSchema: z.object({}),
-              execute: async () => ({ output: "" }),
-            }),
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            permission: [{ permission: "question", pattern: "*", action: "allow" }],
+            system: ["You are a helpful assistant."],
+            messages: [{ role: "user", content: "Hello" }],
+            tools: {
+              question: tool({
+                description: "Ask a question",
+                inputSchema: z.object({}),
+                execute: async () => ({ output: "" }),
+              }),
+            },
           },
-        })
+          ctx,
+        )
 
         const capture = await request
         const tools = capture.body.tools as Array<{ function?: { name?: string } }> | undefined
@@ -651,8 +660,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-2")
         const agent = {
           name: "test",
@@ -671,15 +680,18 @@ describe("session.llm.stream", () => {
           model: { providerID: ProviderID.make("openai"), modelID: resolved.id, variant: "high" },
         } satisfies MessageV2.User
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          system: ["You are a helpful assistant."],
-          messages: [{ role: "user", content: "Hello" }],
-          tools: {},
-        })
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            system: ["You are a helpful assistant."],
+            messages: [{ role: "user", content: "Hello" }],
+            tools: {},
+          },
+          ctx,
+        )
 
         const capture = await request
         const body = capture.body
@@ -767,8 +779,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.openai, ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.openai, ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-data-url")
         const agent = {
           name: "test",
@@ -786,28 +798,31 @@ describe("session.llm.stream", () => {
           model: { providerID: ProviderID.make("openai"), modelID: resolved.id },
         } satisfies MessageV2.User
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          system: ["You are a helpful assistant."],
-          messages: [
-            {
-              role: "user",
-              content: [
-                { type: "text", text: "Describe this image" },
-                {
-                  type: "file",
-                  mediaType: "image/png",
-                  filename: "large-image.png",
-                  data: image,
-                },
-              ],
-            },
-          ] as ModelMessage[],
-          tools: {},
-        })
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            system: ["You are a helpful assistant."],
+            messages: [
+              {
+                role: "user",
+                content: [
+                  { type: "text", text: "Describe this image" },
+                  {
+                    type: "file",
+                    mediaType: "image/png",
+                    filename: "large-image.png",
+                    data: image,
+                  },
+                ],
+              },
+            ] as ModelMessage[],
+            tools: {},
+          },
+          ctx,
+        )
 
         const capture = await request
         expect(capture.url.pathname.endsWith("/responses")).toBe(true)
@@ -886,8 +901,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-3")
         const agent = {
           name: "test",
@@ -907,15 +922,18 @@ describe("session.llm.stream", () => {
           model: { providerID: ProviderID.make("minimax"), modelID: ModelID.make("MiniMax-M2.5") },
         } satisfies MessageV2.User
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          system: ["You are a helpful assistant."],
-          messages: [{ role: "user", content: "Hello" }],
-          tools: {},
-        })
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            system: ["You are a helpful assistant."],
+            messages: [{ role: "user", content: "Hello" }],
+            tools: {},
+          },
+          ctx,
+        )
 
         const capture = await request
         const body = capture.body
@@ -1004,8 +1022,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.make("anthropic"), ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-anthropic-tools")
         const agent = {
           name: "test",
@@ -1110,31 +1128,34 @@ describe("session.llm.stream", () => {
           },
         ] as any[]
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          system: [],
-          messages: await MessageV2.toModelMessages(input as any, resolved),
-          tools: {
-            read: tool({
-              description: "Stub read tool",
-              inputSchema: z.object({
-                filePath: z.string(),
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            system: [],
+            messages: await MessageV2.toModelMessages(input as any, resolved),
+            tools: {
+              read: tool({
+                description: "Stub read tool",
+                inputSchema: z.object({
+                  filePath: z.string(),
+                }),
+                execute: async () => ({ output: "stub" }),
               }),
-              execute: async () => ({ output: "stub" }),
-            }),
-            glob: tool({
-              description: "Stub glob tool",
-              inputSchema: z.object({
-                pattern: z.string(),
-                path: z.string().optional(),
+              glob: tool({
+                description: "Stub glob tool",
+                inputSchema: z.object({
+                  pattern: z.string(),
+                  path: z.string().optional(),
+                }),
+                execute: async () => ({ output: "stub" }),
               }),
-              execute: async () => ({ output: "stub" }),
-            }),
+            },
           },
-        })
+          ctx,
+        )
 
         const capture = await request
         const body = capture.body
@@ -1245,8 +1266,8 @@ describe("session.llm.stream", () => {
 
     await WithInstance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
+      fn: async (ctx) => {
+        const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id), ctx)
         const sessionID = SessionID.make("session-test-4")
         const agent = {
           name: "test",
@@ -1266,15 +1287,18 @@ describe("session.llm.stream", () => {
           model: { providerID: ProviderID.make(providerID), modelID: resolved.id },
         } satisfies MessageV2.User
 
-        await drain({
-          user,
-          sessionID,
-          model: resolved,
-          agent,
-          system: ["You are a helpful assistant."],
-          messages: [{ role: "user", content: "Hello" }],
-          tools: {},
-        })
+        await drain(
+          {
+            user,
+            sessionID,
+            model: resolved,
+            agent,
+            system: ["You are a helpful assistant."],
+            messages: [{ role: "user", content: "Hello" }],
+            tools: {},
+          },
+          ctx,
+        )
 
         const capture = await request
         const body = capture.body

+ 1 - 1
packages/opencode/test/tool/external-directory.test.ts

@@ -48,7 +48,7 @@ describe("tool.assertExternalDirectory", () => {
     }),
   )
 
-  it.live("no-ops for paths inside Instance.directory", () =>
+  it.live("no-ops for paths inside the instance directory", () =>
     provideInstance("/tmp/project")(
       Effect.gen(function* () {
         const { requests, ctx } = makeCtx()

+ 0 - 1
packages/opencode/test/tool/lsp.test.ts

@@ -6,7 +6,6 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { LSP } from "@/lsp/lsp"
 import { Permission } from "../../src/permission"
-import { Instance } from "../../src/project/instance"
 import { MessageID, SessionID } from "../../src/session/schema"
 import { Tool } from "@/tool/tool"
 import { Truncate } from "@/tool/truncate"

+ 0 - 1
packages/opencode/test/tool/read.test.ts

@@ -10,7 +10,6 @@ import { RuntimeFlags } from "@/effect/runtime-flags"
 import { Git } from "@/git"
 import { LSP } from "@/lsp/lsp"
 import { Permission } from "../../src/permission"
-import { Instance } from "../../src/project/instance"
 import { SessionID, MessageID } from "../../src/session/schema"
 import { Instruction } from "../../src/session/instruction"
 import { ReadTool } from "../../src/tool/read"

+ 0 - 1
packages/opencode/test/tool/skill.test.ts

@@ -5,7 +5,6 @@ import path from "path"
 import { pathToFileURL } from "url"
 import type { Permission } from "../../src/permission"
 import type { Tool } from "@/tool/tool"
-import { Instance } from "../../src/project/instance"
 import { SkillTool } from "../../src/tool/skill"
 import { ToolRegistry } from "@/tool/registry"
 import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"

+ 0 - 1
packages/opencode/test/tool/write.test.ts

@@ -3,7 +3,6 @@ import { Effect, Layer } from "effect"
 import path from "path"
 import fs from "fs/promises"
 import { WriteTool } from "../../src/tool/write"
-import { Instance } from "../../src/project/instance"
 import { LSP } from "@/lsp/lsp"
 import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { Bus } from "../../src/bus"

Неке датотеке нису приказане због велике количине промена