Skip to content

fix: load and fork fail closed on unknown runs and stale projections (1.13.0) - #86

Draft
yoheinakajima wants to merge 2 commits into
mainfrom
cursor/load-integrity-fixes-d03d
Draft

yoheinakajima wants to merge 2 commits into
mainfrom
cursor/load-integrity-fixes-d03d

Conversation

@yoheinakajima

@yoheinakajima yoheinakajima commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Closes #81
Closes #82

Reported by @TrendpilotAI, including the reproductions and the #82 acceptance tests.

Governing issue and decision

Invariant and scope

Runtime.load observes an existing canonical run and materializes that run's log into an empty projection. It does not mint run identity, repair a missing runs row, or leave facts in the projection that the log does not describe. Runtime.fork(..., graph_store=) follows the same emptiness rule.

Out of scope:

  • Rebuilding into an isolated store and swapping it in atomically.
  • Automatic clear() and a load-or-create flag.
  • SurrealDB, backend discovery, and a broader run-catalog redesign.
  • A Postgres Runtime.fork. The CLI fork copies events and does not replay into a GraphStore, so there is no projection to refuse.

Failure and compatibility model

Behavior change. Runtime.load(path, run_id=...) used to replay an empty log and upsert_run the requested id, so a typo showed up in list_runs(). It now raises RunNotFoundError (a StorageError and a FileNotFoundError) and does not insert a row or accept an event.

  • reason="missing" — explicit id, the file exists, no catalog row, no events.
  • reason="missing_file" — the SQLite file itself does not exist. The message says so. The file is not created.
  • reason="orphan_events" — events exist for that id but the catalog row does not. Load does not repair the row. The message gives a one-line opt-in: SQLiteEventStore(path, run_id).upsert_run(created_at="<ISO-8601 timestamp>") (Postgres: PostgresEventStore(url, run_id).upsert_run(...)).
  • reason="empty_catalog" — run_id was omitted and an existing store has no runs. Nothing is inserted.

Omitting run_id still loads the most recently appended-to run when one exists. Runtime(..., persist_to=) and Runtime(..., store=) both register the catalog row when the store implements upsert_run. There is no load-or-create flag. except FileNotFoundError and the CLI not-found exit (code 3) still catch this.

GraphStore. Runtime.load and Runtime.fork raise NonEmptyGraphStoreError (a ReplayError) when store.is_empty() is false, before any event is applied. operation is "load" or "fork". Fork checks before fork_run, so a refusal leaves the run list and the event count unchanged. Neither call invokes clear().

Unknown GraphStore backends. GraphStore.is_empty() probes objects with query_objects(ObjectQuery(result_mode="exists")) and still reads all_relations / all_patches. That default allows replay only when those reads are empty. A backend whose real state is invisible to them must override is_empty and return False while that hidden state would survive replay. FalkorDB overrides with one MATCH (n) WHERE n:AGNode OR n:AGPatch RETURN 1 LIMIT 1, the same nodes clear() deletes, so a leftover placeholder is non-empty. Indexes are not projection state.

Version. This adds public API (RunNotFoundError, NonEmptyGraphStoreError, GraphStore.is_empty, catalog_status), so it is 1.13.0. pyproject.toml and activegraph.__version__ are bumped here. The v1.12.0 bump landed in the API PR (#79), not in the release-prep PR (#85).

Acceptance evidence

  • pytest -m "not slow" -q — 1096 passed, 14 skipped, 13 deselected. Skips are the Postgres suite (ACTIVEGRAPH_TEST_POSTGRES_URL is unset here). FalkorDB embedded conformance, including is_empty and the placeholder-only case, ran. Postgres cases for catalog identity and store= registration are in tests/test_load_integrity.py and run on the CI Postgres service.
  • mypy — success, 54 source files
  • python scripts/gate_docstrings.py — Ring 0 131/131, Ring 1 90.8% (floor 80%)
  • Contract/changelog/docs updated where public behavior moved (CONTRACT v1.13 Add README spec and v0 design contract #1–Build v0.5 resumability: persistence, replay, fork, diff #2, CHANGELOG 1.13.0, replay / FalkorDB / error pages)

Contributor credit

@TrendpilotAI reported both bugs, wrote the reproductions, and specified the #82 acceptance tests this PR follows.

Open in Web Open in Cursor 

cursoragent and others added 2 commits September 26, 2026 00:22
Runtime.load no longer registers an unknown run_id, and it refuses to
replay into a GraphStore that already holds projection state. Creating
a run stays an explicit Runtime(...) step. The package version stays
1.12.0 until the release tag.

Co-authored-by: Yohei Nakajima <yoheinakajima@users.noreply.github.com>
Guard fork before any copied rows, register a catalog row for store=,
and keep catalog probes from mutating an unrelated database.

Co-authored-by: Yohei Nakajima <yoheinakajima@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/load-integrity-fixes-d03d branch from b2c34b4 to bab57c1 Compare September 26, 2026 00:35
@cursor cursor Bot changed the title fix: Runtime.load fails closed on unknown runs and stale projections fix: load and fork fail closed on unknown runs and stale projections (1.13.0) Sep 26, 2026

This branch has not been deployed

No deployments
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.

Runtime.load(..., graph_store=...) retains stale projection state Runtime.load() silently creates a missing run instead of failing

2 participants