Unattributed edits

Every edit this store made and cannot attribute. There is exactly one, and this file is where it is recorded, because the alternative was recording it in a Basecamp comment — which is not the repository, and not where a reader of bin/canvas history will be standing when the tool does not answer them.

No history is rewritten to produce this file. The commit that caused it stands. What is written down is what the store can still say about the edit, what it cannot say, and why the gap cannot be closed from inside the store.

The edit

  • Node ihfu, in the canvas for ledger row bc-10327745654-the-ledger-orchestrator-repository-git-g.
  • What changed — v="1" to v="2", a full rewrite of the node’s text.
  • The commit that carries it — 59f6946c07e1a26202ac34fa63ef9bcef793301f, 2026-09-24 15:09:57 +0200.
  • What that commit says about itself — 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, Canvas-Base: 6628e1c97a9a561a9632b71e6edb9648be05e184. Every one of those is about ttz6, a node in a different canvas — bc-10336754422-canvas-operating-skill.xml — which the same commit also changed.

bin/canvas history bc-10327745654-the-ledger-orchestrator-repository-git-g ihfu prints one edit, the insert at 6628e1c, and will never print this one. That is correct behaviour and not a second defect: a node’s history is the commits whose Canvas-Node: trailer names it, and no commit names ihfu twice. Loosening the match would make every node’s history a guess.

How it happened

canvas/store.py::_write_and_commit staged path-scoped — `git add -f –

` — and then committed with **no pathspec**. `state/canvas` is one git repository for every ledger row, which `node-identity.md` §1 settles and which nothing here reopens; one repository is one *index*. So a commit with no pathspec took whatever any other writer had staged in that index, and at 15:09:5x two writers were active. This is the only time it happened. Read at head `3b0cbd1954c78c59b67546c18872e8963cc5f6e9`, one commit out of 115 in the live store touches more than one file, and it is this one. The count moves as the store grows; the query does not, and it is the query and not the number that is the claim: git -C "$OPENCLAW_WORKSPACE/state/canvas" log --format=%H | while read -r sha; do n=$(git -C "$OPENCLAW_WORKSPACE/state/canvas" show --name-only --format= "$sha" | grep -c .) [ "$n" -gt 1 ] && echo "$sha touches $n files" done `--base` could not have caught it and is not at fault. Staleness is scoped to the writer's own file by design — that is what keeps a read-then-write round trip against an unchanged canvas silent in a busy store — and this was the other file. The guard that *did* hold, "one edit is one node", is about nodes within a document and not about documents within a commit. It is fixed: the commit now names its path too, so git builds it from the head plus that one file and leaves every other staged entry for the writer that staged it. See [*What one commit contains*](/canvas/#what-one-commit-contains) and `tests/test_store.py::OneCommitIsOneCanvas`. ## What is recoverable, and what is not **Recoverable — the edit itself.** The bytes are in the log and the store's own read surface reaches them: git -C "$OPENCLAW_WORKSPACE/state/canvas" show 59f6946 \ -- bc-10327745654-the-ledger-orchestrator-repository-git-g.xml bin/canvas read bc-10327745654-the-ledger-orchestrator-repository-git-g \ --since 6628e1c **Recoverable — who made it, and roughly what for.** Not from the store: from the run that made it. The writer was the `ci-green` step of ledger row `bc-10327745654-…`, and its own report says so in `state/orchestrator/bc-10327745654-the-ledger-orchestrator-repository-git-g-state/bc-10327745654-the-ledger-orchestrator-repository-git-g-ci-green.report.md`: it had written `ihfu` at `6628e1c`, noticed that its text fused two statements — "the only workflow run on that sha is 36000852204 is not it but 36002820840", which can be read as attributing the success to the run that had failed — and issued a `replace` to separate them. That is the intent, in prose, from the process that held it. **Not recoverable — the reason, as the store records reasons.** A reason in this store lives in a commit subject and nowhere else. The subject that carries these bytes is `ttz6`'s. The `--why` the `ci-green` step wrote for its correction was never committed anywhere, because its own `git commit` ran after the sweep, found nothing left to commit, and exited non-zero. There is no mechanism that can put that sentence into `59f6946` without rewriting history, and rewriting it would falsify a commit another canvas's node legitimately depends on. So it is recorded here as gone, and not quietly reconstructed: a reason invented after the fact is a sentence in the history that reads like evidence and is not. ## The second thing this cost, and why it cannot recur The sweep did not merely steal the bytes; it left the store in a state worse than either outcome. `_write_and_commit` takes its rename back when git refuses the commit, so the `ci-green` step's rollback put the *pre-edit* `v="1"` back on the path and staged it — while `v="2"` was already at HEAD, committed by somebody else's commit. The next write by any writer would have carried the revert silently. The step noticed, restored the file with `git restore --source=HEAD --staged --worktree`, and said so in its report. The rollback was right; its precondition was not. With the commit path-scoped, the `ci-green` step's own commit finds its own staged file exactly where it left it and succeeds, so there is no refusal, no rollback, and no edit of one canvas riding in another canvas's commit. ## Adding to this file If a second one is ever found, it goes here, in the same shape: the node, the commit, what the commit says about itself, what is recoverable and from where, and what is gone. A store that cannot attribute an edit should be able to say which edit, once, in writing — not leave a reader to discover it by diffing.