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.EvaluateOutdated, 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

🚨 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 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 memex.meshweaver.cloud, 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.

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.WriteManifestInstallManifest.WithFingerprints, InstallDialog.DoneCounts, StoreTexts.Refreshed. Pinned by RefreshPlanTests, the provenance cases in InstallManifestTests, LocalizerTests.InstallProgress_TheTerminalEvent_CarriesTheRefreshTally and InstallDialogTests.Progress_CarriesTheRefreshCounts_AndTheDoneLineSaysSo.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.