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}}| Kind | Who writes it | Changes the document? |
|---|---|---|
edit (an EditOp) | the session authority, for anyone | yes — the only kind that does |
tool | an app, when a tool call runs | no — intent, recorded next to the edits it caused |
input | an app, sampled at a normative rate of exactly 10 Hz while recording | no — playthrough replay |
state | an app, when host state changes | no — the game state no document holds |
clock | an app, on transport events | no — performances, song worlds, tours |
merge | an app, merging a fork | no — 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:
SpawnEntityinverses toDeleteEntity.DeleteEntityinverses to aBatchofSpawnEntityops containing a deep copy of the deleted tree, parents first.ModifyEntityinverses to aModifyEntityrestoring the old values (clearing fields the entity didn’t have).ModifyWorldinverses to aModifyWorldrestoring the old values of the fields it touches.SetEnvironmentandSetCamerainverse to the setting they replace — or, when the document had none, to aModifyWorldclearing it.SetAmbienceinverses to the ambience it replaces.SpawnAudioEmitterinverses toRemoveAudioEmitter;RemoveAudioEmitterinverses to aSpawnAudioEmittercarrying the emitter it removed.- A
Batchinverses to its ops’ inverses in reverse order, each computed against the state its op was 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:
- Bind. Where an entity id goes (
ModifyEntity.id,DeleteEntity.id, aparent), a string is a name: it resolves to the id it names now — the identity rule of the world document, applied to op addresses. ASpawnEntitywithout anidgets 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’saim_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. - Merge. A patch’s object-valued struct fields —
ModifyEntity’stransform,materialandlight;SetEnvironment’senv;ModifyWorld’smeta,environment,camera,avatarandsoundtrack— merge into the current value as a JSON Merge Patch (RFC 7396): present keys replace,nullremoves, 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. - 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”).
- Store assets. A referenced asset must be a file inside
assets/; it is stored or verified as the package, “Assets” says. - 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:
- Entity not found: Attempting to modify or delete an entity that does not exist.
- Invalid patch: A
ModifyEntitypatch that does not match the schema or attempts an invalid field transition. - Already exists: Attempting to spawn an entity with an ID that is currently in use.
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:
-
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. -
Every entity reference rewritten. Within the merged batch, every field that holds an entity id is rewritten through the remap:
Op Fields that hold entity ids SpawnEntityentity.id,entity.parent, the numericOrbit.center/LookAt.targetinentity.behaviors[], and every extension field the list marks (ext-cinematography’scamera.aim_entity/camera.focus_entity)ModifyEntityid,patch.parent, the numeric behavior refs inpatch.behaviors[], the marked extension fields in the patchDeleteEntityidBatchevery 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.jsonis the generated list every reference reads. -
Names. Two branches that each mint a
lighthousemerge 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>,ncounting from 2 up, first unused. Only theSpawnEntitychanges: 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:
- 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. - Semantic replay — re-run triggers and behaviors offline over the
log’s
inputandstateentries. 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 precedingclockentry. - 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’sseedis reserved for engines that want to try anyway, and means nothing to the contract above. The exception is opt-in: a package declaringext-strict-determinismasks for exactly this, and an engine claiming that extension makes the promise the core forbids.