Skip to content

fix(state): legacy sqlite adoption, correct migration semantics, atomic memory migration, surfaced close errors - #201

Merged
ScriptedAlchemy merged 1 commit into
mainfrom
fix/pr171-state-migration
Sep 1, 2026
Merged

fix(state): legacy sqlite adoption, correct migration semantics, atomic memory migration, surfaced close errors#201
ScriptedAlchemy merged 1 commit into
mainfrom
fix/pr171-state-migration

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

Fixes the five state-driver findings from #171/#177 review:

  • sqlite.ts:230 (P1): root-mode opens now discover and adopt the pre-fix(state): review follow-ups from #142/#149 — idempotency ordering, head verification, replay across migrations #171 legacy filename (hex(id)[0:12] suffix, verified against shipped history), including -wal/-shm sidecars (renamed before the main file so un-checkpointed WAL commits survive), with a cross-process race fallback.
  • sqlite.ts:685 (P1): schema migration no longer skips legacy NULL-state rows — a new result_state column holds committed results, and NULL legacy event results are reconstructed by sequential journal replay before migration, restoring idempotent replay for those keys.
  • sqlite.ts:687 (P1): migration rewrites only committed results; the state column (commit input, e.g. reset seeds) is preserved verbatim for retry comparison.
  • memory-driver.ts:329 (P2): in-memory migration builds the new keys map fully and swaps on success — a throwing migration leaves the store untouched.
  • sqlite.ts:778 (P2, chore: deslop pass over the wave 3.5 delta #177): DatabaseSync.close() failures now propagate when the scope is otherwise succeeding; they're only suppressed when protecting an original failure.

Test plan

  • Legacy-format fixture DBs (old filename, NULL-state + reset rows, live un-checkpointed WAL) opened by the new driver: adoption, replay, and reset-input preservation asserted (state-sqlite + state-kernel: 58/58)
  • Memory migration atomicity regression (failed migration leaves store consistent, later migration succeeds)
  • Close-failure propagation regression
  • pnpm typecheck, pnpm lint clean

Adopt legacy SQLite databases with their WAL sidecars, retain canonical reset inputs while migrating committed results, make memory migrations atomic, and surface successful-scope close failures.
@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3734f57

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

@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-01T20:58:53.524830Z 3734f57 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
ScriptedAlchemy merged commit ae7722c into main Sep 1, 2026
2 of 9 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3734f57e9b

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +718 to +720
replayState = definition.reduce(
replayState as TState,
{ name: record.name, payload: record.payload } as AgentStateEvent<TEvents>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not replay legacy rows with the current reducer

When a migration changes the state shape, reducer behavior, or event schema, these legacy rows were produced by the old reducer but are reconstructed with definition.reduce from the new definition. For example, a v1 { count } state migrated to v2 { value } will either fail while opening or produce a result that never existed, so retrying an old idempotency key returns incorrect state. Legacy post-commit results cannot safely be inferred with current-version reducer semantics; they need version-appropriate recovery or a fail-closed compatibility strategy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #213 (merged as 941aa08). Legacy event rows with no stored committed result are no longer replayed through the current-version reducer during a schema migration: the journal-head row's result is recovered from the authoritative materialized head state, and earlier unrecoverable rows now fail closed with a typed migration-failure error instructing the operator to materialize results under the old definition (or restore a backup) before migrating. Regression tests cover both the fail-closed path and a shape-changing v1→v2 migration replaying an old idempotency key from preserved state.

Comment on lines +852 to +854
return Exit.isFailure(exit)
? close.pipe(Effect.catch(() => Effect.void))
: close;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve initialization errors when close also fails

If initialize() fails and db.close() also throws, the exit received here is the managed layer scope's disposal exit, not the earlier initialization exit, because initialization runs separately through runtime.run(). Consequently runtime.close() at line 868 takes the unsuppressed branch and its close error replaces the original corruption or migration error, contrary to the intended “do not mask the caller's failure” behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #213 (merged as 941aa08). The open() initialization-failure path now suppresses a runtime.close() failure and rethrows the original initialization error, so a corruption/migration error is no longer masked by a subsequent close failure. The #208 finalizer contract (infallible via orDie on the success path) is unchanged. Regression test: preserves the initialization error when database close also fails.

}, true);
return Exit.isFailure(exit)
? close.pipe(Effect.catch(() => Effect.void))
: close;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close every store before propagating a close failure

Now that this finalizer can reject, closing a root-backed driver with multiple open stores stops at the first failing store.close() in the driver loop at line 790. Later stores are never closed, remain operable with live database connections, and cannot be retried because the driver caches the rejected closing promise. The driver should attempt all store closures and propagate the collected/first failure only afterward.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in #213 (merged as 941aa08). Driver close() now attempts every open store's closure, collecting failures, and only then propagates the first failure — no store is left with a live connection after a partial-close. Regression test: attempts every store close before propagating the first close failure (asserts both stores closed and both read as store-closed afterwards).

ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
acquireRelease release finalizers cannot carry an error channel, so the
runtime package's declaration build failed on every job. A close failure
on the success path now dies (still visible) instead of failing.
ScriptedAlchemy added a commit that referenced this pull request Sep 1, 2026
…p) (#208)

* fix(ci): keep route-unit proofs out of the example plain test pool

* fix(ci): defer to the mainline Skill IR digest canonicalization (#191)

* fix(state): keep the sqlite close finalizer infallible (#201 follow-up)

acquireRelease release finalizers cannot carry an error channel, so the
runtime package's declaration build failed on every job. A close failure
on the success path now dies (still visible) instead of failing.
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/pr171-state-migration branch September 1, 2026 21:10
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