Skip to content

Runtime.load() silently creates a missing run instead of failing #81

Description

@TrendpilotAI

Plain-language summary

Asking ActiveGraph to open a run that does not exist quietly creates an empty run with that name instead of saying "not found." A typo can therefore manufacture a record that looks legitimate, hide the original mistake, and later be inspected or operated on as if it had always existed.

Reproduction

import tempfile
from pathlib import Path

from activegraph import Runtime, SQLiteEventStore

with tempfile.TemporaryDirectory() as directory:
    database = str(Path(directory) / "runs.sqlite")
    missing_run_id = "run_missing"

    before = [r.run_id for r in SQLiteEventStore.list_runs(database)]
    loaded = Runtime.load(database, run_id=missing_run_id)
    after = [r.run_id for r in SQLiteEventStore.list_runs(database)]

    print("before:", before)
    print("loaded:", loaded.run_id)
    print("after:", after)

Observed output:

before: []
loaded: run_missing
after: ['run_missing']

The same result reproduces at the released v1.10.0 commit 148e12c2969f18fa12a1a3c2e75f3affd9aa0616 and PR #78's current head 221d9ecd8dcba510c4707c219b463a9a1404efc2 (activegraph 1.11.0).

Expected behavior

Runtime.load(..., run_id=<unknown>) should fail with the run catalog unchanged and no event accepted. A load/resume operation should not create the thing it was asked to retrieve.

Actual behavior

Runtime.load() replays an empty event list, returns a usable empty runtime, and inserts the requested ID into the runs table through its unconditional store.upsert_run(...) call.

The CLI already works around this in promote: it checks the run catalog before calling Runtime.load() because a mistyped ID would otherwise create a phantom run.

Concise technical explanation

When an explicit run_id is supplied, Runtime.load() skips the most_recent_run_id existence check, opens a store scoped to that arbitrary ID, reads zero events, and then calls upsert_run(created_at=...). Store construction plus an empty event iterator does not distinguish an existing empty run from a missing run.

Proposed solution

Make load/resume strictly non-creating:

  1. Check canonical run metadata before replay when a run_id is explicit.
  2. Raise a typed not-found error without mutating the store when neither metadata nor events exist.
  3. Treat events without canonical run metadata as corruption or route legacy repair through an explicit migration path.
  4. Remove the unconditional metadata repair from the normal load path.

The exact error type and any legacy migration behavior are design choices; the required invariant is that a failed lookup cannot create a run.

Repeatable acceptance tests

  1. On SQLite and PostgreSQL, loading an unknown explicit run raises, leaves list_runs() unchanged, and accepts no event.
  2. An explicitly created, valid empty run still loads successfully.
  3. Loading from a store with no runs and no explicit ID fails without creating metadata.
  4. A store containing events but no canonical run row fails closed or is handled only by an explicit migration test.
  5. CLI and library entry points produce the same not-found semantics.

These tests should run against the common store/runtime lifecycle contract rather than only one CLI call site.

Invariant at risk

  • Owning layer: runtime lifecycle and run identity.
  • Invariant: load/resume observes an existing canonical run; it does not mint identity or silently repair authority metadata.
  • What would disprove the fix: any load path that changes list_runs() after an unknown-run lookup, or different behavior across SQLite and PostgreSQL.
  • Out of scope: backend discovery, SurrealDB support, graph projection rebuilding, and broader run-catalog API design.

Failure-path evidence

Before the call, the run catalog is empty. No exception is raised. After the call, run_missing exists in canonical run metadata even though no creation API was invoked and no event was accepted.

Framework version

activegraph 1.10.0 at tag v1.10.0 / 148e12c2969f18fa12a1a3c2e75f3affd9aa0616
activegraph 1.11.0 at PR #78 head 221d9ecd8dcba510c4707c219b463a9a1404efc2

Python version

Python 3.12.13

OS

macOS 27.0 arm64

Anything else worth knowing

The reproduction uses only the standard-library SQLite backend and no custom store, pack, provider, network call, or private payload.

Activity

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

    bugSomething isn't workingpriority: integrityThreatens audit, replay, persistence, or state-transition integritystatus: ready-for-prShape agreed; implementation contribution welcome

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions