Maintenance check: did a person keep a canvas current when nothing forced them to?

A reading of the canvas store itself, answering the question product-spec.md:105 makes step 1’s whole point and product-spec.md:124 states the risk of — “A stale canvas injected into every prompt is worse than no canvas, because it misleads with authority” — and then reporting, for each of the three judgement calls docs/canvas-in-prompts.md wrote down in the ledger-orchestrator repository, whether the condition it named as reversing it has fired.

It is a third document beside ./VERDICT.md and ./off-corpus-check.md and amends neither. Those two grade --why reasons. This one does not grade a single reason: it counts writes, reads timestamps, and asks who made them.

Written for Basecamp todo 10334523272, Read what a person and the steps did with a canvas they could edit, and write the tasks that follow, on 2026-09-24.


0. The store this reads, named exactly

  • Path: /Users/lfigea/.openclaw/workspace/state/canvas — one git repository holding every row’s file, which is what $OPENCLAW_WORKSPACE/state/canvas resolves to on the machine that runs the ledger.
  • Head at the time of reading: 8f773f2ce113a2aec0315b49df9f860562971d53, 99 commits.
  • First commit: 4540a32e2e385a1bcc187a7db570018f76643882, the repository’s own initial commit, 2026-09-24 12:51:03 +0200. Last: 2026-09-24 18:08:08 +0200.
  • Window: five hours and seventeen minutes. That is the whole of it, and §5.1 explains why it is not the ledger’s window.

Eleven files: eight canvases belonging to ledger rows, two sample canvases (skill-validation-20260924, skill-validation-b-20260924) made while validating the Canvas skill, and one artefact this review created and did not remove (§5.4).

This review is in its own corpus and says where. Of the 99 commits, 94 predate it. Three are its own ledger row’s birth commits (bc-10334523272-what-a-person-and-the-steps-did, author task-ledger | open) and two are the probe in §5.4 (author lfigea | by-hand). The head pinned above is that probe’s second commit. Nothing in §§1–4 is counted from any of the five.

The commands

STORE=/Users/lfigea/.openclaw/workspace/state/canvas
git -C "$STORE" rev-parse HEAD
git -C "$STORE" rev-list --count HEAD
git -C "$STORE" log --format='%(trailers:key=Canvas-Author,valueonly)' | grep -v '^$' \
  | sort | uniq -c | sort -rn
for f in "$STORE"/*.xml; do
  printf '%s %s\n' "$(git -C "$STORE" log --format=%H -- "$(basename "$f")" | wc -l)" "$(basename "$f")"
done

How long a canvas was open to a tab edit

On the day this was measured a canvas could take a write from the page only between its birth and its freeze, and only while a server was up. Per canvas, intersected with the 14:46:37–18:08:08 window in which pid 33867 has been running — born / frozen / hours open:

canvas born frozen open
duplicate-invoice-criterion-output-schema 14:45:08 never 3.36 h
bc-10336603973-… 15:34:27 17:45:12 2.18 h
bc-10336256184 17:02:52 never 1.09 h
bc-10327745654-… 14:04:01 15:16:47 0.50 h
bc-10336754422-canvas-operating-skill 14:57:24 15:13:41 0.27 h
bc-10334522382-… 13:41:58 14:52:13 0.09 h
bc-10334520493-verify-the-drift-check-delivery 12:51:03 13:34:12 0.00 h
    total 7.49 h

Freeze times are git log --grep='^Canvas-Freeze: <ledger-id>'; the server’s start is ps -o lstart= -p 33867.

Later note, 2026-09-28. The freeze stopped refusing writes on that date — ledger row bc-10348813099-remove-terminal-freeze, Ending a canvas — so the first sentence above is a fact about 2026-09-24 and not a standing rule. The numbers are not affected: they are what was measured on the day, under the rule as it then stood, and they still re-derive from the commands beside them. A reading taken now would need the window’s upper bound stated differently, because a canvas whose freeze is recorded goes on accepting writes after it.

The census

Canvas-Author commits what it is
leo \| via claude 36 a person, through the CLI — §1
task-ledger \| open 21 create’s three birth commits, ×7 rows
claude-opus-5 \| canvas-follow-through 15 an agent session, not a step and not a person
step ids (13 distinct trailers, 4 rows) 21 orchestrator steps writing from their prompts
task-ledger \| close 4 the automatic freeze at a terminal row
lfigea \| by-hand 2 this review’s probe — §5.4

Per canvas: bc-10327745654-… 18, duplicate-invoice-criterion-output-schema 36, bc-10336754422-canvas-operating-skill 7, bc-10334520493-verify-the-drift-check-delivery 6, bc-10336603973-… 6, bc-10334522382-… 4, bc-10336256184 3, and this review’s own row 3; plus skill-validation-b-20260924 6, skill-validation-20260924 3 and probe-empty-problem-20260924 2.


1. The ruling on the question the design was built to answer

A person wrote a canvas once, in three minutes and fifty seconds, and never went back to it — while the row it belonged to ran for another two hours and thirty-nine minutes, was blocked twice, resumed twice and closed. And nobody has edited a canvas from the tab at all: the page that makes a person able to write to one without a terminal has zero writes in this store.

Both halves of that need their evidence, and the second half is the one that decides how much the first is worth.

1.1 Nobody has used the tab

bin/watch-runs-web authors an edit made from the page as $USER when the form does not say otherwise — DEFAULT_AUTHOR = os.environ.get("USER") or "", overridable per server with --author and per edit in the form. The server running on this machine is pid 33867, started 2026-09-24 14:46:37, bin/watch-runs-web with no --author. So a tab edit on this machine carries the trailer lfigea.

No commit in this store carries it. The two lfigea | by-hand commits in the census are a different string: that is store.default_author, what the tool fills in for a CLI caller that passed no --author at all, and both are this review’s probe (§5.4).

1.2 The one canvas a person wrote

All 36 leo | via claude commits are on one file, duplicate-invoice-criterion-output-schema.xml, and they span 14:45:08 to 14:48:58 — three minutes fifty seconds:

time commits
14:45:08 3
14:46:00–14:46:02 17
14:48:19–14:48:20 11
14:48:45 3
14:48:58 2

Two things in that table rule out the tab as the surface these came through, and neither is an inference about intent:

  • Twenty of the 36 land before the server was running — 14:45:08 and 14:46:00–14:46:02 are all earlier than the 14:46:37 start of pid 33867.
  • Seventeen land inside three seconds. The page’s editor writes one node per submission, which is the whole shape of /api/canvas-edit; seventeen form submissions in three seconds is not a person at a page.

The reasons agree: seventeen of the 36 commit subjects are the identical string “describe the problem and solution space for this row” and eleven more are the identical string “the canvas is a drawing surface, not a document; replace the prose with a diagram and a table”. That is one writer issuing batches, not a person composing a reason per node — and it is, incidentally, the failure VERDICT.md §5.1 exists to stop, arriving here through a door that review never looked at.

1.3 What happened to that canvas afterwards

The row’s own record, from /Users/lfigea/.openclaw/workspace/state/ledger/duplicate-invoice-criterion-output-schema.json:

time event
14:27:08 transition:framed — opened
14:27:09 canvas:failed — exit 2: Canvas-Exit: 2 — the tool or its environment is wrong; do not touch the canvas
14:34:35 transition:executing — started
14:45:08 – 14:48:58 the 36 writes above
15:34:43 transition:blocked — the plan did not finish, …-baseline-check said FAIL
15:54:53 transition:executing — resumed
16:15:11 transition:blocked — the same gate, again
16:48:16 transition:executing — resumed
17:28:00 transition:done — validated
17:28:10 canvas:failed — freeze: exit 2: …

So the canvas was born by hand because the automatic create had failed a quarter of an hour earlier, was filled in during the run, and then recorded none of the two failures, two recoveries and one close that followed it. Forty-five minutes later a step was arguing with a gate the canvas does not know exists.

1.4 What this supports, and what it does not

Supported. In this window, on the one canvas a person touched at all, the person wrote once and did not come back. There is no instance in this store of a person keeping a canvas current, and no instance of a person writing to one from the tab.

Not supported, and this is most of it. One canvas, one person, one afternoon is not a measurement of a habit. In particular:

  • The denominator is tiny, and smaller than it first looks. The tab’s write path merged at 2026-09-24 09:47 (ledger-orchestrator #107) and a server carrying it has been up since 14:46:37 — but a canvas is only open to a tab edit while it is both born and unfrozen, and five of the seven ledger-row canvases here were frozen by their rows closing. Summed over the seven, the store offers 7.5 canvas-hours in which a person at that page could have written anything at all: 3.4 of them on one canvas, 2.2 on a second, and bc-10334520493-verify-the-drift-check-delivery frozen at 13:34 — before the server existed — contributing none. Zero tab writes across 7.5 canvas-hours is a real fact about those hours and a weak one about habits. Nobody was asked to write, and no canvas here belonged to work a person was doing by hand.
  • The one person-written canvas is the one whose row could not make its own. Its automatic create had failed, so a person creating it at all was already an intervention. A canvas somebody had to build by hand before they could use it is the worst possible sample for “does somebody maintain one when nothing forces them to”.
  • Three minutes and fifty seconds is a sitting, not a period of neglect. What the store shows is a burst followed by silence. Whether that silence is staleness depends on whether anything in those 2h39m changed the shared understanding, and this review did not read the run’s 22 output files to find out. It reports the shape and stops.

So: a first data point, consistent with the worry product-spec.md:124 records and far too small to confirm it. The experiment the design actually wants — a person with a canvas of their own, a tab in front of them, and nothing telling them to write — has not been run, and §6 says what running it would take.


2. Reversal condition 1 — the over-budget notice. Did not fire, and could not have.

canvas-in-prompts.md §1, in its own words:

if over-budget canvases are observed being carried run after run with no step ever resolving one, then the notice is not working as an instruction and something with teeth — a step that may do nothing but resolve, or a framed-side gate — has to replace it.

The budget is BUDGET_CHARS = 20000 in orchestrator/canvas.py, measured on the document as bin/canvas read prints it — the Canvas-Base: line plus the XML.

Largest canvas in this store, now: bc-10327745654-the-ledger-orchestrator-repository-git-g, 11,549 characters — 57.7% of the budget. Every other one is between 392 and 5,875.

Largest any canvas has ever been, at any commit: the same document, 11,495 bytes of XML at commit 2e704d2. Measured over every commit of every file:

for f in "$STORE"/*.xml; do
  b=$(basename "$f")
  peak=$(for sha in $(git -C "$STORE" log --format=%H -- "$b"); do
           git -C "$STORE" cat-file -s "$(git -C "$STORE" rev-parse "$sha:$b")"
         done | sort -rn | head -1)
  printf '%7s  %s\n' "$peak" "$b"
done | sort -rn

That is bytes of XML; read adds the 54-character Canvas-Base: header line on top, which is where 11,495 becomes 11,549.

Nothing in this store has ever been over budget, so nothing could have been carried over budget run after run. The condition is untested, not refuted, and the judgement call stands unexamined. canvas-in-prompts.md §1 measured its own budget against a canvas — “canvas A”, 28,262 characters, 141% — which is not in this store; §5.1 explains where it went.


3. Reversal condition 2 — is_untouched()’s narrowing. Fired.

canvas-in-prompts.md §2, on the deliberate narrowing of orchestrator/canvas.py::is_untouched() — the canvas exists, and every Canvas-Author in its log is task-ledger | open:

What it misses: a canvas written in a row’s first run and never again. What would reverse it: seeing that case happen for real.

That case happened, on duplicate-invoice-criterion-output-schema, and in a harder form than the sentence anticipated. is_untouched() returns False for it. The row was never flagged, and could not have been — not because a run wrote to it, but because no author in its log is task-ledger | open at all. The birth commits that would carry that string were never made: create failed at 14:27:09, and the person who made the canvas by hand at 14:45 authored it as themselves. The predicate keys on a string that a failed create guarantees will never appear, so the rows likeliest to have an unmaintained canvas are exactly the rows the predicate cannot see.

Present values, for the record — orchestrator.canvas.is_untouched(<ledger-id>):

canvas is_untouched is that right?
bc-10336256184 True yes — one linked run, nothing has written
bc-10334523272-what-a-person-and-the-steps-did True yes — this review’s own row
bc-10327745654-… False yes — fourteen step writes
bc-10334520493-verify-the-drift-check-delivery False yes — three step writes
bc-10334522382-… False yes — one step write
bc-10336603973-… False yes — three step writes
bc-10336754422-canvas-operating-skill False yes — four agent writes
duplicate-invoice-criterion-output-schema False no — nothing that ran ever wrote to it

One row, so this reverses nothing on its own; what it does is exactly what the sentence asked for — it is the case, seen for real, and the ruling said that was the trigger for looking again. Note what is not claimed: canvas-in-prompts.md §2 also ruled that a finding of staleness is not a licence to escalate, and nothing here argues with that. The question this opens is what is_untouched should key on, not what the sweep should do about the answer.


4. Reversal condition 3 — N = 10 edits in the prompt. Did not fire.

canvas-in-prompts.md §4:

Judgement call; what would reverse it: if steps are seen re-deciding things a canvas settled 11 edits ago.

Only one canvas here has more than ten edits written by steps: bc-10327745654-the-ledger-orchestrator-repository-git-g, 18 commits, of which 15 are caller-written. It is also the only canvas in this store where steps re-decided anything at all — five replaces. Numbering the file’s own log:

# commit edit the node’s previous edit distance
11 7b4d797 replace wmss #7 4
12 a1c0515 replace asrt #9 3
13 ccffd38 replace mtmt #10 3
14 3a31ec5 replace mtmt #13 1
17 c884758 replace asrt #12 5

Maximum distance five. In every case the node’s current state was inside the last-ten window EDITS_IN_PROMPT puts in the prompt, so no step re-decided anything it had not been shown. The other canvas with more than ten edits, duplicate-invoice-criterion-output-schema, was written entirely by one writer in one sitting and read by no step at all, so it cannot speak to this.

This is weak evidence for N = 10 rather than evidence against it: five re-decisions on one row, all at half the window or less. What it rules out is the specific failure the number was chosen against, in this window.


5. Four things the reading found that nobody asked for

These are not answers to §§1–4. They are what a reading of the store turned up, and each is recorded because the store is the only place it is visible.

5.1 The store’s window is not the ledger’s, and the reason is a deployment

bin/task-ledger open has created a canvas since 137859c, merged to ledger-orchestrator main on 2026-09-23 14:47. This store’s first commit is 2026-09-24 12:51:03, and it is the repository’s initial commit — nothing was deleted; there was nothing here before.

The gap is the deployment. The live checkout at /Users/lfigea/Projects/ledger-orchestrator is what runs on this machine, and its own reflog shows it sitting on feature branches — canvas-why-brief, then canvas-why/bc-10334520493-evidence-clause, then fix/retry-and-check-recovery — from 2026-09-23 02:15 until 2026-09-24 11:54. Those branches were cut before the merge:

cd /Users/lfigea/Projects/ledger-orchestrator
git show 4b9f938:bin/task-ledger | grep -c _born_a_canvas   # 0   (canvas-why-brief)
git show 74e3c1a:bin/task-ledger | grep -c _born_a_canvas   # 0
git show e98145e:bin/task-ledger | grep -c _born_a_canvas   # 0
git show 0e9b211:bin/task-ledger | grep -c _born_a_canvas   # 4   (back on main, 11:54)

Twenty-six ledger rows were opened in that window. Not one has a canvas, and not one has a canvas:failed event — a bin/task-ledger that does not know canvases exist cannot report that a canvas is missing, so the degradation the design took care to make loud was silent for twenty-one hours. Among the twenty-six are bc-10334519573-… (the tab’s own write path), bc-10334515971-… (the tab itself) and bc-10331851610-… (the row’s rendered-canvas projection): the three rows that built the surfaces this document exists to measure all ran without one.

It is also where “canvas A” went. canvas-in-prompts.md §1 derived the 20,000-character budget from a 28,262-character canvas for todo 10329619223 and a second one of 1,899 characters. Neither is here, and neither is anywhere on this machine — a find for state/canvas directories returns exactly one path. §2’s “nothing has ever been over budget” is therefore true of this store and says nothing about the one the budget was measured on.

5.2 One commit carries two rows’ edits, and history cannot see one of them

Exactly one of the 99 commits changes more than one file:

59f6946c07e1a26202ac34fa63ef9bcef793301f   2026-09-24 15:09:57
  subject:  insert ttz6: deciding to spend a test file on a Markdown document …
  trailers: Canvas-Node: ttz6 / Canvas-Author: claude-opus-5 | canvas-follow-through
  files:    bc-10336754422-canvas-operating-skill.xml            (+1  — inserts ttz6)
            bc-10327745654-the-ledger-orchestrator-…-git-g.xml   (±1 — ihfu v="1" → v="2")

The subject, the Canvas-Node: trailer and the reason are all about ttz6 in the skill row’s canvas. The change to ihfu — a full rewrite of a node in a different row’s canvas, correcting which workflow run the merge read — rides along unnamed. And so:

bin/canvas history bc-10327745654-the-ledger-orchestrator-repository-git-g ihfu
# one edit: the insert at 6628e1c. The replace is not in the output.

ihfu’s text changed and the canvas’s own history cannot say who changed it, why, or when — which is the one thing --why exists to make impossible.

The mechanism is in canvas/store.py’s write path and is not a race in the document: the edit is staged path-scoped, git add -f -- <path>, and then committed with no pathspec, so anything else already in the index is committed with it. The store is one git repository for every row — orchestrator/canvas.py::create says so in its own docstring — and two writers were active at 15:09:5x. --base could not have caught it: staleness is scoped to the writer’s own file, by design, and this is the other file.

5.3 Every canvas:failed event records the same eight words

Eight rows opened today carry an event whose entire detail is:

exit 2: Canvas-Exit: 2 — the tool or its environment is wrong; do not touch the canvas

Byte-identical, on all eight. orchestrator/canvas.py::create builds it as "exit %d: %s" % (returncode, detail[-1]) where detail is the tool’s stderr split into lines — and the tool’s last stderr line is always the Canvas-Exit: line, the generic meaning of the code. The two lines above it, Canvas-About: and Canvas-Next:, are what name the actual cause and what to do, and they are the ones dropped.

Reproduced, read-only, in the ledger repository:

env -u OPENCLAW_WORKSPACE python3 -c \
  "import sys;sys.path.insert(0,'.');from orchestrator import canvas;print(canvas.create('probe','p','e'))"
# (False, 'exit 2: Canvas-Exit: 2 — the tool or its environment is wrong; do not touch the canvas')

That is one of at least five conditions that produce exit 2, and it produces the recorded string exactly — as would each of the others. So the record cannot distinguish “this machine has no OPENCLAW_WORKSPACE” from “this machine has no xmllint”, and the row that is the only thing able to say “this one has no document” says it without saying why. bin/task-ledger defaults OPENCLAW_WORKSPACE and bin/canvas deliberately does not, which is the asymmetry that makes a row land correctly while its canvas cannot — but nothing in the record establishes that this is what happened on those eight.

5.4 bin/canvas create is not atomic, and this review proved it in the wrong store

In an isolated store (OPENCLAW_WORKSPACE at a fresh temporary directory):

bin/canvas create probe-empty-problem --problem "" --expected-value "x"
# canvas: refusing to write a commit naming jg74 that also changes 1 other node(s) (grux):
#         one edit is one node
# Canvas-Exit: 1
# exit status 1
git -C "$TMP/state/canvas" log --oneline
# bd6782c insert grux: the problem the ledger row states     <- committed
# e1ddcfa create probe-empty-problem: born at open, root only <- committed

create makes three commits. With an empty --problem the first two land, the third is refused by the tool’s own one-edit-is-one-node guard, and the caller is left with a canvas that exists and has no expected-value node. There is no recovery: create refuses to overwrite an existing canvas. The mirror case, --expected-value "", exits 0 and makes all three. bin/task-ledger’s _born_a_canvas passes entry.get("problem") or "" into the same call, so a row opened with an empty problem reaches this.

Declared, because it is in the corpus: the first probe of this was run against the real store before the isolated one, and it left probe-empty-problem-20260924.xml there with two commits, 3aea0a2 and 8f773f2 — the second of which is the head this document pins in §0. It has not been removed. Rewriting an append-only store to hide a reviewer’s own mistake is worse than the mistake, and a later reader re-running §0’s commands will see 99 commits and eleven files and should: that is what was there.


6. What this check did not cover

  • The experiment the question deserves was not run. A person given a canvas of their own, over days rather than hours, with the tab in front of them and nothing instructing them to write, is what would answer §1. This read a store in which that had not happened.
  • One day, one machine, one store. Five hours and seventeen minutes, eight ledger-row canvases, 94 commits not this review’s own. Every count here is small enough that one more row could move it.
  • No reason was graded. VERDICT.md and off-corpus-check.md own that, and this document deliberately does not re-open it — §1.2 notes that 28 of the 36 person-written reasons are two repeated strings, and stops there rather than scoring them.
  • The run’s own outputs were not read. §1.4 says the canvas recorded none of the two blocks and two recoveries; it does not say whether any of them changed the shared understanding, which is the only thing that would make the silence staleness rather than correct restraint. Twenty-two output files exist on that row and this review opened none of them.
  • canvas A is gone and its measurements cannot be rechecked. §2’s budget answer is scoped to a store that never held it.
  • Only one of the three reversal conditions fired, and the two that did not, did not because the store contains no instance of the shape they name — not because the shape was looked for and found absent under pressure.
  • This is again a reader at no distance. As in both earlier documents: the bar in §1 is about somebody months later, and everything read here is hours old.

Written 2026-09-24 for Basecamp todo 10334523272, ledger row bc-10334523272-what-a-person-and-the-steps-did. Store /Users/lfigea/.openclaw/workspace/state/canvas at 8f773f2ce113a2aec0315b49df9f860562971d53, 99 commits; canvas checkout at c54eaa1, branch docs/bc-10334523272-maintenance-check; ledger-orchestrator checkout at 0b35ce6.


A later attempt to run the experiment §6 names found two things in front of it, and wrote them down beside this document: ./experiment-preconditions.md. It reports that the denominator §0 publishes can no longer be re-derived — ps -o lstart= -p 33867 prints nothing now — and that no canvas in this store has ever stayed writable long enough for the experiment to be run on it.