@@ -99,6 +99,61 @@ public edge. Pattern files:
9999- [ agent-patterns/effect-concurrency.md] ( ../agent-patterns/effect-concurrency.md )
100100- [ agent-patterns/effect-errors.md] ( ../agent-patterns/effect-errors.md )
101101
102+ ## Stage 3 (dev seam) outcomes
103+
104+ One subsystem per PR, all behind unchanged Promise APIs and wire contracts
105+ (#158 boundary, #159 MCP session lifecycles, #160 rebuild scheduler,
106+ #161 EpochStore). Leaf filesystem/SDK helpers stay imperative and are
107+ identity-lifted through ` src/effect/lift.ts ` ; the orchestration —
108+ lifecycles, mutual exclusion, coalescing, compensation, failure
109+ aggregation — is Effect.
110+
111+ ** ` ProjectEventHub ` (the SSE hub) stays imperative.** Its public contract is
112+ * synchronous* re-entrant fan-out: ` publish ` delivers to listeners in the same
113+ turn (foreground shutdown depends on tombstone frames reaching socket buffers
114+ before one ` setImmediate ` , ` foreground-server.ts ` documents the invariant),
115+ ` subscribe ` replays retained history plus replay-gap frames synchronously,
116+ and listener failures remove the subscription mid-dispatch. Effect ` PubSub `
117+ is an asynchronous fan-out structure; fiber-delivered events would change
118+ that observable contract, and driving a ` PubSub ` through unsafe synchronous
119+ drains would re-implement today's dispatch loop with none of Effect's
120+ guarantees. Revisit only if the hub's consumers ever move onto fibers
121+ end-to-end.
122+
123+ ### Stage 3 helped / hurt addendum
124+
125+ Helped:
126+
127+ - ` Semaphore.make(1) ` + ` withPermit ` is a drop-in for the hand-rolled serial
128+ queues (FIFO waiters), including the process-wide per-project lease mutex
129+ map in the epoch store.
130+ - ` Deferred ` gives coalesced rebuild waiters one shared completion;
131+ ` Effect.onExit ` sits exactly where a ` .finally ` drain hook sat.
132+ - ` Effect.forEach(..., { concurrency: 'unbounded' }) ` with per-element
133+ ` Effect.exit ` is the exact analogue of ` Promise.allSettled ` for
134+ settle-then-aggregate teardown contracts (` McpSessionServiceCloseError ` ,
135+ ` EpochCleanupError ` , publication rollback ` AggregateError ` ).
136+ - ` Effect.gen({ self: this }, function* (this: X) { ... }) ` keeps ` #private `
137+ member access inside class-internal Effect programs.
138+ - ` Effect.acquireRelease ` + a transferred-ownership flag models "release on
139+ failure only until the constructed resource takes ownership" (MCP session
140+ open chain).
141+
142+ Hurt / gotchas:
143+
144+ - Effect finalizers are infallible by type. Teardown contracts that
145+ * propagate* cleanup failures (the dev seam's last-failure-wins ` finally `
146+ chains, the staging-root removal that replaces the publish outcome) must be
147+ explicit effect sequences — capture the attempt's ` Exit ` , run the cleanup,
148+ then unwrap — never scope finalizers.
149+ - Bare ` Effect.tryPromise(fn) ` wraps rejections in ` Cause.UnknownError ` ;
150+ always route through ` src/effect/lift.ts ` so typed dev errors and raw
151+ rejection values (` AbortSignal.reason ` ) cross the boundary untouched.
152+ - Synchronous admission windows are load-bearing: same-turn rebuild
153+ coalescing and session-close invalidation must not move inside a fiber.
154+ Construct ` Semaphore ` /` Deferred ` with the boundary's ` runSync ` and keep the
155+ admission bookkeeping synchronous.
156+
102157## Banned modules and APIs
103158
104159- ` Effect.runPromise ` / ` runSync ` / ` runFork ` / ` runCallback ` (and ` *With ` /
@@ -108,13 +163,14 @@ public edge. Pattern files:
108163- ` @effect/vitest ` — this repo uses rstest.
109164- ` NodeRuntime.runMain ` / ` BunRuntime ` as a substitute for the boundary.
110165- Ad-hoc ` ManagedRuntime ` outside a boundary module.
111- - ` effect/unstable/* ` until listed below (Stage 2 listed none).
166+ - ` effect/unstable/* ` until listed below (Stages 2 and 3 listed none).
112167
113168## Unstable-module adoptions
114169
115170Re-pin chores re-verify every row. Stage 2 adopts none: Flight is a React
116171binary stream, not Ndjson/SchemaBinary, and no other ` effect/unstable/* `
117- module fits the dispatcher rewrite.
172+ module fits the dispatcher rewrite. Stage 3 also adopts none: the dev seam
173+ needed only stable ` Semaphore ` , ` Deferred ` , ` Scope ` , and ` Exit ` .
118174
119175| Module | Adopted in | Re-verify |
120176| --- | --- | --- |
0 commit comments