Skip to content

refactor(state): Effect-native kernel internals behind the unchanged API (wave 3.5 stage 1) - #154

Merged
ScriptedAlchemy merged 1 commit into
mainfrom
wave35/state-effect-kernel
Sep 1, 2026
Merged

refactor(state): Effect-native kernel internals behind the unchanged API (wave 3.5 stage 1)#154
ScriptedAlchemy merged 1 commit into
mainfrom
wave35/state-effect-kernel

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

Wave 3.5 stage 1: the #98 state kernel becomes the exemplar Effect migration. Internals are Effect v4; the public API (defineState, dispatch, read, changes, reset, the typed AgentStateError codes, Promise shapes) is byte-for-byte unchanged, enforced by the untouched conformance suite.

  • Scope owns lifecycles. Each sqlite store's DatabaseSync connection lives in a Layer behind a per-store scoped runtime (makeScopedEffectRuntime from the stage-0 boundary); store.close() disposes the runtime and the connection closes as the scope finalizer. BEGIN IMMEDIATE/COMMIT/ROLLBACK is one Effect.acquireUseRelease: rollback on failure, and a failed COMMIT still rolls back before re-raising so the connection never holds a transaction open.
  • Typed error channel. Kernel fail-closed states ride Effect.Effect<A, AgentStateError>; sqlite/system throws map to the existing corrupt/unavailable/busy→unavailable codes; unexpected throws stay Effect defects (with the transaction still rolled back — new test pins the defect passing through rejects.toBe(defect) with revision 0 preserved). The Promise edge goes through the stage-0 boundary module only; the boundary lint rule keeps it that way.
  • Close-race hardening that fell out of scoping the lifecycles: driver close() now settles pending open()s with store-closed before resolving, and repeated closes share one settlement. New race tests pin this for both drivers.
  • Packaging boundary extended: root/plugin entries are additionally asserted to contain no from "effect" / Effect.runPromise bytes, so stateless projects ship zero effect cost.
  • Pre-existing example fix (not caused by this PR): examples/rsc-agent-runtime host-artifacts.test.ts failed on pristine main because feat(test): route-unit consumer test harness (#103 stage 1) #151 added react as an optional peer of agent-bundle. The assertion now bans hard deps but allows peers explicitly marked optional in peerDependenciesMeta.
  • Dogfood feedback folded back: agent-patterns/effect-scope.md gains the transaction idiom (acquireUseRelease + rollback-on-failed-commit) the rewrite settled on.
  • Changeset: patch (internal rewrite, no API change).

Parity bar (all unchanged suites, all green)

  • 18-case conformance suite against both drivers: green (unit pool: 132 passed / 1 documented durable-only skip)
  • cross-process two-writer + SIGKILL proofs: green (state-sqlite-cross-process.test.ts)
  • packaging-boundary test incl. new effect-bytes assertions: green
  • examples/rsc-agent-runtime: 168 passed / 6 skipped / 0 failed
  • pnpm eval:spot: green
  • lint (incl. boundary rule) + runtime & example typechecks: green
  • hook cold-start budget check: 37 ms median vs 39.74 ms baseline — no regression (state entry grew 17.6→24.2 kB with bundled effect; hooks and root entries unaffected)

…API (wave 3.5 stage 1)

- sqlite driver: connection owned by a Layer + per-store scoped runtime;
  BEGIN IMMEDIATE / COMMIT / ROLLBACK as Effect.acquireUseRelease with
  rollback-on-failed-commit; SqliteConnection as a Context.Service
- typed AgentStateError error channel end to end; sqlite/system throws map to
  the existing corrupt/unavailable codes, unexpected throws stay defects and
  still roll the transaction back
- memory driver: same stateEffect channel; store close is the scope finalizer
- driver close now settles pending opens with store-closed before resolving
  (new race tests pin it for both drivers)
- packaging test additionally proves root/plugin entries ship no effect bytes
- host-artifacts example test: allow the optional react peer that #151 added
  (pre-existing failure on main; hard deps stay banned)
- agent-patterns/effect-scope.md: document the transaction idiom the rewrite
  settled on (dogfood feedback)
@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 180e708

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@agent-bundle/runtime Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ScriptedAlchemy
ScriptedAlchemy merged commit 3e891ac into main Sep 1, 2026
2 of 9 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T11:32:55.209527Z 180e708 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
agent-bundle's test helpers import @agent-bundle/runtime; CI was typechecking that package before runtime dist existed, which is why main's post-#154 Verify jobs fail in ~30s.
ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
agent-bundle's test helpers import @agent-bundle/runtime; CI was typechecking that package before runtime dist existed, which is why main's post-#154 Verify jobs fail in ~30s.
ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
… 3.5 stage 2) (#156)

* refactor(runtime): Effect-native dispatcher and stream pipeline (wave 3.5 stage 2)

Rewrite the #145 Flight decode and bound-event pipeline on Effect Streams and fibers while keeping dispatch() and stream() byte-compatible. Refs #152.

* fix(build): emit runtime types before agent-bundle declaration emit

agent-bundle's test helpers import @agent-bundle/runtime; CI was typechecking that package before runtime dist existed, which is why main's post-#154 Verify jobs fail in ~30s.

* docs(runtime): record the verified locked-stream teardown mechanism in comments

The maxEvents hang was not a blocking-interrupt deadlock: the acquireRelease
finalizer called cancel() on the Flight readable while the Flight client held
its reader, the locked-stream rejection became an Effect.promise defect during
scope close, and the event stream's exit never surfaced. Document that at the
scopedAbortSignal fix site and correct the boundary cancel() comment.
ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
…head verification, replay across migrations (#171)

Codex review follow-ups on the state kernel and drivers, re-verified
against the Effect-native internals from #154:

- Both drivers consult the idempotency key before running the reducer
  (#142/#149 P1): a committed key replays its stored result even when the
  reducer would now fail against the current head. Payload validation and
  canonicalization moved ahead of the key lookup; the reducer runs only on
  the append path.
- Committed results replay across migrations (#142 P1): every journal
  record stores its post-commit state (event rows too, in the sqlite
  driver's existing nullable column), and migrations run the same chain
  over the stored results, so replay no longer depends on exact-revision
  history that migrations rebase. Legacy event rows without a stored state
  fall back to journal replay.
- The sqlite driver verifies storage on open (#149 P1): journal continuity
  via expectConsistentJournal (a deleted intermediate row fails closed) and
  the materialized head against journal replay (a schema-valid hand-edited
  head fails closed); with a pending migration the head is checked against
  the last stored post-commit state instead.
- sanitizedFileName hashes the complete definition id (sha-256) instead of
  hex-encoding its first six bytes (#149 P2), so ids sharing a sanitized
  prefix get isolated database files.
- isJsonSafe rejects sparse arrays (#142 P2): holes no longer canonicalize
  like dense arrays under permissive schemas.

The conformance suite pins the corrected semantics for both drivers:
replay-before-reduce, replay across migrations, and prefix-colliding id
isolation; sqlite corruption tests cover the two new fail-closed opens.
@ScriptedAlchemy
ScriptedAlchemy deleted the wave35/state-effect-kernel branch September 3, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant