Saves, rewind and sharing

Axiom is event-sourced: every turn appends events (player input, narrative text, stat changes, time…) to a journal, the Event_Log. The visible game state is derived from that journal. This one design decision buys most of the engine’s tricks:

  • Rewind — drop the events after turn N and rebuild the state: you are back at turn N.

  • Fork — copy a save with its journal truncated at a point: an alternate timeline.

  • Inspect anywhere — materialise the state at any past turn or in-game minute without touching the save.

  • Edit safely — corrections are applied on top of the journal, never by mutating derived state.

Where saves live

The universe definition and the play-throughs are separate things:

~/AxiomAI/
├── universes/<name>/…                    # definition (source tree + cache)
└── saves/<universe key>/save_<uuid>.db   # one file per save (runtime state)

Each save database is self-contained: it carries a copy of the universe definition taken at creation time. Patching the universe never breaks an existing save — the definition copy is resynchronised when the save is opened, while the runtime state survives. A save is therefore a single portable file.

Pulling new Axiom code does not delete saves. They live under ~/AxiomAI/saves/, not in the git tree. The first time you open a save after an upgrade, migrate_schema runs in place: new tables/columns are added, a flat inventory bag is copied into the nested Item_Instances tree, and play-promoted lore that used to sit in Lore_Book is moved to Session_Lore before a world refresh can wipe it. A one-time save_<id>.db.pre-migrate.bak is written next to the file on that first structural migrate. Journal, stats, and turns stay. If migrate itself throws, the save still opens.

(Saves created by very old versions live inside the universe .db; they remain listed and playable as-is.)

Rewind and checkpoints

From Python, rewind is one call on the session:

session.rewind(target_turn_id)     # back to that turn
session.list_checkpoints()         # every recorded turn you can rewind to

A rewind is comprehensive. axiom.checkpoint.CheckpointManager drops the Event_Log after turn N and rebuilds every derived view to its turn-N state in a single transaction:

  • the materialised stats (State_Cache), the Timeline, periodic Snapshots, and — in living mode — the structured facts and beliefs;

  • temporary modifiers (buffs/debuffs): these decay in in-game minutes and are not event-sourced, so the engine snapshots the modifier table each turn and restores the one captured at turn N;

  • the nested inventory (Item_Instances): same idea, one snapshot of the whole tree per turn (Inventory_Snapshots), so an item gained, lost or moved after turn N goes back to where it was — containers keep their contents. Turns played before inventory snapshots existed have none: rewinding to such a turn leaves the inventory as it is. Viewing or forking a save at a past turn uses the same snapshots;

  • scheduled events: any event that fired after turn N is un-fired, so it triggers again once the in-game clock next crosses its minute.

The semantic memory store is rolled back alongside via axiom.memory.VectorMemory.rollback(), which removes every narrative chunk from a later turn. Nothing from a later turn leaks back: buffs, items, world events and remembered facts all return to exactly how they stood at turn N.

In Hardcore mode, death deletes the save — that is the point of Hardcore.

Inspecting and editing a save

The axiom save-* commands (see the CLI guide) expose the editing toolbox. Points in time are selected by turn (--turn N) or by in-game minutes (--minute M).

$ axiom save-show MyWorld.db <save_id> --turn 12     # materialised state
$ axiom save-export MyWorld.db <save_id> state.toml  # state -> editable TOML
$ axiom save-import MyWorld.db state.toml            # TOML -> new save
$ axiom save-fork MyWorld.db <save_id> --turn 10     # alternate timeline
$ axiom save-edit MyWorld.db <save_id> patch.toml    # in-place correction

The editable save_state.toml format:

[save]
player_name = "Alice"
difficulty = "Normal"
player_persona = "A reformed clockwork thief."

[point]                  # informative at export time
turn_id = 12
in_game_minutes = 540

[state.innkeeper]        # effective entity stats
Health = "100"
Mood = "friendly"

[[inventory]]            # nested: holder is entity, location, or container instance
item_id = "rusty_sword"
quantity = 1
holder_kind = "entity"
holder_id = "player_alice"
entity_id = "player_alice"   # legacy alias for holder_kind=entity

[[inventory]]
item_id = "purse"
quantity = 1
holder_kind = "entity"
holder_id = "player_alice"
is_container = true

[[session_lore]]         # this playthrough only — not the world lore book
entry_id = "harbour-rumour"
category = "rumour"
name = "The sealed contract"
keywords = "bunker, contract, highport"
content = "Brask is recruiting and will not name her charter."

[[modifiers]]
entity_id = "player_alice"
stat_key = "Strength"
delta = 2
minutes_remaining = 120

Old flat [[inventory]] rows (entity_id / item_id / quantity only) still import as items carried on that entity.

On the web Hub, Edit opens a structured editor (entities, nested inventory, session lore, temporary modifiers) with the TOML above as the Advanced tab. Qt still uses the TOML dialog. Both apply through the same apply_correction path.

Stats the editor shows are the live play values (Event_Log replayed on top of authored Entity_Stats), not the universe template. Stat keys are matched case-insensitively: a definition id arousal and an authored key Arousal are the same field.

Temporary stats are marked on the definition (Creator checkbox, Infer, or play-start classification). The engine ticks them from that profile: heal toward resting over days, hold a buildup at max for a few minutes, crash on a listed event. Extra short overlays still live in [[modifiers]] / the Temporary tab and count down in in-game minutes. The narrator emits stat_events (and optional modifiers); it does not invent a decay schedule.

Two rules keep editing safe:

  1. The journal stays the source of truth. Imports create a new save whose journal starts with “genesis” events at turn 0; exports materialise the state by replaying the journal.

  2. An imported save starts with an empty vector memory — it fills up again as you play.

Session lore (Session_Lore) is runtime data on the save. World refresh and a new game do not see it. Promoting a fact into the universe still goes through Canonize with world scope.

Sharing a save

Pack a save (journal, state, illustrations metadata) into a single .axiomsave archive and import it next to any copy of the same universe:

$ axiom save-pack MyWorld.db <save_id> alice.axiomsave
$ axiom save-unpack MyWorld.db alice.axiomsave

From Python

The same operations are exposed by axiom.savestore (separate save files, archives) and axiom.saves (materialise / fork / import / edit), with axiom.checkpoint.CheckpointManager underneath.