skip to the page

The history of a page

The last two pages ended at the same place: the Gate said yes, and the change was applied. This page is about the sentence after that one.

Applying a change and recording it are two different steps, and Loom does the second one on purpose. A page is not just its current state — it is the list of changes that produced it, in order, each with the plan behind it and the name of whoever asked. That list is called the log, and it is the thing that makes the rest of the framework worth having.

Press a chip below, then look underneath the verdict. There is a row there now.

The smallest tree that renderslive · rendered through the runtime

Hello from a tree

Nothing here was written as markup.

Propose a change:

Every one of these goes through the same pipeline a model’s answer would: interpreted, analysed, weighed, judged, applied, appended. The log is real and it is in this tab — reload the page and the example is back as it was.

show the tree
{
  "kind": "element",
  "id": "n_firsttree5",
  "type": "loom.page",
  "props": {
    "fills": true,
    "width": "readable",
    "loom:theme": {
      "palette": "minimal",
      "fontPack": "minimal-sans",
      "stylePreset": "precise"
    }
  },
  "children": [
    {
      "kind": "element",
      "id": "n_firsttree2",
      "type": "loom.heading",
      "props": {
        "level": 1
      },
      "children": [
        {
          "kind": "text",
          "id": "n_firsttree1",
          "value": "Hello from a tree"
        }
      ]
    },
    {
      "kind": "element",
      "id": "n_firsttree4",
      "type": "loom.prose",
      "props": {},
      "children": [
        {
          "kind": "text",
          "id": "n_firsttree3",
          "value": "Nothing here was written as markup."
        }
      ]
    }
  ]
}
A page with a heading and a sentence. Every node names a registered primitive and carries props that primitive declared.

The store keeps two things

A store is where a page lives. It holds two things and the relationship between them is the whole idea:

  • The log. Every change that was ever applied, in order. Append-only: nothing in it is edited, and nothing is taken out.
  • The snapshot. The page as it is right now.

The log is the truth. The snapshot is a convenience — it exists so that showing somebody a page does not mean replaying its entire history first, which would get slower every time anybody changed anything. If the two ever disagree, the log is the one that is right, and there is a function whose whole job is to check (auditSnapshot, which replays the log and compares).

What a log entry says, and what it deliberately does not

Every row in the box above is one entry. Here is everything one carries:

FieldWhat it is
revisionthe number this entry produced
deltathe operations that were applied
provenancewho asked, what planned it, and how confident that planner was
appliedAtwhen the runtime applied it
answeredBywho allowed it, when a person had to

Notice what is not there. There is no verdict, because a change the Gate refused never reached the log — a refusal changes nothing, so there is nothing to record about the page. And there is no sentence a person typed: the log records the change, not the conversation that produced it.

Who asked and who allowed are two different fields

provenance.actor is the person who asked. answeredBy is the person who said yes when the Gate held the change for a human.

They are separate because the whole value of holding a change is putting a second person in the way of it. A log with only the first field would make every confirmed change look like somebody waving through their own request.

Try it on the example: press demote the page's heading, which the Gate holds, then answer it. The row that appears says both.

Undo is a proposal, not a rewind

Here is where most systems take a shortcut, and Loom does not.

The obvious way to build undo is to reverse the last entry — pop it off, or apply its inverse straight to the page. It is quick, and it makes undo the one operation that changes a page without being judged, and the log the one record with a hole in it.

So an undo in Loom takes the long way round, and the long way is the same way everything else takes:

  1. The runtime reads the log and computes the operations that would put the page back.
  2. Those operations become an ordinary proposal, from an interpreter called loom/revert.
  3. The Gate judges it.
  4. If it is allowed, it is applied and appended as a new revision.

Press Undo on a row above and watch the revision number go up. The page looks like it did before; the history is longer than it was. Which means an undo is attributable to whoever asked for it, refusable by the Gate, holdable for confirmation — and itself undoable. Try pressing Undo on the undo.

An undo can cost something, and it says so first

Undoing the most recent change is usually clean. Undoing an older one may not be, because somebody may have built on it since.

Suppose revision 1 adds a paragraph and revision 2 moves that paragraph to the top. Undoing revision 1 removes the paragraph — which throws away what revision 2 did to it. That is not a bug and it is not a reason to refuse: it is a cost, and somebody may well want to pay it.

So the plan says so. Every undo carries the list of later revisions it would write over, the box prints that list beside the button before you press it, and the Gate reads the same declaration and holds the change for a person instead of deciding on your behalf.

You can produce this on the example: add a sentence, then move the last block to the top, then look at the row for revision 1.

A stale ask is refused before it costs anything

One more thing the store makes possible. Every ask names the revision it was written against — the page the person was actually looking at.

Before anything is interpreted, the write path compares that number to the page as it stands. If they differ, the ask is refused there, without running an interpreter at all. On a real deployment that is a model call not spent on a plan that could only have been thrown away.

This is the same rule from the other side: a proposal written for revision 3 does not quietly apply to revision 4.

Where this actually runs

Everything on this page is real and none of it is a database. Each example on this site opens an in-memory store in your browser tab, and it is gone when you reload — the rows you are looking at were written by the runtime's own write path and will not outlive the page.

A deployment swaps the store and changes nothing else:

import { memoryTreeStore } from "@jam-overture/loom/store"
import { postgresTreeStore } from "@jam-overture/loom/postgres"

const onThisSite = { store: memoryTreeStore(), holds, runtime }

const inADeployment = { store: postgresTreeStore(db), holds, runtime }

TreeStore is an interface with five methods: create, head, revisions, list and append. The one this site uses keeps entries in an array; the one a deployment uses keeps them in Postgres. Both are held to the same contract by the same test suite, so "it works in the browser" and "it works in production" are the same claim about the same code.

holds is the second, smaller store beside it — the custody a change sits in while it waits for a person to answer. It is a separate interface for a reason worth knowing: a held change has not been applied, so it cannot live in the log without breaking the rule that a revision counts entries. It ships in the same two shapes as the tree store — memory, which is what this site uses, and Postgres — and the same contract suite is run against both.

Which one you want is decided by a question about your host rather than about your scale: can the process that judged a change still be spoken to when the answer arrives? On a long-lived server, usually. On a serverless one, usually not — so a change held in one instance's memory would be confirmed against another instance that has never heard of it.