versioned-runtime
The versioned runtime — how the substrate evolves itself
A seed + roadmap document. Written for the next agent (human or model). It captures *why* we are versioning the action system and *how* we intend to get there, so the intent survives the implementation. Status: Phases 1–2 BUILT (2026-06); Phases 3–6 are design. The schema stamp + load-time gate and the verification primitive now ship in the engine (see the phase list below and [consolidation.md](./consolidation.md)). Phases 3–6 (real migrations, engine-version-per-schema, data versioning, the LLM authorship loop) remain a captured plan. The seeds it leans on already existed; now the foundation is real. Also BUILT (2026-06): run any version at any time. Beyond authoring/stamping, a saved version is now *runnable* by its content hash without disturbing "current" — thing.runversion evals .versions/<sha>.snap crash-proof; thing.versions shows the hashed lineage (reason + parent = "from this to that") as a native scene; the verify pillar proves a version reproduces its own output (verify-evolve.sh). The self-evolving actions ship baseline-versioned. This is the run/verify half of Phase 6, usable today; the autonomous *author* loop still needs an oracle provider.
The one idea
[VISION.md] says Zero is a substrate that becomes what the moment demands. This document is about the next move: making the substrate able to evolve itself — safely, and primarily for an LLM, not a human.
We version the action system so that an LLM can:
1. author a new action,
2. version it (stamp the schema it was written for),
3. run it (on an engine that understands that schema), and
4. verify that the outcome is what was intended.
The end state we are designing toward: the action language is allowed to drift toward whatever is optimal for an LLM to read and write — even if it stops making sense to a human at all. Humans are not the target reader of the language. The LLM and the engine are. Verification is what makes that safe: when no human can read the language anymore, the *outcome check* — not a human reviewer — is the only thing guaranteeing behavior. So verification is not a nice-to-have at the end; it is the thing that *earns the right* to let the language go weird.
The four pillars (keep them separate)
The reason this feels deep is that change moves in both directions at once — a new way of writing actions can force the engine forward, and an engine change can force a schema bump. Pulling the concern into separate, stamped axes turns that tangle into edges in a version graph.
1. Schema version — *what a valid action looks like* (the DSL grammar /
structure). The "language version." Today an action is just name + file + body with no stamp (kernel/interp.h).
2. Engine version — *how a schema is executed*. There can be several engine
implementations for one schema, with a pointer to the current/latest. The seed already exists: ZERO9_VERSION is a compile-time macro exposed as __version on every request (kernel/dispatch.cpp), and there are already two engines for one schema — the tree-walker (run → execStatement) and the optional compiled C++ views (kernel/compiled_rt.cpp, tools/transpile.py).
3. Migration — *versioned pure transforms vN → vN+1* over actions, folders,
and .data. The only genuinely new concept. A migration has the same shape as a store hydration (see below), so it is cheap.
4. Conformance / verification — *a way to check that behavior is preserved
(or intentionally changed) across a version step.* Designed as a pair with the version stamp, not bolted on. This is the pillar that makes human-illegibility safe.
The invariant that tames bidirectional change
The engine supports a *range* of schema versions [min, max], and migrations run lazily on read.
• class schema > engine max → refuse with a clear "update the engine" message.
• class schema < engine min → migrate on load, then run.
• class schema in window → just run.
No flag day. Every node always makes forward progress, because the engine never has to understand exactly one version — it understands a window, and migrations close the gap on read. (Borrowed from how browsers and Kubernetes APIs survive version churn.)
Why this is cheap in zero.9 specifically
The store is already a copy-on-hydrate cache: a class folder missing locally is pulled from remote into ~/.zero/stores on first touch (functions/core/loadchain.cpp; see [zero9-store-architecture]). A migration is the same shape as a hydration — a transform that produces a versioned artifact in the cache. We are not bolting on a migration system; we are reusing machinery that already exists.
And the per-class .remote marker is the precedent for *where a version lives*: a sibling .schema marker file per class folder (or a schema = key in defaults.data) — versioned at the class folder, not stamped on every .action file (the class folder is already the unit of distribution and resolution).
Seeds already in the codebase (what we lean on, not what's built)
Seed
Location
Why it matters
Engine version stamp
kernel/dispatch.cpp (ZERO9_VERSION → __version)
Where [min_schema, max_schema] constants attach
Action struct
kernel/interp.h
Where a parsed version field would attach
Class load / search path
functions/core/loadchain.cpp
Where the load-time version gate attaches
Two engines, one schema
kernel/compiled_rt.cpp, tools/transpile.py
The embryo of "many engine versions per schema"
Copy-on-hydrate cache
functions/core/loadchain.cpp, ~/.zero/stores
Migrations reuse this; the .remote marker is the .schema precedent
Roadmap (phased)
Phase 0 — design (done)
This document + the design captured in agent memory (zero9-llm-versioned-runtime).
Phase 1 — the stamp + the gate (MVP) — BUILT (2026-06)
• .schema version marker per class (mirrors .remote) — read in
functions/core/loadchain.cpp (read_schema).
• Engine declares the window: kSchemaMin / kSchemaMax in kernel/interp.h,
stamped to the DSL as __schema_min / __schema_max (main.cpp, dispatch.cpp).
• Load-time gate in loadchain (schema_gate): refuse-above-max ("update the
engine"), migrate-below-min (via the registry), else run; an unstamped class runs as the current grammar.
• Migration registry (try_migrate) is the designed no-op: a below-min class
with no registered transform is correctly refused.
• *Verified:* .schema = 999 refused; = 0 refused (no migration); = 1 /
unstamped run. (See [consolidation.md](./consolidation.md).)
Phase 2 — verification as a first-class pillar — BUILT (2026-06)
• functions/verify <action> <expected> [arg...] runs an action and compares its
return value to the expected outcome, returning "1"/"" — the deterministic outcome check (functions/core/verify.cpp). An action that errors is a verification FAILURE, never a crash, so the harness reports pass/fail.
• Return-value equality is the first concrete outcome shape; a declared assertion
action or an LLM judge can layer on top without changing the contract.
• *Verified:* verify greet "hi" → "1" on a matching action, "" on a mismatch.
Phase 3 — real migrations
• First non-trivial schema bump + its vN→vN+1 migration, run lazily on read,
producing a hydrated versioned artifact in the cache.
• *Done when:* an old store runs on a new engine without manual intervention.
Phase 4 — engine-version-per-schema
• The "current latest execution version" pointer: select among multiple engine
implementations for a given schema (interpreted / compiled / future).
Phase 5 — data versioning
• The identical version + migration + verification pattern applied to .data.
Phase 6 — LLM authorship loop (the point)
• Close the loop: an LLM authors → stamps → runs → verifies → migrates, iterating
on the platform itself. The language is now free to drift toward LLM-optimal.
Built alongside (2026-06) — run + verify any version, the loop's face
• Run any version at any time (thing.runversion ?sha=): execute a saved version by
its content hash via functions/eval on its snapshot, crash-proof, the live version untouched. Versions stay internal to the system, content-addressed.
• Scene-native lineage (thing.versions ?action=): every version newest-first, each
with its reason + the version it changed FROM, and a "run this version" button.
• The loop's face (thing.loop, thing.match): the per-thing self-evolution +
autonomous-loop conversation and demand↔supply matchmaking, on the chat fabric.
• *Verified:* verify-evolve.sh — a staged version runs true by hash; the live one is
untouched; evolve carries ≥2 versions; every page renders as a scene (and as HTML).
Status of the wider project (for honesty)
The versioned runtime's foundation now ships (Phases 1–2: the schema gate + the verification primitive); its higher self-evolution machinery (Phases 3–6 — real migrations, engine-version-per-schema, data versioning, the autonomous LLM authorship loop) is still a plan. The wider zero.9 project releases engine/desktop via release.yml (now including 32-bit ARM / Raspberry Pi — see [consolidation.md](./consolidation.md)) and the mobile app via mobile.yml. Do not conflate the foundation with the full loop.
Open questions (decide before/while building Phase 1–2)
• Where exactly does the schema version live — .schema marker vs. defaults.data
key? (Leaning .schema, to mirror .remote.)
• Is the schema version per class or per store? (Leaning per class.)
• What is the *first* concrete shape of a "verification outcome"? (Output equality?
A declared assertion action? An LLM judge with a deterministic fallback?)
• How does a node *fetch* a migration it doesn't have — same path as store hydration?
Where this came from
A design conversation (2026-06). The *why* — LLM-evolves-its-own-platform, verify to earn illegibility — is the load-bearing part; the mechanism is secondary and replaceable.
[VISION.md]: ../VISION.md [zero9-store-architecture]: ../ (see the engine + store design)