Project observer scope: does a belief vanish from its own project?¶
When one memory store serves several projects and a session reads it through
its project's observer (claude_code.observer_scope: project), the
write path still runs across the whole store, so a belief project A states can
be retired by something project B did, and A's session then sees neither its
own claim nor the one that replaced it. The ADR named two such mechanisms and
declined to flip the default until the rate was measured; the dogfood
store, holding one real project, cannot produce that number.
particles benchmark observer does.
What it does¶
A seeded generator builds two repositories, alpha and beta, whose
MEMORY.md files talk about the same generic subjects (the repository, the
test suite, the deploy pipeline, the release process) and evolve
independently over --days. Three kinds of line:
| Kind | Example | What it exercises |
|---|---|---|
fact |
"The default branch of the repository is main." |
A value for a slot both projects have. Values are chosen independently, so the projects often disagree. |
rule |
"Every commit to the repository needs a sign-off." | Slot-less and byte-identical wherever it appears. Stated by both projects it becomes one particle. |
own |
"The build of project alpha takes seven minutes." | A subject only this project has. The control. |
Each day, every project whose file changed is harvested exactly as the
SessionEnd hook would harvest it (LOCAL_MARKDOWN, MUTABLE, a file://
URI, the claude-code and project: tags), then extracted through the
real ingest pipeline (rung 2.5, duplicate suppression, the generation
cascade, all of it) with perception scripted: an extractor that emits one
candidate per line, and a contradiction probe that answers from the slot
table. Every other LLM purpose is refused and counted, so a run makes zero
calls. Two global lines are deposited by hand, keyless, as the lines every
observer should see.
At the end of every day, for each project, every line its file currently states is checked through that project's observer. If it is not in view, the particle holding it says why:
superseded_by_update: rung 2.5 paired it with the other project's value for the same subject, and the other project's was the later deposit. Insinglestore mode two memory files are one lineage:file://URIs have no authority, both areLOCAL_MARKDOWN, neither carries an author.cascade: the generation cascade. Both projects stated the line, it became one particle, the other project dropped it, and the new snapshot of the other project's entry retired the particle for both.active_elsewhere: the store holds the claim ACTIVE, but the surviving particle is attested only by the other project's sources: this project's own copy was retired in an earlier round of supersession, and its restatement has not been re-deposited since. The store believes it, this project's file says it, and the lens hides it. This one the ADR did not anticipate; it is the only cost the lens adds over the store-wide view.
Beside each line the same check runs without the lens (is the claim ACTIVE anywhere?), and the lens's own correctness is checked (does a project see anything it never stated?). Rates carry their denominators.
Result (2026-09-21)¶
Twenty seeds, fourteen days, single store mode, the real embedding model,
zero LLM calls. Full report: observer-scope-2026-09-21.md.
| Measure | Rate |
|---|---|
| Own lines in view for their project | 79.4% (3,393 / 4,273) |
| … the same lines ACTIVE anywhere (no lens) | 80.2% (3,428 / 4,273) |
| Own lines retired by the other project's activity | 20.6% (880 / 4,273) |
| Winner in view after a cross-project supersession | 0.0% (0 / 810) |
| Lines in view a project never stated (lens leak) | 0.0% (0 / 3,393) |
In view, fact lines |
62.4% (1,397 / 2,240) |
In view, rule lines |
97.5% (1,436 / 1,473) |
In view, own lines |
100.0% (560 / 560) |
Causes: superseded_by_update 810 · cascade 35 · active_elsewhere 35.
Reading the numbers¶
A shared-subject fact is out of view for its own project 37.6% of the
time. The mechanism is a ping-pong. Day one, alpha says main and beta says
master; beta's deposit is later, so rung 2.5 retires alpha's line. Whenever
alpha's file next changes for any reason, its re-emitted main is a fresh
candidate (the retired copy is not in the duplicate index) and it retires
beta's master, whose next re-deposit retires it back. Each project holds
its value for roughly the intervals between the other project's deposits.
The winner is never in view for the loser (0 of 810), so under the lens
the loser sees nothing for that slot; without the lens it sees the other
project's value. Neither is what the loser's own file says.
A shared rule is out of view 2.5% of the time, for exactly the window between the other project dropping it and this project's next re-deposit. This is the cascade acting on a particle two entries attest, which is a defect on its own terms: the entry that still attests the line was never asked.
The control holds and the lens is correct. Lines about a subject only one project has never vanish, and no project sees a line it never stated.
The lens itself costs 0.8 points. Compare the first two rows: nearly every
own line the lens hides is one the store had already retired. The difference,
35 active_elsewhere checkpoints, is the lens's whole marginal cost, and it
is the price of "attested in" being read from the surviving particle. A
re-deposit of the stating project folds its attestation onto that particle
and the line comes back.
What the world does not represent. Every fact slot here is shared by both projects and valued independently, a worst case for overlap. Real projects overlap on generic subjects less, and the rate scales with that overlap; the ping-pong only starts once they do.
Result after observer-aware reconciliation (2026-09-23)¶
The write path now reconciles a pair only when every project that currently
observes the existing claim also observes the update, a project
observes a claim only while one of its sources currently states it, and the
generation cascade retires only what no current source states. The fixture
gained what the first run could not see: a chunked arm, whose extractor
sends two lines per chunk through the real carry-forward so an unchanged line
is carried rather than re-emitted; a write census, which attributes every
retirement as own or cross-project from the retired claim's scope just before
the extraction that retired it; and, after the last day, one global line
contested by each project. The generator is unchanged, so every world is the
first run's world. Full reports: observer-scope-2026-09-23.md.
| Measure | First run (2026-09-21) | lines arm |
chunked arm |
|---|---|---|---|
| Own lines in view for their project | 79.4% | 100.0% (4,273 / 4,273) | 100.0% (4,273 / 4,273) |
| … the same lines ACTIVE anywhere (no lens) | 80.2% | 100.0% | 100.0% |
In view, fact lines |
62.4% | 100.0% (2,240) | 100.0% (2,240) |
In view, rule lines |
97.5% | 100.0% (1,473) | 100.0% (1,473) |
In view, own lines |
100.0% | 100.0% (560) | 100.0% (560) |
| Lines in view a project never stated (lens leak) | 0 | 0 | 0 |
| Cross-project supersessions | 810 checkpoints | 0 | 0 |
| Cross-project cascade retirements | — | 0 | 0 |
Own-value supersessions (SUPERSEDED_BY_UPDATE) |
— | 63 | 63 |
Declined pairs / with a CONTRADICTS relation |
— | 936 / 936 | 262 / 262 |
Distinct CONTRADICTS relations |
— | 157 | 157 |
| Global line contested by a project → review | — | 40 / 40 | 40 / 40 |
| Chunks carried forward | — | — | 926 of 1,432 |
Each part, measured alone. The same twenty seeds after each slice of the
change, own lines in view (fact / rule), with the cross-project
supersessions and cascades the census attributed:
| After | lines arm |
chunked arm |
|---|---|---|
| §1 + §5 (re-observation recorded; identical files are two entries) | 79.4% (62.4 / 97.5) · 647 + 27 | 71.6% (58.7 / 80.7) · 545 + 57 |
| + §3 (observed means currently stated) | 79.4% (62.4 / 97.5) · 647 + 27 | 71.6% (58.7 / 80.7) · 545 + 57 |
| + §4 (the cascade retires only unattested claims) | 80.3% (62.4 / 100.0) · 647 + 0 | 74.7% (58.7 / 90.0) · 529 + 0 |
| + §2 (the precondition) | 100.0% (100.0 / 100.0) · 0 + 0 | 100.0% (100.0 / 100.0) · 0 + 0 |
The first row reproduces the first run exactly on the lines arm (79.4%,
causes 810 / 35 / 35), which is what makes the comparison a like-for-like one.
§3 moves nothing on its own, because until §2 and §4 land a line a project's
file drops is retired by the cascade anyway; it matters once claims survive a
project's edit. On the real dogfood store it is visible at once (below).
What the chunked arm found. Before the final slice, the chunked arm lost
own lines that only a project's own edits had touched. The carry-forward
lookup matched a chunk through the provenance edge index, whose single row per
(particle, entry) keeps the first chunk hash: a claim folded in from another
file, or whose line had moved into a different chunk, was neither carried
forward nor re-emitted, and the cascade then retired a line the file still
stated. A chunk whose text came back after one of its lines had been retired
was a cache hit on the survivors, so the restated line never came back. The
lookup now reads the provenance refs, and a chunk whose claim was retired by
the cascade or an update is re-extracted once. Without the second half the
arm stands at 99.1%.
Reading it. The ping-pong is gone: two projects that disagree about a slot each keep their own value, joined by one recorded contradiction (157 distinct pairs across the twenty worlds), and a project's own update still retires its own earlier value (63 times). The lens now costs nothing: the first two rows are equal, so every line a project states is a line it sees. A project that contradicts one of the operator's global lines reaches review every time, where before rung 2.5 retired the operator's line silently.
Probe spend. The precondition runs after the probe, so a declined pair is
paid for once per extraction that meets it: 936 declined probes on the lines
arm, where every line is re-emitted on every deposit, against 262 on the
chunked arm, where unchanged lines are carried forward. The first run paid
the same probes to retire the other project's line instead.
On the dogfood store (a copy of 28,245 ACTIVE beliefs, one real project),
the change is small and all of it expected. This repository's observer sees
27,951 beliefs; 294 read as lapsed, and they are exactly the five memory
files whose superseded generations the generation backlog sweep has never
retired (particles corpus refresh --backfill-cascade would retire 304 there).
The same sweep now retires 304 beliefs instead of 361: 57, in 12 entries, are
still stated by another source's current snapshot. The scope join takes
0.15–0.23 s over every ACTIVE belief (0.17–0.25 s before) and the scoped
digest 2.1–2.2 s (2.05–2.2 s before), well inside the 10 s hook deadline.
What it decides¶
The decision record blocks the default flip on a non-trivial rate until the
write path is made observer-aware. 37.6% of shared-subject facts is non-trivial, so
claude_code.observer_scope stays store by default. The remedy is on the
write path, not the lens: candidacy that does not pair two claims whose
observer scopes are disjoint, which would turn the ping-pong into
two projects each holding their own value, the situation the thesis
describes. The cascade half is a smaller, separate fix.
2026-09-23. With the write path observer-aware, every line each project
states is in view on both arms, and no retirement crosses a project. The
blocker the first run found is addressed; flipping claude_code.observer_scope
to project by default remains the owner's decision, and the
fixture remains a worst case on overlap.
Re-run after any change to rung 2.5, duplicate suppression, carry-forward, the cascade, the observer precondition or the lens, on both arms: