Skip to content

refactor(lifecycle): bootstrap as pure orchestration#25510

Merged
kitlangton merged 3 commits intodevfrom
kit/instance-scope-fibers
May 3, 2026
Merged

refactor(lifecycle): bootstrap as pure orchestration#25510
kitlangton merged 3 commits intodevfrom
kit/instance-scope-fibers

Conversation

@kitlangton
Copy link
Copy Markdown
Contributor

@kitlangton kitlangton commented May 3, 2026

Summary

Three small, focused refactors that fix the original forkDetach leak by aligning every service to the established InstanceState.make pattern. No new lifecycle concepts, no new scopes, just consistency.

The problem: bootstrap.run used Effect.forkDetach for per-service init fibers. Those forks deliberately escaped any scope and ran forever — cleanup happened incidentally because each service also used InstanceState.make to register a per-directory disposer that fired on dispose. The asymmetry was the leak: fork lifetime was independent of instance lifetime, papered over by the registry walking.

The fix: Move the slow work into each service's InstanceState.make body, where Effect.forkScoped against the per-instance state scope handles cleanup natively. After this, bootstrap.run is pure orchestration: load config, await plugin, await each service's fast init() materialization, return. Every service self-manages its own background work.

Commits

  1. refactor(file/watcher): self-fork ParcelWatcher subscribes — wrap the synchronous subscribe(...) calls in Effect.forkScoped. FileWatcher was the only service that didn't already self-fork; this brings it in line with share-next, vcs, snapshot, file. Subscribe still completes before its first event, but init() no longer blocks on it.
  2. refactor(project): own /init command subscription via Project.init() — move the Command.Event.Executed → setInitialized wiring out of bootstrap and into a Project.init() that uses the InstanceState.make pattern. Same shape as ShareNext.init. Public init() returns Effect<void>. Project.layer now requires Bus.
  3. refactor(bootstrap): pure orchestration, no fork — replace forkDetach with awaited Effect.forEach over every service's init() (concurrent, discard, per-init catchCause). Interface.run stays Effect<void> with R = never.

Why this shape

Each service already owns a per-instance scope via InstanceState.make's ScopedCache per-key scope. That scope dies on dispose (via the existing instance-registry walk). Bootstrap doesn't need to introduce a parallel scope mechanism — the existing one is sufficient if every service uses it consistently. This PR just makes that consistent.

Test plan

  • bun run typecheck clean
  • bun run test test/project/ test/agent/plugin-agent-regression.test.ts test/file/watcher.test.ts — 77 pass / 1 skip / 0 fail

Out of scope

  • Retiring instance-registry.ts — the registry works and isn't load-bearing in a fragile way; aesthetic only. If we ever do retire it, the replacement is "let InstanceState.make install its cleanup as a finalizer on a per-instance scope discoverable from InstanceContext," but there's no current need.
  • Effect.forkDetach in config/config.ts:577 and control-plane/workspace.ts:810 (separate ownership; not bootstrap fibers).

@kitlangton kitlangton force-pushed the kit/instance-scope-fibers branch 2 times, most recently from f0da764 to 380aeff Compare May 3, 2026 01:40
@@ -1,7 +1,12 @@
import { Context, Effect } from "effect"
import { Context, Effect, Scope } from "effect"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

import { Context, Effect } from "effect"
import { Context, Effect, Scope } from "effect"

export interface Interface {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again test

kitlangton added 3 commits May 2, 2026 22:20
Wrap the synchronous subscribe(...) calls in Effect.forkScoped so they
run in the background of FileWatcher's per-instance state scope,
matching every other service (share-next, vcs, snapshot, file). State
materialization no longer blocks on ParcelWatcher.subscribe; the
subscribe still completes before its first event is delivered, but
init returns immediately.

This is the missing piece that lets InstanceBootstrap stop forking
init() calls itself — every service now self-manages its own slow
work against its own per-instance scope.
Move the Command.Event.Executed → setInitialized wiring out of
InstanceBootstrap.run and into Project.init(). The subscription was
added in #3677 directly to bootstrap when it was still a Promise
function. With Project as a proper Effect Service it belongs there —
the project is listening to its own /init event.

Implementation matches the established InstanceState.make pattern (same
shape as ShareNext, MCP, Plugin, etc.): the make body holds the
subscription via Effect.forkScoped against the per-instance state
scope, and init() just materializes the state. The public
Interface.init returns Effect<void> — no Scope leak.

Project.layer now requires Bus.Service in R; defaultLayer adds
Layer.provide(Bus.defaultLayer) so consumers don't need to know.
Replace Effect.forkDetach with an awaited Effect.forEach. Each
service's init() materializes its per-instance state, and the slow
background work is forked inside the service's InstanceState.make body
against the per-instance state scope (the established pattern across
share-next, vcs, snapshot, and now file/watcher).

Bootstrap is now pure orchestration: load config, await plugin.init,
await each service.init in parallel. Returns when materialization
completes. Failures are logged per-service and never propagate.

Interface.run stays Effect<void> with R = never — no Scope leak in
the public API. The fix to the original forkDetach leak lives in each
service: their state-scoped forks die when the per-instance state
scope is invalidated on dispose.
@kitlangton kitlangton force-pushed the kit/instance-scope-fibers branch from 380aeff to 2a601a7 Compare May 3, 2026 02:21
@kitlangton kitlangton changed the title feat(lifecycle): tie bootstrap fibers to instance scope refactor(lifecycle): bootstrap as pure orchestration May 3, 2026
@kitlangton kitlangton enabled auto-merge (squash) May 3, 2026 02:22
@kitlangton kitlangton disabled auto-merge May 3, 2026 02:26
@kitlangton kitlangton merged commit ad05a46 into dev May 3, 2026
11 checks passed
@kitlangton kitlangton deleted the kit/instance-scope-fibers branch May 3, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant