A Content Verdict Is Per Node

A static-repo import writes each source node through the canonical upsert, and a node whose write the owner refuses is isolated: the failure is logged against that file, every other node still lands, and the pass reports how many did not.

Some of those refusals are about the moment — a store briefly unreachable, an owner that did not answer — and some are about the bytes: a validator rule, an invalid path, a NodeType the mesh does not know, an RLS denial. The second kind is deterministic. Re-reading the same bytes at the same fingerprint re-derives the identical refusal, so re-issuing that write can accomplish nothing, and doing it anyway cost memex-cloud 19 full import passes in three hours — roughly 425 identical failing upserts plus a NodeType compile each, on a portal already at 8/8 replicas. That is issue #3146, and its rule is right.

What was wrong is where the rule was recorded.

The shape of the defect

The refusal was recorded on the content-addressed marker — the activity at {Partition}/_Activity/import-{fingerprint} whose whole purpose is to say something about the partition. A pass whose every failure was a content verdict stamped that marker Failed with the outcome ImportedWithContentErrors, and the next import read it and answered Skipped for the whole partition without reading anything.

A content verdict, though, is earned by one node. Forty files can land and one be refused, and the pass still earns that word — it means "every failure was deterministic", not "everything failed". Recording a per-node fact as a per-partition verdict is the entire defect:

The marker said "an earlier FULL import already recorded this exact content" about content the import demonstrably lost.

What it cost

Measured on memex.systemorph.com, 2026-09-15.

One file — Hosting/Deployment/Source/SelfUpdateRouting.cs — contained a literal NUL byte (0x00) inside a char literal. PostgreSQL cannot store a NUL in a text column, so the write of that one row was refused while the rest of the Hosting tree imported normally. The pass reported:

Re-imported ImportedWithErrors (41 node(s)) at 85515f62.

— a count, no paths, at Information, on an activity whose terminal status was Succeeded. Then:

Imported Skipped — an earlier FULL import already recorded this exact content at
fingerprint bb9801101e859a21 (Hosting/_Activity/import-bb9801101e859a21), so the
partition was not re-read.

The visible symptom was a compile error on a symbol whose source file is plainly in git, with nothing anywhere naming the node that had not landed. The issues are #4459 and #4456; #4456 reads the same evidence as "applies modifications but not additions", which is what one dropped file looks like from outside when nothing names it.

The fix: the memory moves to the node

The per-node import manifest ({Partition}/_Activity/import-manifest, a {path → token} map) already exists and is already the thing that decides, per node, whether a write is worth issuing. Two changes make it carry the refusal too.

1. A run may only claim what it WROTE. The manifest used to record a node's source token even when its write had been refused — the ledger asserting the partition held content that is provably not in it. It is the same false claim the marker made, one layer down, and it had to become honest first. Now:

what happened to the node what the manifest records
written its source token
refused, deterministically ! + its source token — evaluated, and these bytes were refused
failed, retryably nothing at all — which is what makes the next pass look again

The ! prefix is a prefix rather than a schema change on purpose: a reader that does not know it compares !abc against the token it computes (abc), sees a mismatch, and re-evaluates the node — the safe direction, and exactly the behaviour before the field existed.

2. The skip moves with it. The import loop skips a node whose manifest entry is a refusal of this exact token — reported, with no write attempted — and ImportedWithContentErrors stops being a fingerprint-recording outcome. The marker keeps it as a record of what the pass found; it is no longer a licence to answer Skipped for the partition without reading it.

Both properties then hold at once:

A Force or Reconcile import bypasses the refusal memory, exactly as it bypasses the marker: re-applying regardless is what those are for.

What it costs

A partition holding a refused node now re-evaluates on every trigger instead of short-circuiting. That pass issues no node writes and no recompiles — every landed node is manifest-skipped, the refused one is refusal-skipped — so what it costs is one subtree read, the prune evaluation and two bookkeeping writes. That is the same cost the marker already accepts whenever a verdict is absent or records a non-converged run; see The Import Marker Records Convergence.

An import that loses a node has to NAME it

The second half of both issues is reporting, and it is the half that decides how long an outage lasts. Every other thing an import can leave behind already named its paths — written, pruned, blocked creates, held NodeTypes, refused content. A failure was the one category reported as a bare number.

What this does not cover

An operator who starts from the COMPILE ERROR was the third item on both issues and is deliberately not fixed on this page. It is #4469, and it is now done — see A Parked Type Names the Import.

A partition left incomplete still hands the compiler source files that reference a symbol whose own file did not land, so the NodeType fails on CS0246 / CS0103 for a symbol that is plainly in git. What this page changed is the other direction: an operator reading the sync activity learns which file is missing and why, and the activity is no longer green. Because the facts were all recorded here — the manifest's !<token>|<reason> entries say which declared nodes the partition is missing, and why — #4469 was a wiring problem rather than a measurement one, and the compile failure now leads with the refusal that explains it.

Note the granularity trap #4469 had to avoid: accusing (or refusing to compile) a whole partition because one unrelated node was refused would re-create exactly the over-broad reading this page is about, so the join is scoped to the types that actually reference the missing node.

🚨 A manifest written BEFORE this change still carries the old lie, for one case. Entries predate the ! sigil, so a node whose update was refused back then sits under its plain token while the mesh holds the older content; the incremental skip matches that token and passes over it. That is not new — it is precisely the behaviour this change removes going forward — and it is self-limiting in the same way the marker's absent-verdict residue is: any edit to that file moves its token and the node is evaluated again. It is named here rather than migrated because a migration would have to decide, for every pre-existing entry, a fact no pre-existing entry records. The create case needs nothing: a refused create leaves no node, and the incremental skip already requires the node to be present.

What this does not change

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.