Installing — add what is missing, refresh what nobody changed, never touch what was edited
A package's installPaths are copied into the viewer's own space ({viewer}/{package}, plus
{viewer}/Skill and {viewer}/Agent for the fixed segments) by Localizer.Localize — the one
engine behind Get, Repair, Update, every acquisition's self-install, and the whole-course
row of the per-exercise Restore dialog. This page is the contract that engine keeps for copies that
are ALREADY there.
The problem this replaced
Until Store 1.9 a second run only ever added: every path the viewer held was excluded up front
(Localizer.CopyPlan) and the bulk create verb skipped existing paths server-side. That kept the
learner's work safe by construction, and it also meant a copy was a snapshot forever: a fix the
course authors shipped inside a copied page reached only learners who had not installed yet. The
cover already knew the source had moved (InstallStatus.Evaluate → Outdated, the hero relabelled
Repair as Update), but Update ran the same add-only plan and changed nothing.
Measured on AgenticOffice (MeshWeaver.Education): the exercise pages' back-links were rewritten in
the repo on 2026-08-05 at 15:31 UTC; a learner's copy taken that morning at 13:44 UTC still carried
the dead ../../ links a month later — all 24 files that commit touched, plus 14 more that changed
afterwards — and nothing short of Reset (uninstall + Get, which discards every edit) could mend
it. The per-exercise Restore dialog could not tell that copy apart from an edited one either: it
fingerprints the copy against the current source, so "stale because the source moved" and "the
learner's work" read the same, and both sat behind the discard-your-edits confirmation.
The record: what the install wrote
Every run records, per copy it writes, the content fingerprint of what it put there —
ContentFingerprint.Of(written), the same primitive the Restore dialog compares with — in the
viewer's install record, {viewer}/_Install/{repo-slug} → the item's fingerprints, keyed by the
copy's path relative to the viewer (AgenticOffice/01-Willkommen/Exercise/WasWeissDerAgent,
Agent/Coach), so the fixed-segment roots that land beside the target are covered too.
A fingerprint ignores the render cache (prerenderedHtml) and the sync stamp, so re-rendering a
page is not an edit (RepairTests.RenderingACopy_IsNotAnEdit), and it is taken AFTER the install's
own transform (re-anchored @/@@ references), so an untouched copy fingerprints exactly as its
source would (RepairTests.SourceFingerprint_MatchesThePristineCopy_DespiteTheInstallTransform).
The rule (RefreshPlan.Of, pure)
For every node the install would write today, against the copy the viewer holds and the recorded fingerprint for that key:
| the copy | the record | verdict |
|---|---|---|
| absent | — | Add (bulk create verb; a create can never overwrite), and record it |
| equals the record | source moved | Refresh with the current source (upsert verb), and re-record |
| equals the record | source did not move | nothing to write; the record stands |
| differs from the record | — | Keep — the learner's work; the record is NOT touched, so reverting to the pristine text makes the copy pristine again |
| no record, equals today's source | — | Keep, and record it now — pristine from here on, so the next update can refresh it |
| no record, differs | — | Keep — stale or edited, the engine cannot tell, and guessing wrong costs someone their answers |
The run reports what it did beyond adding: the terminal LocalizeProgress carries Refreshed and
Kept, the dialog's status string trails them (done:{n}:{target}:{refreshed}:{kept}), and the
finished line says "{0} page(s) you had not changed were brought up to date; {1} that differ from
the course were left as they are." The second count is deliberately not called "your edits": a copy
with no record differs for reasons the engine cannot see.
What it does not do, on purpose
- A copy made before the record existed is never refreshed while it is stale. It enters the record only through the "no record, equals today's source" row. The way out for a learner is still Reset, or the per-exercise Restore behind its confirmation.
- Edu's learner-first-open paths now enter this engine through
Store/InstallRequest.Edu/Lesson'sEnsureCopyused to copy the course's declared roots itself, with core'sNodeCopyHelper, and wrote no manifest — so a course a learner first met through a lesson had no item, no fingerprints, and nothing above could ever reach it: measured onsglauser/AgenticBusiness, 60 nodes frozen at version 1 since 2026-08-31 while the central course had been fixed, showing the learner an authoring warning the course no longer had. It now files aStore/InstallRequestnode at{viewer}/_InstallRequests/{package}and the type's watcher runs THIS engine — therequiresclosure leaves-first, then the package into{viewer}/{package}with the entitlement gateEntitlements.RequiresEntitlementcomputes — so a first-open copy and a Get from the cover are the same fact: the same item, the same fingerprints, the same Apps-band tile. The request is the front door for any caller that cannot compile againstStore/Installer/Source(cross-type reuse is source inclusion, and the writer is 466 KB); it carries onlypackageId,requestedAction: Installand avialine, the viewer is the partition the node sits in, and the invoker (the framework-stamped author) must be that owner or the System identity — nobody installs into somebody else's space through it. The outcome streams back onto the node (state/message/error/installedPath/log); a learner reading a public lesson of a course they never acquired gets aFailedrequest naming the refusal and the read-only master. 🚨 The viewer's entitlement to the package is verified BEFORE the dependencies run (InstallRequestControlPlane.InstallChain):DependencyInstall.InstallDependenciesgrants the viewer each auto-installable dependency, because its ordinary callers — the cover's Get, an acquisition's tail — have already established the viewer's claim on the dependent, and a request has established nothing; run first, it would hand a requester of a gated course every dependency before the engine refused the course. The engine's own gate stays where it is; this one keeps the dependencies behind it (TheEntitlementGate_RunsBeforeAnyDependencyIsGranted). 🚨 And a malformed request is reported, never dropped: the watcher gates onIsArmed(action asked, no run in flight), not onShouldRun, so a request that names no package reachesRefusaland lands asFailedwith the reason — gating onShouldRunleft exactly that request parked atRequestedforever (ABlankPackage_IsArmed_SoItIsReported_ButNeverRuns). Edu declaresStore@^1.14.0for this type; an older Store satisfies nothing here. The module-level paths behindStartExercise,GoToMyCopy, and course-rail exercise links now file the same request and wait for its verdict. That also submits a copy made by the former direct subtree copier: missing declared nodes may be added, and an unrecorded copy that still matches the source may be recorded; a differing unrecorded copy is deliberately kept because the Store cannot distinguish an edit from stale content. The old directIMeshService.CopyNoderoute is gone. Pinned live byInstallRequestTests.ARequest_Live_InstallsThePackage_AndWritesTheStoresOwnRecord, which removes both partitions it mints whatever the verdict. - The per-exercise Restore dialog is unchanged: its rows are
*/Exercisesubtrees, it still compares against the current source, and it is still the way to get the pristine version back OVER an edit. - Uninstall clears the record with the coverage list: a fresh copy must never pass for a pristine old one.
🚨 Reset removes nothing unless the read that plans it names the viewer BY VALUE
Reset to defaults is Uninstall(...) concatenated with a fresh Localize(...)
(InstallDialog.ResetToDefaults), and Localize is additive by contract — the bulk verb skips
existing paths server-side. So the whole of Reset's destructive half is the uninstall's delete
set, and if that set is empty the reset is a no-op that still reports success.
On memex, 2026-09-07 08:11:48–08:13:32Z, that is exactly what happened. The dialog read
✅ Done — 11 item(s) installed in rbuergi/AgenticEngineering; afterwards all 211 copied nodes
were still at version 1, created 2026-08-13, carrying the pre-2026-09-04 quiz. A learner who
resets to discard their edits, or to pick up a corrected exercise, kept precisely what they wanted
gone.
The mechanism, and why it only bites in production
Uninstall derived its delete set from an unstamped request:
MeshQueries.QuerySnapshot(mesh.Query<MeshNode>(MeshQueryRequest.FromQuery(
$"path:{nodesTarget} scope:subtree limit:{SnapshotQueryLimit}"))) // ← no viewer
FromQuery(query) leaves UserId null. MeshService.Query stamps the ambient viewer at call
time — not at subscribe time — and deliberately does not pin the Anonymous fallback. This request
is constructed inside a SelectMany over the manifest read, i.e. on the thread that delivered that
result: a hub action-block or IIoPool thread the circuit's AsyncLocal AccessContext never
reached. So the viewer resolved Anonymous, RLS dropped every node in the learner's private
partition, and the read answered empty — which is byte-identical to "nothing is installed
here". The record was cleared, the additive re-install skipped all 211 existing paths, and the
dialog reported Done.
It is production-specific for a reason worth remembering: on a single-pod disposable mesh the same code replaces the copy correctly (MeshWeaver.Education #281, run 34125224148, twice), because there the ambient context happens to survive. A green e2e on a fresh mesh is not evidence about this defect.
The rule
Every read whose result composes a WRITE — and every read whose emptiness would be shown to a human as "you have nothing here" — passes the viewer as a VALUE, and fails closed:
Localizer.ViewerSubtree(path, viewer) // FromQuery(...).ForViewer(viewer).RequireViewer()
ForViewer is what makes it independent of any scheduler hop; RequireViewer is the half that
matters most, because stamping alone still answers empty when the viewer is blank, and empty is
again read as "already clean". With it, a viewer that cannot be resolved throws
QueryIdentityUnresolvedException — "I could not see it" can no longer be read as "it is not
there". Three reads on this path were unstamped and all three now go through it: the uninstall's
delete set, CopyRoot's installed-scope read, and the install verify's re-read.
🚨 Not AsSystem(). These results compose the DELETES, and a system-stamped emission carries the
elevation into every write built on it — the defect
UninstallsSystemRead_DoesNotStampTheDeletesOrTheResetThatFollows exists to pin. Reads that
genuinely need elevation (the gated fixed-directive sources) are wrapped in SystemIdentity.Run,
which scopes the impersonation around the subscribe instead.
Two things this does not fix, deliberately
- The Done line still counts declared roots, not effects.
✅ Done — 11 item(s)isroots.Countfrom the localize plan; it never said anything about the uninstall, and it would have read the same over zero removals even with the delete set correct. Reporting the effect — nodes removed, andCreated/ExistingfromCreateNodesResponse, which are already returned and already discarded — is the remaining half. - A verification built on the same read cannot fail. An uninstall verify that re-reads with an
unstamped request and intersects planned with still present yields zero survivors from an
empty answer and passes. The verify must use
ViewerSubtreetoo, and its log must sit above thetotal == 0early return — otherwise it is unreachable in exactly the case it was written for.
The copy and the verify share ONE notion of "the same node" (MeshWeaver#4817)
[ProvisionPlan] 'Voice' FAILED in phase 6/8 'Verify install': Install did not persist 1 node(s): 'rbuergi/Agent/voice' … please retry the install — 42 occurrences over six pods in the two days
this fingerprint has existed (and under its predecessor's since 2026-09-12), the same install
re-failing at every pod boot. It was never a lost write and never a stale read, and no retry could
have passed.
Measured on the public instance, 2026-09-20. The package ships its agent at
Voice/Agent/voice. The viewer has held rbuergi/Agent/Voice — capital V, created 2026-08-18 —
ever since. get @rbuergi/Agent/voice answers Not found; the copy phase's read of the declared
root's installed subtree, path:rbuergi/Agent scope:subtree (the source has a node at
Voice/Agent, so that is order[0]), lists rbuergi/Agent/Voice. Then, inside ONE install:
| phase | question | comparer | answer |
|---|---|---|---|
copy (RefreshPlan.Of, CopyPlan) |
does the viewer already hold this copy? | OrdinalIgnoreCase |
held — nothing is written |
verify (MissingInstalledPaths) |
is the expected copy present? | Ordinal |
missing — the phase fails |
Each half is defensible where it stands. The copy is right: a path is a case-insensitive identity
to the platform — the in-memory store keys nodes OrdinalIgnoreCase, so a create at the other
casing would land ON the viewer's node and overwrite it; Postgres folds namespace/nodeType
equality and indexes LOWER(path), so a second row would sit behind every listing of that
namespace. Together they are an install whose verify can never succeed — the copy will never
write the node and the verify will never see it — and StandardPacks.EnsureForAllUsers re-enters
it for every user at every activation, because no manifest is ever written.
The fix is one statement of path identity — Localizer.PathIdentity — read by both halves
(CopyPlan, RefreshPlan.Of, MissingInstalledPaths, MissingAfterInstall). It loosens nothing
about presence: a copy absent under every spelling still fails by name, and
Verify_ACopyHeldUnderAnotherCasing_IsPresent_ButAnAbsentOneStillFails pins both directions. The
viewer's own differently-cased node is kept, exactly as any other unrecorded copy is — the
install is never destructive.
The second shape on that issue, and why the failed verify now asks the store
The other lines on #4817 are a different shape: for one user, every expected path missing —
the home root included — for every package, at every boot. 🚨 That one was not measured: the
user's space is outside what the investigating credential can read, and Not found from outside
your own denominator closes nothing.
What the code establishes is that the two halves of an install do not run as one identity: the
all-users sweep writes as System into a home that is not the caller's, while the verify reads
with the viewer stamped by value (ViewerSubtree, see above). A home whose owner cannot read
it therefore takes every write and shows its owner nothing, and the verify's text — "accepted but
not stored (a transient hub error?) … please retry" — offers two guesses, neither of which is
that.
So a verify that has already failed looks once more at the same scopes as System
(SystemIdentity.Run, sealed) and says which it was (Localizer.NotPersistedOrHiddenText):
"Install stored …, but 'x' cannot read them — … an ACCESS problem on that space, not a lost write.
Retrying will not help." It changes what the failure says, never whether it fails: a viewer
who cannot read their install has not been given one, no manifest is written, and a probe that
faults or answers nothing leaves the original failure in place.
🚨 The System look is taken after the viewer's last look, so a copy that landed in between would
be in the store and not in the viewer's answer — for timing, not access. So the viewer is asked
once more, after the System read: a copy they can see now was late and keeps the retry text;
only one the store holds that the viewer still cannot see is named an access problem. The
mesh-backed case Verify_Live_CopiesTheViewerCannotRead_FailAsAnAccessProblem drives the real
phase over content written as System into a home the probe viewer cannot read. The next occurrence
is the measurement.
🚨 "Holds no grant on" is not enough to make a home unreadable on the gate. The first cut of
that case leaned on the framework being deny-by-default and was red on mw-plugin-test: the gate
seeds Public as ROOT Admin (PluginGateRunner.RootAdminAccess, so every install a gate runs has
an admin to authorize it), and a probe viewer with no grant of its own therefore reads everything
there. The viewer-stamped settle loop saw both copies, the phase failed on the never-written path
alone with the lost-write text, and the red read as "the wrong message" when it was a premise that
had quietly not held. The case now CAPS the home with a PartitionAccessPolicy { Read = false } —
the one instrument that binds every viewer, admins included, while System bypasses row-level
security — and ASSERTS the premise before the verify runs (the System read returns both copies, the
viewer's own read returns nothing), so a harness where either is false fails by name rather than
downstream.
Whether the measurement has been taken — how to read the occurrence record
"The next occurrence is the measurement" is only useful with a way to tell whether one has
happened, and the incident record is easy to misread in four ways. All four were measured on the
control instance, where Admin/_LogIncident lives — not on the portal whose installs fail.
1. The rate of one defect is read from a Shapes entry — never from any node's occurrences.
The fingerprint an issue quotes is usually a REPORTER: it carries occurrences and firstSeen for
the whole ProvisionPlan install family and spawns successors that point back through
reporterFingerprint. 🚨 A successor is not one message shape: it may bucket several
(Variants > 1), recording each in Shapes with its own occurrences/firstSeen/lastSeen. So
there are two ceilings, and both get mistaken for a rate — the reporter's count is a ceiling over the
family, and a successor's is a ceiling over every shape it buckets. Read the matching Shapes
entry. Measured 2026-09-21, the two successors carrying the install-verify wording read 89 and
45 against a headline of "4,293 occurrences since 2026-08-24", and only 3 of the newer one's 10
retained samples are this shape at all — so the headline over-states this defect by more than an
order of magnitude.
2. A reporter's lastSeen FREEZES once it is superseded, so neither "still firing" nor "it
stopped" can be read off it. supersededBy is a moving pointer to the newest spawned shape, not a
verdict on this one: the same reporter named two different successors seven hours apart on
2026-09-21, the later one an unrelated Record entries write timeout.
3. "It has not recurred" needs a liveness control IN THE SAME NAMESPACE, or it is
indistinguishable from silence — the dead-watcher reading. 🚨 The namespace qualifier is the whole
control: LogWatchWorker collects and catches per watched namespace, so incidents can keep arriving
from one namespace while the subject's collector is dead. A control drawn from the wrong namespace
proves nothing and looks exactly like a good one — the first cut of this section cited six
ProvisionPlan folds from the afternoon of 2026-09-21 without noticing that the newest was
the control instance's own namespace, while these failures are the public instance's
(review finding on Plugins#2228).
So the control is content.namespace:<the failing namespace>, newest lastModified: for
the public instance that read 2026-09-21T18:20:46Z, 1 h 38 min before the reading, so that collector was
folding. A truncated result set is fine here and only here, because liveness needs a floor on
recency rather than a maximum.
The absence half needs the opposite and must be complete: the anchored
content.category:ProvisionPlan sweep answered count: 11, truncated: false,
coverage.partitions ["admin"], and none of the eleven carried the new wording or a lastSeen after
2026-09-20T13:58:14Z. Floor for the control, complete set for the absence — swapping them is how
both halves end up proving nothing.
4. The phase being healthy is a separate, POSITIVE control. The same packages that can never
verify for the affected space wrote their manifests for another user — six Store/Install records
under that user's _Install container, 2026-09-12 → 2026-09-16. So this is one space, not the
phase, and a fix aimed at the phase would be aimed at nothing.
🚨 The subject space itself cannot be resolved from outside, and that is now true by
construction. search 'namespace:<space> scope:descendants' answers count: 0 over
coverage.partitions ["<space>"] and get answers Not found — the same string a node you hold no
grant on gets. A credential that demonstrably reads other users' private homes still cannot
separate "torn down" from "denied", and Admin/Partition/<space> is not the tie-breaker: it answers
Not found for a space that certainly exists, making it a blind probe rather than a negative
control. That wall is exactly why the diagnosis had to move INTO the failing verify instead of being
taken from a console.
The measurement came in — and "an ACCESS problem" is several problems (MeshWeaver#5225)
The instrument fired: from 2026-09-22 the affected space's LearningRoadmap and Mail installs fail
every boot with "Install stored … but 'x' cannot read them … an ACCESS problem on that space". So
the store holds the copies and the owner's own read does not return them. What that sentence could
not say is which access predicate hides them, and the candidates have different
owners:
| Cause | What it is | Who fixes it |
|---|---|---|
| A copy's stored MainNode lies outside the space | A viewer-scoped read returns a row when the viewer holds Read at the row's MainNode, not at its path — so no grant on the space reaches the row. A full-instance write cannot move a MainNode back onto its own path (MeshWeaver#2631), which is why every retry leaves it where it is. | The writer that stamped it, plus a heal of the stored rows |
No owner grant in {space}/_Access |
The space was never given to its owner; nothing an install writes can change that. | Whoever provisions the space (onboarding) — and, for the live row, an owner restoring the grant |
A Read cap — a PartitionAccessPolicy with read: false at or above the copy |
Denies Read to every viewer there, the owner included. | Whoever wrote the policy |
| The owner grant is stored and not in effect | None of the above, and a grant naming the viewer with a built-in Read-bearing role is stored — so the permission projection the read consults does not carry it. | The access store's projection |
It is not the id's shape: with row-level security on (the memex wiring), a hyphenated owner reads
what System wrote into their home exactly like a plain one, and a stranger reading the same home sees
nothing — the negative control that proves the boundary was live (OwnerReadsOwnSubtreeTests, all
three green on the partitioned Postgres backend).
So the failed verify now reads what it needs as System — each hidden copy's stored MainNode (from the
diagnosis read it already took), plus the space's AccessAssignments and PartitionAccessPolicys
through the SYNCED read, because the decision is made off their CONTENT and a raw query row does not
carry content reliably (MeshQueries.QuerySnapshot is metadata-only) — and appends the cause that
holds (Localizer.HiddenCauseText). 🚨 It names a cause only on COMPLETE, DECISIVE evidence: a grant
listing that came back at its row cap, or an owner grant through a custom role (whose Read bit lives on
a Role node the check does not read), is reported as "Cause not named …", never guessed (review
findings on Plugins#2358). Each sentence is pinned as a negative control for the others
(HiddenCause_NamesThePredicateThatHidesTheCopy), and the mesh-backed
Verify_Live_CopiesTheViewerCannotRead_FailAsAnAccessProblem now also asserts the real phase names the
Read cap it plants. As before it changes the text, never the verdict, and a cause read that fails or
answers nothing says so instead of taking the access diagnosis down with it.
A standing "no owner grant" verdict is not retried (MeshWeaver#5225)
Naming the cause did not stop the loop. StandardPacks.EnsureForAllUsers runs on every Store
activation, so one space with no owner grant re-copied the same packs and logged the same Error on
every pod roll — 791 lines for ONE user in two weeks, every one a verdict that could not change until
somebody restored the grant.
The sweep now asks, before it re-runs a pack that has no manifest, whether the earlier run's verdict
still stands (StandardPacks.AccessVerdictStands). The evidence is already on the mesh, so nothing new
is recorded: the copy phases land BEFORE the verify and the manifest is written only AFTER it, so an
install target System can read with no manifest means an earlier run stopped at or after the copies.
Whether its verdict still stands is the verify's own predicate, asked again: does the owner's
viewer-scoped read (Localizer.ViewerSubtree, the read the verify takes) return that target? Only
when it does not is the pack skipped, emitting nothing (InstallUnlessAccessVerdictStands) — no copy,
no second report. The run that reached the verdict is the one that reported it.
🚨 It asks the EFFECTIVE read, never _Access itself. A grant can reach the owner through a group
assignment or a publicRead policy with no assignment naming them directly, and a check of _Access
for one would have held back installs the owner could read (review on Plugins#3184).
It is a condition, not a timer: both reads are fresh one-shot listings, so the first pass after the
owner can read the target installs the pack. A first install (nothing stored) is never blocked, and a
guard read that faults or answers nothing decides nothing — the install then runs as before and its
own verify reports what is true. Pinned by the StandardPacksBatchTests "ACCESS verdict" cases.
Not covered: two pods whose sweeps reach the same user before either has stored a copy can each
report the verdict once.
Where it lives
Store/Installer/Source/RefreshPlan.cs (the decision), Localizer.CopyRoot (adds via
CreateNodes, refreshes via sequential CreateOrUpdateNode, the same write ItemRestore.RestoreItem
makes), Localizer.WriteManifest → InstallManifest.WithFingerprints, InstallDialog.DoneCounts,
StoreTexts.Refreshed. Pinned by RefreshPlanTests, the provenance cases in
InstallManifestTests, LocalizerTests.InstallProgress_TheTerminalEvent_CarriesTheRefreshTally and
InstallDialogTests.Progress_CarriesTheRefreshCounts_AndTheDoneLineSaysSo.