Open World Format

The session log (L1)

ops.jsonl is the one history: one JSON entry per line, each entry a committed batch of ops by one author. It holds what changed the world (edits) and what happened around the change (tool calls, visitor input, state deltas, clocks). The world at any tip is the fold of this log over the base — and manifest.json is that fold at the tip of main, written down (the package, “Head-first”).

The entry

{"revision": 42, "timestamp_ms": 1790000000123,
 "author": {"peer": 3, "name": "maya", "source": "hand"},
 "message": "a lantern by the gate",
 "ops": [ … ]}

revision is the document revision after the entry’s edits applied. Only edits bump it; history-only entries carry the current revision, so readers MUST tolerate repeated and non-monotonic-looking revisions between edit entries. Edit entries themselves are totally ordered.

author is who or what did it — a person, a model, a visitor, a host. Authorship is what makes the log an audit trail: “what did the model do here, versus the people?” is a filter on one field. The filter has two levels. author.name says who. author.source (optional) says how the batch came to be, in one word: hand (a person acting directly), library (ready-made content placed from a catalogue or template) or ai (a model’s output, whether or not a person accepted it). A reader MUST treat any other value as no label, so the vocabulary can grow without a bump; the field is part of the entry’s identity like every other, and a reader that re-encodes an entry keeps it. The label is a fact about the actor, so it lives on the author, not in ext-provenance, which is a world’s generation lineage (model, prompt, duration) and belongs to meta: a label needs no model name, and a prompt never belongs in a history (the package, “Exports from other systems”).

timestamp_ms is milliseconds since the Unix epoch, and the format requires no precision of it. A writer MAY coarsen a timestamp — to the day, for a public history, where a per-entry clock would tell when a person was where — and a reader MUST NOT assume a timestamp is finer than the writer chose. Replay never keys on entry timestamps: the clock of a replay is the clock entries (“Replay and determinism”).

message (optional) is what the author says the batch is for — a commit message. It folds to nothing, is part of the entry’s identity, and is the subject of the git commit the batch becomes in a git-backed package.

The op kinds

An entry’s ops hold any mix of these kinds. Each kind is serialized as its own fields (there is no wrapping tag object); an op is recognized by its shape:

{"SpawnEntity": {"entity": {"id": 1, "name": "lighthouse", "transform": …}}}
{"tool": "gen_spawn_primitive", "args": {…},
 "result_hash": "sha256:…", "phase": "blockout", "timestamp_ms": 1790000000131}
{"input": {"actor": "visitor-7", "position": [3.1, 1.8, -2.0],
           "look": [12.0, -4.0], "click": 17}}
{"state": {"score.chest": 10}}
{"clock": {"playing": true, "position_s": 41.5}}
KindWho writes itChanges the document?
edit (an EditOp)the session authority, for anyoneyes — the only kind that does
toolan app, when a tool call runsno — intent, recorded next to the edits it caused
inputan app, sampled at a normative rate of exactly 10 Hz while recordingno — playthrough replay
statean app, when host state changesno — the game state no document holds
clockan app, on transport eventsno — performances, song worlds, tours
mergean app, merging a forkno — provenance that a batch came from a branch: {"merge": {"branch": "moat-variant"}}

Edits are SpawnEntity, DeleteEntity, ModifyEntity (an entity id plus a field patch, where an absent key means unchanged, null means clear/remove, and a value sets it), SetEnvironment, SetCamera, SetAmbience, SpawnAudioEmitter, RemoveAudioEmitter, ModifyWorld, and Batch (all-or-nothing). Deleting an entity deletes its descendants.

ModifyWorld patches the document’s scene-wide fields with the same rule as ModifyEntity — absent unchanged, null clears, a value sets:

{"ModifyWorld": {"patch": {"meta": {"name": "harbor", "description": "…"},
                           "tours": [ … ], "soundtrack": null}}}

Its patch reaches meta, environment, camera, avatar, tours, soundtrack, ambience and creations. meta is replaced whole and can’t be cleared (a world always has a name); clearing a list field leaves it empty. Keys it doesn’t name are ignored (must-ignore).

Every edit has a computable inverse; undo is appending the inverse — the log never rewinds. The inverse is computed at the time of the edit, against the document it is about to change:

The fold is total

The fold’s state is a whole manifest: every field a manifest holds is carried through the fold, and every one is reachable by an edit. For any world m, folding an empty log gives m back — equal as worlds, up to names bound to ids at ingestion, entity order, an absent field the same as null, and numbers compared by value. The conformance suite holds every reference to this for every conformance world and every example package’s base and head.

next_entity_id only grows. Ids are monotonic and never reused, so the fold carries the larger of the base’s declared value and one past every id it has seen — deleting the newest entity does not free its id — and a manifest’s effective next_entity_id is the larger of its declared value and one past its largest id.

Authoring

An authority — an open app, or a command-line tool when no app is open — is the one writer of a package. Authors (agents, scripts, people) change the world only by sending it a batch of edit ops: a JSON array, or an envelope:

{"ops": [ … ], "author": "maya", "message": "a lantern by the gate",
 "expected_revision": 42}

author and message become the entry’s. expected_revision (optional) is the revision of the document the author read before writing the batch — the head’s, or the entry’s the batch builds on. An authority that finds the document at another revision MUST refuse the batch whole, naming both revisions (the world is at revision 44, not 42), so an author whose world moved under it re-reads and decides again instead of landing a stale edit; without the field, the batch binds against the world as it is now, which is the right default for an author addressing entities by name. The authority then takes each op in order, against a trial document that already holds the batch’s earlier ops:

  1. Bind. Where an entity id goes (ModifyEntity.id, DeleteEntity.id, a parent), a string is a name: it resolves to the id it names now — the identity rule of the world document, applied to op addresses. A SpawnEntity without an id gets the next one. Committed ops always carry ids: the top-level references bind here, and the deeper marked ones — a behavior’s target, an extension’s aim_entity — bind once the op has applied, against the world it made, into the op as committed as well as into the document. A log never holds a name.
  2. Merge. A patch’s object-valued struct fields — ModifyEntity’s transform, material and light; SetEnvironment’s env; ModifyWorld’s meta, environment, camera, avatar and soundtrack — merge into the current value as a JSON Merge Patch (RFC 7396): present keys replace, null removes, absent keys keep. Send {"transform": {"position": [0, 2, 0]}} and the rotation and scale stay. The committed op carries the merged whole value, so the fold itself never merges.
  3. Read strictly. No key the format would drop: an unknown field is a refusal with a JSON pointer to it, not a silent ignore (profiles, “Strict Mode”).
  4. Store assets. A referenced asset must be a file inside assets/; it is stored or verified as the package, “Assets” says.
  5. Apply to the trial — an op that no longer applies fails here.

Then the world the batch makes must validate. Validation is structural: the document holds against the schema and every reference the fold binds resolves — a parent, a name at intake — the same checks the fold’s apply step runs. A reference an entity still holds to an entity deleted later (a look_at target) is not one of these: the fold keeps reading the world, the behavior has nothing to look at until it is retargeted or dropped, and an authority reports it as a warning on the batch that caused it. Budget limits — an entity’s extent, a chunk’s entity or triangle count, an entity’s behavior or modulation count — are an authority’s policy, not the format’s: the authority reports them to the author as warnings, and they never refuse a batch. Any structural failure refuses the whole batch: nothing is written, and the reply lists a reason for every op that failed. Success appends one entry — the batch’s edits, its author and its message — and writes the new head.

The reply is the author’s receipt, whatever carries it (an HTTP body, a function’s return value). A commit’s receipt holds at least the entry’s revision and id, spawned — the names the batch spawned and the ids they got, which is how an author or a panel finds what it just made — and warnings; a refusal’s holds errors, one reason per failing op (op 2: no entity is named "lanterm"), and promises that nothing changed. An authority MAY add what its readers need (whether the entry started a branch, what the canvas shows, where stored assets went); the reference authority’s receipts are examples of both forms.

A batch builds on main’s tip unless the author names another entry to build on (the reference authority’s at); from an entry that already has a child that starts a branch. An entry always builds on an entry: once the log holds one, the base can no longer be built on — the format has no second root — so an authority MUST refuse a batch that asks for it rather than land it on main. To branch from the very beginning, build on the first entry and undo its ops in the same batch.

Undo is a batch like any other: the inverse of the newest batch nobody has undone, appended. Two authorities never write one package at once; one that finds another holding the package (.live/endpoint.json with a process that answers) sends its batches there.

Compatibility

To prevent future collisions, the shape collision rule applies: Edits MUST be PascalCase, History kinds MUST be lowercase.

New op kinds MUST fold to nothing for readers that don’t know them, and readers MUST keep reading logs written before a kind existed. The reference rule: op kinds are recognized by shape, edits first — so a log containing only edits (every log written before this format had history kinds) parses unchanged, and an edit serializes today exactly as it always did.

Folding and Invalid Operations

During a fold, an operation may “no longer apply” to the current state. The fold stops at the first such entry and names the reason — the same refusal class an authority returns at intake, so a log that replays cleanly folds cleanly and one that cannot never folds halfway in silence. An entry is atomic: if any of its edits no longer applies, none of them commits. The error taxonomy includes:

Entry identity, forks and branches

An entry may carry an id (its identity) and a parent (the entry it builds on). To ensure identical IDs across forks, implementations computing content hashes MUST serialize the entry using a canonical JSON format (e.g., no whitespace, keys sorted alphabetically) — every field but id, message included. A branch is an entry whose parent already has a child; a tip is an entry that is nobody’s parent. Folding generalizes: state at any tip is a fold of the path — walk parent links from tip to base, fold that chain. A log with no ids is a chain in file order (a reader synthesizes line-<n> ids, counting lines from 0), so branching is purely additive and old readers see a linear prefix. revision stays the room’s total order — every entry across every branch, in the order written, so a branch’s tip may carry a higher revision than main’s; an authority compares expected_revision with the revision of the entry the batch builds on, never with the highest in the log. parent records causality — sequence is not causality.

Forks and refs live in package.json (the package); the merge op records where a merged batch came from. When merging a branch, the merge authority must handle ID collisions. See rfcs/branching-histories.md. In a git-backed package, branches are git branches and the same merge rules resolve a merge’s conflicts (the package, “Git”); an authority may also keep every entry on one git line, branch entries included, with the tree in ops.jsonl’s parents — the reference authority does — since the log alone carries causality.

The merge rules, exactly

A merge appends the branch’s entries to main, rewritten so they apply cleanly on main’s head. The rewriting is deterministic — the same main head and the same branch entries always produce the same merged entries:

  1. Fresh ids. An id the branch spawned that the main line currently holds is colliding. Colliding ids are reallocated in ascending order. Fresh ids start at main’s effective next_entity_id — the fold already carries it: the larger of the declared value and one past every id main has ever held, deleted ones included — and count up, skipping every id the branch spawns. An id main deleted stays spent: fresh ids come from that floor, never from the live set.

  2. Every entity reference rewritten. Within the merged batch, every field that holds an entity id is rewritten through the remap:

    OpFields that hold entity ids
    SpawnEntityentity.id, entity.parent, the numeric Orbit.center / LookAt.target in entity.behaviors[], and every extension field the list marks (ext-cinematography’s camera.aim_entity / camera.focus_entity)
    ModifyEntityid, patch.parent, the numeric behavior refs in patch.behaviors[], the marked extension fields in the patch
    DeleteEntityid
    Batchevery inner op, recursively
    ModifyWorldpatch.avatar.model_entity (numeric), patch.creations[].entities[]

    These are the schema’s marked entity-reference fields (x-entity-ref, the world document “Identity”); schema/entity-refs.json is the generated list every reference reads.

  3. Names. Two branches that each mint a lighthouse merge into one world with two of them, and the authority MUST rename the merged side. When a branch spawn’s name is already taken — by main, or by an earlier spawn in the same merge — the merge renames the spawn to <name>-<n>, n counting from 2 up, first unused. Only the SpawnEntity changes: references are ids by then (the world document), so a rename breaks nothing in the log.

The merged entries keep their id, parent, author and message — an entry’s identity survives the merge.

Snapshots

A snapshot is the folded document written to snapshots/entry-<id>.json (or snapshots/rev-<N>.json for linear logs without explicit ids): derived, never authoritative, deletable without loss — the fold from the base reaches the same state. manifest.json is the newest keyframe of all, always present.

Compaction moves the base up: the producer writes the head (or the state at a chosen revision) as the new snapshots/base.json, sets package.json’s base_revision to its revision, moves the entries it now holds to ops.archive.jsonl (or deletes them), and keeps the rest in ops.jsonl. manifest.json is untouched. Compaction changes nothing observable about the current state, but truncates structural replay.

Replay and determinism

Three levels, and only the first two are part of this format:

  1. Structural replay — fold to any revision. Exact by construction: the fold is the document’s history. Anything that folds to the same state — on any machine, in any language — is conformant here. A git-backed package also replays as keyframes: each commit’s manifest.json, oldest first, with a renderer stepping from one to the next by the edits that turn one into the other.
  2. Semantic replay — re-run triggers and behaviors offline over the log’s input and state entries. The contract: the same fold plus the same inputs produce the same trigger outcomes and the same state trajectory. Frames are approximately the same — input is sampled (10 Hz), behaviors are functions of folded time, and renderers draw. During semantic replay, timers observe the session’s recorded transport clock. After a seek, a timer observes the clock as dictated by the closest preceding clock entry.
  3. Bit-exact replay — the same pixels. Not part of this format, and MUST NOT be promised by implementations of it. Floats, physics ordering and renderer differences make it a lie waiting to be caught; package.json’s seed is reserved for engines that want to try anyway, and means nothing to the contract above. The exception is opt-in: a package declaring ext-strict-determinism asks for exactly this, and an engine claiming that extension makes the promise the core forbids.