Repository navigation
wave 3.5: Effect-native internals #152
Description
Child of #107. Inserted between Wave 3 (complete: #139-#151) and Wave 4 by owner decision on 2026-09-01.
Why
The internals keep hand-rolling what Effect provides: typed error unions (AgentRequestError, typed Observed reasons, AB fail-closed contracts), resource lifecycles (request leases, EpochStore staging/leases/recovery, session teardown), backpressure (#145's pull-gated decode), and manual AbortSignal threading. Waves 4-6 build the biggest concurrency surfaces yet (hook thin-clients under host deadlines, the MCP progress projector, the notices ledger, warm-runtime lifecycle) — building those Effect-native now is cheaper than migrating them later.
Version and surface
Exact pin effect@4.0.0-rc.113 (unified ecosystem version). Full surface available including effect/unstable/* — latest and greatest by default; the exact pin controls exposure, RC bumps are a named re-pin chore (same treadmill pattern as React, #107 R4), and each unstable-module adoption gets a one-line note in docs/effect-conventions.md so re-pins know what to re-verify.
Hard boundary rules
- The public authoring surface stays Promise + zod:
await agent(), route modules,defineState, every subpath export, generated artifact signatures. The four-concept newcomer ledger is untouched; Effect never appears in user-facing types, docs, or examples' user code. - Effect lives in internals of
@agent-bundle/runtimeandpackages/agent-bundle/src/dev+ the state kernel; each package gets exactly one boundary module (src/effect/boundary.ts) owningrunPromiseedges,AbortSignal↔ interruption bridges, and error-channel mapping to the existing typed contracts. Ad-hocrunPromiseoutside boundary modules is mechanically enforced. - zod stays at all schema boundaries (MCP SDK interop; recorded G-decisions).
Stages
- Stage 0 — foundation + agentic feedback loop: exact pin; vendored Effect source at
repos/effectvia git subtree (squash) with AGENTS.md read-only-reference rules and editor excludes;agent-patterns/effect-{stream,scope,concurrency,errors}.mdgenerated from the vendored source; boundary module + bridges;docs/effect-conventions.md; tsgo language service with v4-aware diagnostics; boundary lint enforcement; stdio-hook cold-start baseline (the stage-2 budget gate). - Stage 1 — state kernel internals (the exemplar): Provide an optional durable Agent state kernel #98's kernel + both drivers on Effect (
Scopefor txn/connection lifecycles, typed error channel) behind the unchanged API. Parity: the 18-case conformance suite, cross-process + SIGKILL proofs, packaging boundary,eval:spot. - Stage 2 — runtime dispatcher internals: feat(runtime): stream Suspense replacements through Flight #145's pipeline on Effect
Stream/fibers (native backpressure, interruption),dispatch()/stream()byte-compatible; parity: event-contract tests + example + eval:spot + the cold-start budget. - Stage 3 — dev seam, one subsystem per PR (highest value, highest risk, last):
EpochStore→ Scope/acquireRelease; coalescing rebuilds → fiber + Semaphore/Latch; SSE hub → PubSub-backed with the identical wire contract; MCP session lifecycles (the fix(dev): fail MCP tool calls closed when the pinned epoch vanishes #134 fail-closed contract) as scoped resources. Parity: unchanged DTO/SSE wire contracts + dev/workbench e2e suites. - Stage 4 — policy flip: all NEW internal mechanics in waves 4-6 (hook thin clients Compile semantic Agent event routes into native host hooks #97, MCP progress projector Build a real RSC Agent Document renderer with streaming semantics #96-s4, notices ledger Add recipient-aware notices and capability-gated delivery #99, read-only doctor Add safe managed lifecycle for generated host bundles #101-s1) are built Effect-native from day one behind Promise edges. Sequencing: stage 2 lands before Build a real RSC Agent Document renderer with streaming semantics #96 stage 4 so the progress projector is born on Effect streams.
Out of scope
Compiler pipeline migration, Effect Schema at the public zod boundaries, any public API change.
Landing bar
Scoped tests + typecheck + lint per PR, merge on green (owner policy); parity means existing tests unchanged and green. Fragile-seam discipline: stage 3 PRs land only with full parity; per-test rstest retries cover known flakes.
Activity
refactor(runtime): Effect-native dispatcher and stream pipeline (wave…
refactor(runtime): Effect-native dispatcher and stream pipeline (wave…
ScriptedAlchemy commentedon Sep 1, 2026
Stage 2 (Effect-native dispatcher and stream pipeline) merged in d691cdb (#156).
Root cause of the final event-count hang, verified by bisect + instrumentation: teardown called
cancel()on the FlightReadableStreamfrom anacquireReleasefinalizer while the Flight client held its reader; the locked-stream rejection became anEffect.promisedefect during scope close and wedged the event stream's exit. Fixed withscopedAbortSignal+Stream.interruptWhenon the Flight source (no interaction with the locked stream). Details and stage-3 helped/hurt notes in the PR body.Parity: runtime 144 pass / 1 skip; example 168 pass / 6 skip; typecheck / lint (boundary-lint green) / eval:spot green; cold start 41.82ms vs 39.74ms baseline (+5.2%, under the 15% gate); event-count test 30/30 stress runs.
ScriptedAlchemy commentedon Sep 1, 2026
Wave 3.5 is complete. Stage 3 (dev seam) landed one subsystem per PR, strictly parity-gated — every existing dev/workbench suite passed unchanged, zero pin flips across the stage.
packages/agent-bundle/src/effect/boundary.ts, exacteffect@4.0.0-rc.112pin, boundary-lint coverage)70deb9987a0e81f7471764e317c0b963ff6e5116McpSessionStaleEpochError)0343e04a59740015c6e3cd187c31de22e2674838Semaphore(1)+ sharedDeferred87711c4f81b2c85bfccf51dd1fc9e41439a93775forEach+ per-elementExitf596377618a634b7c0ba76d106886a8da76bd80e44e31b56c0ea7debfff0b5fd9bf8764934e80f41One honest exception, recorded in
docs/effect-conventions.md:ProjectEventHub(the SSE hub) stays imperative. Its public contract is synchronous re-entrant fan-out — foreground shutdown depends on same-tick tombstone delivery (documented inforeground-server.ts) and ~15 tests pin sync replay/removal/ordering — which EffectPubSub(an async fan-out structure) does not model. The SSE wire contract (sequence numbers, replay-gap frames) is untouched either way.Parity evidence per PR is in each PR body; the stage-3 tail runs were full unit pool 1999/0 plus the epoch-heavy integration suites 35/0, lint and typecheck green. Stage 4 (policy flip: new wave 4–6 internals born Effect-native behind Promise edges) is now in force.
ScriptedAlchemy commentedon Sep 1, 2026
Wave 4 Lane A / #97 proof dependency is landed.
363890b69d475d7a79ef50c4067fb49a9158ca87)examples/rsc-agent-runtime/src/events/tool/after.tsxmigration + Claude/Codex schema-conformance replays: feat(examples): adopt semantic event route #182 (9004850090061bab8bacce1ab8635819399b39e8)pnpm eval:spotis green against the compiler-generated Claude event-route wrapper.Lane C can proceed from current
main.ScriptedAlchemy commentedon Sep 1, 2026
Wave 4 Lane C — #103 stage 2 landed: #190, squash
3ae77210e.Proof levels shipped, each labeled in provenance and in every failure message:
mcp-in-memory(real generated MCP server + real MCP client over the SDK'sin-memory transport pair — protocol contract only, explicitly not host
evidence),
cli-dispatch(argv through the routed CLI's own shell, in-process,not a spawned binary),
packed-stdio(built artifact's generated entry as aseparate OS process over real stdio — the only process evidence). Plus
expectEventsover the #140 render-event stream: sequence-tolerant matchingwith the #120 monotonic lesson baked in as its own matcher.
Cost rules held: the packed pool gained no new build and no new pack — the
single journey draws the run-level shared tarball, installs once with the #109
cached args, builds one artifact, spawns one server, and iterates every
per-route assertion inside that one session. The new
test:projectionpool isin-process only (22 tests, no build/pack/spawn).
Coordination outcomes:
invokeClidispatchesthrough the product's own
runGeneratedCliEntryover the compiled commandgraph the test manifest now carries (
cliCommands), so resolution, argvprojection, help,
--version, and the exit-code policy are the product's.The
runRscClifallback was not needed, so there is no follow-up here.rather than as a follow-up.
examples/rsc-agent-runtimegains a route-unitpool proving the migrated
src/events/tool/after.tsxrenders from a realClaude PostToolUse envelope as a compiled
event-route, and the same routerenders through
renderRouteEventsin the projection pool.Two defects the packed level surfaced, both fixed here (Effect-native internals
kept behind Promise edges per this issue's policy, and the harness's public
surfaces stay Promise-shaped):
were bundled without the React plugin, so JSX lowered to the classic
React.createElementfactory that no generated entry or Flight worker has inscope — every documented
.tsxroute failed at run time withReact is not defined. It survived because every build-level test authored routes with anexplicit
createElementimport.public
AgentEventRoutePropsreceivedundefinedforcanonicalandnative;renderRoutenow performs the same unwrapping the generated Flightworker does.
Core-first note for anyone touching the generated MCP entry: the warm Flight
host, artifact epoch, route registration, MCP projection, and event-runtime
lifecycle now live in
src/mcp-server-runtime.ts, which the generated entryaliases and feeds data. Templating any of that back into the entry would split
the artifact from what the in-memory level proves;
tests/entry-shell.test.tsasserts against exactly that.
packages/agent-bundle/tests/dev-services.test.ts("creates an exact deeply frozen root-independent project context") fails on
current
mainwith a differing project-contextmodelDigest. Reproduced on aclean
origin/mainworktree at6baf597; it passes at#183, so it arrivedwith #184/#185. Whoever owns the skill-IR model digest may want a look.
ScriptedAlchemy commentedon Sep 1, 2026
dev-services
modelDigestregression — fixed in #191Root cause: PR #185 (#185) added
skillIr,hostDocuments, andskillTreeLayouttoNormalizedSkill, butcanonicalizeNormalizedModelwas never updated to root-relative those new fields. Absolute temp-directory paths (skillIr.source,skillIr.resources[].source, etc.) leaked intomodelDigest, so two byte-identical fixtures in different directories produced different digests — exactly whatcreates an exact deeply frozen root-independent project contextasserts against.Fix: Extend
canonicalizeNormalizedModelto canonicalize Skill IR and host-document paths (source, resource, sidecar, and diagnostic paths). Product-code change with a patch changeset.Verification:
dev-services.test.ts(26/26), digest-adjacent tests (dev-artifact-service,package-identity,manifest),pnpm typecheck,pnpm lint. Release gates green on #191.Note: full Verify matrix still reports unrelated failures on current
main(hooks.test.ts,runtime-playground.e2e.test.tsfrom #186); those are outside this lane.