Skip to content

Commit 5fc656e

Browse files
authored
docs(opencode): add instance context migration plan (#22529)
1 parent fe01fa7 commit 5fc656e

2 files changed

Lines changed: 314 additions & 0 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
# Instance context migration
2+
3+
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.
4+
5+
## Goal
6+
7+
End state:
8+
9+
- request, CLI, TUI, and tool entrypoints shift into an instance through Effect, not `Instance.provide(...)`
10+
- Effect code reads the current instance from `InstanceRef` or its eventual replacement, not from ALS-backed sync getters
11+
- per-directory boot, caching, and disposal are scoped Effect resources, not a module-level `Map<string, Promise<InstanceContext>>`
12+
- ALS remains only as a temporary bridge for native callback APIs that fire outside the Effect fiber tree
13+
14+
## Current split
15+
16+
Today `src/project/instance.ts` still owns two separate concerns:
17+
18+
- ambient current-instance context through `LocalContext` / `AsyncLocalStorage`
19+
- per-directory boot and deduplication through `cache: Map<string, Promise<InstanceContext>>`
20+
21+
At the same time, the Effect side already exists:
22+
23+
- `src/effect/instance-ref.ts` provides `InstanceRef` and `WorkspaceRef`
24+
- `src/effect/run-service.ts` already attaches those refs when a runtime starts inside an active instance ALS context
25+
- `src/effect/instance-state.ts` already prefers `InstanceRef` and only falls back to ALS when needed
26+
27+
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".
28+
29+
## End state shape
30+
31+
Near-term target shape:
32+
33+
```ts
34+
InstanceScope.with({ directory, workspaceID }, effect)
35+
```
36+
37+
Responsibilities of `InstanceScope.with(...)`:
38+
39+
- resolve `directory`, `project`, and `worktree`
40+
- acquire or reuse the scoped per-directory instance environment
41+
- provide `InstanceRef` and `WorkspaceRef`
42+
- run the caller's Effect inside that environment
43+
44+
Code inside the boundary should then do one of these:
45+
46+
```ts
47+
const ctx = yield * InstanceState.context
48+
const dir = yield * InstanceState.directory
49+
```
50+
51+
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.
52+
53+
## Migration phases
54+
55+
### Phase 1: stop expanding the legacy surface
56+
57+
Rules for all new code:
58+
59+
- do not add new `Instance.directory`, `Instance.worktree`, `Instance.project`, or `Instance.current` reads inside Effect code
60+
- do not add new `Instance.provide(...)` boundaries unless there is no Effect-native seam yet
61+
- use `InstanceState.context`, `InstanceState.directory`, or an explicit `ctx` parameter inside Effect code
62+
63+
Success condition:
64+
65+
- the file inventory below only shrinks from here
66+
67+
### Phase 2: remove direct sync getter reads from Effect services
68+
69+
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.
70+
71+
Primary batch, highest payoff:
72+
73+
- `src/file/index.ts`
74+
- `src/lsp/server.ts`
75+
- `src/worktree/index.ts`
76+
- `src/file/watcher.ts`
77+
- `src/format/formatter.ts`
78+
- `src/session/index.ts`
79+
- `src/project/vcs.ts`
80+
81+
Mechanical replacement rule:
82+
83+
- `Instance.directory` -> `ctx.directory` or `yield* InstanceState.directory`
84+
- `Instance.worktree` -> `ctx.worktree`
85+
- `Instance.project` -> `ctx.project`
86+
87+
Do not thread strings manually through every public method if the service already has access to Effect context.
88+
89+
### Phase 3: convert entry boundaries to provide instance refs directly
90+
91+
After the service bodies stop assuming ALS, move the top-level boundaries to shift into Effect explicitly.
92+
93+
Main boundaries:
94+
95+
- HTTP server middleware and experimental `HttpApi` entrypoints
96+
- CLI commands
97+
- TUI worker / attach / thread entrypoints
98+
- tool execution entrypoints
99+
100+
These boundaries should become Effect-native wrappers that:
101+
102+
- decode directory / workspace inputs
103+
- resolve the instance context once
104+
- provide `InstanceRef` and `WorkspaceRef`
105+
- run the requested Effect
106+
107+
At that point `Instance.provide(...)` becomes a legacy adapter instead of the normal code path.
108+
109+
### Phase 4: replace promise boot cache with scoped instance runtime
110+
111+
Once boundaries and services both rely on Effect context, replace the module-level promise cache in `src/project/instance.ts`.
112+
113+
Target replacement:
114+
115+
- keyed scoped runtime or keyed layer acquisition for each directory
116+
- reuse via `ScopedCache`, `LayerMap`, or another keyed Effect resource manager
117+
- cleanup performed by scope finalizers instead of `disposeAll()` iterating a Promise map
118+
119+
This phase should absorb the current responsibilities of:
120+
121+
- `cache` in `src/project/instance.ts`
122+
- `boot(...)`
123+
- most of `disposeInstance(...)`
124+
- manual `reload(...)` / `disposeAll()` fan-out logic
125+
126+
### Phase 5: shrink ALS to callback bridges only
127+
128+
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.
129+
130+
Known bridge cases today:
131+
132+
- `src/file/watcher.ts`
133+
- `src/session/llm.ts`
134+
- some LSP and plugin callback paths
135+
136+
If those libraries become fully wrapped in Effect services, the remaining `Instance.bind(...)` uses can disappear too.
137+
138+
### Phase 6: delete the legacy sync API
139+
140+
Only after earlier phases land:
141+
142+
- remove broad use of `Instance.current`, `Instance.directory`, `Instance.worktree`, `Instance.project`
143+
- reduce `src/project/instance.ts` to a thin compatibility shim or delete it entirely
144+
- remove the ALS fallback from `InstanceState.context`
145+
146+
## Inventory of direct legacy usage
147+
148+
Direct legacy usage means any source file that still calls one of:
149+
150+
- `Instance.current`
151+
- `Instance.directory`
152+
- `Instance.worktree`
153+
- `Instance.project`
154+
- `Instance.provide(...)`
155+
- `Instance.bind(...)`
156+
- `Instance.restore(...)`
157+
- `Instance.reload(...)`
158+
- `Instance.dispose()` / `Instance.disposeAll()`
159+
160+
Current total: `54` files in `packages/opencode/src`.
161+
162+
### Core bridge and plumbing
163+
164+
These files define or adapt the current bridge. They should change last, after callers have moved.
165+
166+
- `src/project/instance.ts`
167+
- `src/effect/run-service.ts`
168+
- `src/effect/instance-state.ts`
169+
- `src/project/bootstrap.ts`
170+
- `src/config/config.ts`
171+
172+
Migration rule:
173+
174+
- keep these as compatibility glue until the outer boundaries and inner services stop depending on ALS
175+
176+
### HTTP and server boundaries
177+
178+
These are the current request-entry seams that still create or consume instance context through the legacy helper.
179+
180+
- `src/server/instance/middleware.ts`
181+
- `src/server/instance/index.ts`
182+
- `src/server/instance/project.ts`
183+
- `src/server/instance/workspace.ts`
184+
- `src/server/instance/file.ts`
185+
- `src/server/instance/experimental.ts`
186+
- `src/server/instance/global.ts`
187+
188+
Migration rule:
189+
190+
- move these to explicit Effect entrypoints that provide `InstanceRef` / `WorkspaceRef`
191+
- do not move these first; first reduce the number of downstream handlers and services that still expect ambient ALS
192+
193+
### CLI and TUI boundaries
194+
195+
These commands still enter an instance through `Instance.provide(...)` or read sync getters directly.
196+
197+
- `src/cli/bootstrap.ts`
198+
- `src/cli/cmd/agent.ts`
199+
- `src/cli/cmd/debug/agent.ts`
200+
- `src/cli/cmd/debug/ripgrep.ts`
201+
- `src/cli/cmd/github.ts`
202+
- `src/cli/cmd/import.ts`
203+
- `src/cli/cmd/mcp.ts`
204+
- `src/cli/cmd/models.ts`
205+
- `src/cli/cmd/plug.ts`
206+
- `src/cli/cmd/pr.ts`
207+
- `src/cli/cmd/providers.ts`
208+
- `src/cli/cmd/stats.ts`
209+
- `src/cli/cmd/tui/attach.ts`
210+
- `src/cli/cmd/tui/plugin/runtime.ts`
211+
- `src/cli/cmd/tui/thread.ts`
212+
- `src/cli/cmd/tui/worker.ts`
213+
214+
Migration rule:
215+
216+
- converge these on one shared `withInstance(...)` Effect entry helper instead of open-coded `Instance.provide(...)`
217+
- after that helper is proven, inline the legacy implementation behind an Effect-native scope provider
218+
219+
### Tool boundary code
220+
221+
These tools mostly use direct getters for path resolution and repo-relative display logic.
222+
223+
- `src/tool/apply_patch.ts`
224+
- `src/tool/bash.ts`
225+
- `src/tool/edit.ts`
226+
- `src/tool/lsp.ts`
227+
- `src/tool/multiedit.ts`
228+
- `src/tool/plan.ts`
229+
- `src/tool/read.ts`
230+
- `src/tool/write.ts`
231+
232+
Migration rule:
233+
234+
- expose the current instance as an explicit Effect dependency for tool execution
235+
- keep path logic local; avoid introducing another global singleton for tool state
236+
237+
### Effect services still reading ambient instance state
238+
239+
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.
240+
241+
- `src/agent/agent.ts`
242+
- `src/config/tui-migrate.ts`
243+
- `src/file/index.ts`
244+
- `src/file/watcher.ts`
245+
- `src/format/formatter.ts`
246+
- `src/lsp/client.ts`
247+
- `src/lsp/index.ts`
248+
- `src/lsp/server.ts`
249+
- `src/mcp/index.ts`
250+
- `src/project/vcs.ts`
251+
- `src/provider/provider.ts`
252+
- `src/pty/index.ts`
253+
- `src/session/index.ts`
254+
- `src/session/instruction.ts`
255+
- `src/session/llm.ts`
256+
- `src/session/system.ts`
257+
- `src/sync/index.ts`
258+
- `src/worktree/index.ts`
259+
260+
Migration rule:
261+
262+
- replace direct getter reads with `yield* InstanceState.context` or a yielded `ctx`
263+
- isolate `Instance.bind(...)` callers and convert only the truly callback-driven edges to bridge mode
264+
265+
### Highest-churn hotspots
266+
267+
Current highest direct-usage counts by file:
268+
269+
- `src/file/index.ts` - `18`
270+
- `src/lsp/server.ts` - `14`
271+
- `src/worktree/index.ts` - `12`
272+
- `src/file/watcher.ts` - `9`
273+
- `src/cli/cmd/mcp.ts` - `8`
274+
- `src/format/formatter.ts` - `8`
275+
- `src/tool/apply_patch.ts` - `8`
276+
- `src/cli/cmd/github.ts` - `7`
277+
278+
These files should drive the first measurable burn-down.
279+
280+
## Recommended implementation order
281+
282+
1. Migrate direct getter reads inside Effect services, starting with `file`, `lsp`, `worktree`, `format`, and `session`.
283+
2. Add one shared Effect-native boundary helper for CLI / tool / HTTP entrypoints so we stop open-coding `Instance.provide(...)`.
284+
3. Move experimental `HttpApi` entrypoints to that helper so the new server stack proves the pattern.
285+
4. Convert remaining CLI and tool boundaries.
286+
5. Replace the promise cache with a keyed scoped runtime or keyed layer map.
287+
6. Delete ALS fallback paths once only callback bridges still depend on them.
288+
289+
## Definition of done
290+
291+
This migration is done when all of the following are true:
292+
293+
- new requests and commands enter an instance by providing Effect context, not ALS
294+
- Effect services no longer read `Instance.directory`, `Instance.worktree`, `Instance.project`, or `Instance.current`
295+
- `Instance.provide(...)` is gone from normal request / CLI / tool execution
296+
- per-directory boot and disposal are handled by scoped Effect resources
297+
- `Instance.bind(...)` is either gone or confined to a tiny set of native callback adapters
298+
299+
## Tracker and worktree
300+
301+
Active tracker items:
302+
303+
- `lh7l73` - overall `HttpApi` migration
304+
- `yobwlk` - remove direct `Instance.*` reads inside Effect services
305+
- `7irl1e` - replace `InstanceState` / legacy instance caching with keyed Effect layers
306+
307+
Dedicated worktree for this transition:
308+
309+
- path: `/Users/kit/code/open-source/opencode-worktrees/instance-effect-shift`
310+
- branch: `kit/instance-effect-shift`

packages/opencode/specs/effect/migration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Use `makeRuntime` (from `src/effect/run-service.ts`) to create a per-service `Ma
1313

1414
Rule of thumb: if two open directories should not share one copy of the service, it needs `InstanceState`.
1515

16+
## Instance context transition
17+
18+
See `instance-context.md` for the phased plan to remove the legacy ALS / promise-backed `Instance` helper and move request / CLI / tool boundaries onto Effect-provided instance scope.
19+
1620
## Service shape
1721

1822
Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions:

0 commit comments

Comments
 (0)