Skip to content

wave 3.5: Effect-native internals #152

Closed

Description

@ScriptedAlchemy

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/runtime and packages/agent-bundle/src/dev + the state kernel; each package gets exactly one boundary module (src/effect/boundary.ts) owning runPromise edges, AbortSignal ↔ interruption bridges, and error-channel mapping to the existing typed contracts. Ad-hoc runPromise outside boundary modules is mechanically enforced.
  • zod stays at all schema boundaries (MCP SDK interop; recorded G-decisions).

Stages

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

  1. ScriptedAlchemy commented on Sep 1, 2026

    @ScriptedAlchemy
    OwnerAuthor

    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 Flight ReadableStream from an acquireRelease finalizer while the Flight client held its reader; the locked-stream rejection became an Effect.promise defect during scope close and wedged the event stream's exit. Fixed with scopedAbortSignal + Stream.interruptWhen on 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.

  2. ScriptedAlchemy commented on Sep 1, 2026

    @ScriptedAlchemy
    OwnerAuthor

    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.

    PR Subsystem Merge SHA
    #158 Dev-seam boundary module (packages/agent-bundle/src/effect/boundary.ts, exact effect@4.0.0-rc.112 pin, boundary-lint coverage) 70deb9987a0e81f7471764e317c0b963ff6e5116
    #159 MCP session lifecycles as scoped resources; #134 fail-closed contract on the typed error channel (McpSessionStaleEpochError) 0343e04a59740015c6e3cd187c31de22e2674838
    #160 Coalescing rebuild scheduler on fiber + Semaphore(1) + shared Deferred 87711c4f81b2c85bfccf51dd1fc9e41439a93775
    #161 EpochStore staging/leases/recovery: process-wide lease mutexes as Semaphores, publication as an Exit-aware compensation saga, retention via forEach + per-element Exit f596377618a634b7c0ba76d106886a8da76bd80e
    #162 Conventions doc: stage-3 outcomes, helped/hurt addendum, unstable-adoption record (none) 44e31b56c0ea7debfff0b5fd9bf8764934e80f41

    One 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 in foreground-server.ts) and ~15 tests pin sync replay/removal/ordering — which Effect PubSub (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.

  3. ScriptedAlchemy commented on Sep 1, 2026

    @ScriptedAlchemy
    OwnerAuthor

    Wave 4 Lane A / #97 proof dependency is landed.

    Lane C can proceed from current main.

  4. ScriptedAlchemy commented on Sep 1, 2026

    @ScriptedAlchemy
    OwnerAuthor

    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's
    in-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 a
    separate OS process over real stdio — the only process evidence). Plus
    expectEvents over the #140 render-event stream: sequence-tolerant matching
    with 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:projection pool is
    in-process only (22 tests, no build/pack/spawn).

    Coordination outcomes:

    • Lane B (feat(cli): routed CLI from src/cli/** routes (#102 stage 2) #178): reconciled rather than stubbed. invokeCli dispatches
      through the product's own runGeneratedCliEntry over the compiled command
      graph the test manifest now carries (cliCommands), so resolution, argv
      projection, help, --version, and the exit-code policy are the product's.
      The runRscCli fallback was not needed, so there is no follow-up here.
    • Lane A (feat(examples): adopt semantic event route #182): landed in time, so the PostToolUse proof ships in this PR
      rather than as a follow-up. examples/rsc-agent-runtime gains a route-unit
      pool proving the migrated src/events/tool/after.tsx renders from a real
      Claude PostToolUse envelope as a compiled event-route, and the same route
      renders through renderRouteEvents in 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):

    1. Generated executables crashed on any JSX-authored route. Route entries
      were bundled without the React plugin, so JSX lowered to the classic
      React.createElement factory that no generated entry or Flight worker has in
      scope — every documented .tsx route failed at run time with React is not defined. It survived because every build-level test authored routes with an
      explicit createElement import.
    2. Event routes rendered with the wrong props. A route written against the
      public AgentEventRouteProps received undefined for canonical and
      native; renderRoute now performs the same unwrapping the generated Flight
      worker 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 entry
    aliases 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.ts
    asserts against exactly that.

    ⚠️ Heads-up, unrelated to this lane: packages/agent-bundle/tests/dev-services.test.ts
    ("creates an exact deeply frozen root-independent project context") fails on
    current main with a differing project-context modelDigest. Reproduced on a
    clean origin/main worktree at 6baf597; it passes at #183, so it arrived
    with #184/#185. Whoever owns the skill-IR model digest may want a look.

  5. ScriptedAlchemy commented on Sep 1, 2026

    @ScriptedAlchemy
    OwnerAuthor

    dev-services modelDigest regression — fixed in #191

    Root cause: PR #185 (#185) added skillIr, hostDocuments, and skillTreeLayout to NormalizedSkill, but canonicalizeNormalizedModel was never updated to root-relative those new fields. Absolute temp-directory paths (skillIr.source, skillIr.resources[].source, etc.) leaked into modelDigest, so two byte-identical fixtures in different directories produced different digests — exactly what creates an exact deeply frozen root-independent project context asserts against.

    Fix: Extend canonicalizeNormalizedModel to 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.ts from #186); those are outside this lane.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmeta-frameworkAgent Bundle compiler-coupled meta-framework

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions