Repository navigation
GitSync adopts a stale prebuilt assembly over the source it just synced — silently running last week's code, and it destroyed client data #2813
Description
What happened
A git_hub_sync update pulled new source for Crm/Migration, then adopted a prebuilt assembly instead of compiling what it had just pulled. The node reported success. The next run executed the old code and destroyed live client data.
Sync activity Crm/_Activity/0c389edf, 2026-08-30T17:06:04Z:
Fetching the branch HEAD from GitHub and importing the deltas…
Recompiling 2 NodeType(s): Crm/Migration, Crm/Opportunity
Adopted 2 prebuilt assembly(ies) — the rest compile.
Imported Imported (6 node(s)).
Status Succeeded. Nothing in that log tells an operator that the assembly now serving Crm/Migration was not built from the source in the line above it.
Why it was wrong, provably
The synced commit (904854f) contained three fixes merged ~40 minutes earlier. A migration run at 17:06:37 exhibited all three pre-fix behaviours:
| fixed in the synced source | what the adopted assembly did |
|---|---|
| read the document body shape-tolerantly | stripped the body from 4 promoted documents |
| a bullet whose bold text is a link is not a person | promoted 2 documents to Crm/Contact |
never promote/suggest a node already typed Crm/* |
suggested the client's own Crm/Opportunity |
I then forced a compile via the compile verb. Afterwards compiledSources matched currentSourceVersions and the MVID changed (ed21acce… → 07d5ca73…), confirming the adopted assembly had been built from different source than the node was holding.
The damage
Four client documents lost their entire text — one a 7,000-word commercial offer. Recovered from version history.
One did not recover. ClientA/EslLifeCycleProcess has exactly one version in history — the one the stale code wrote. restore_from_point_in_time returns "No version found at or before…", and content indexing is off on that deployment, so there is no chunk copy either. Its markdown body is permanently gone.
Why this is worse than a normal staleness bug
The operator did everything right: merged a green PR, ran check, ran update, saw Succeeded, saw the NodeType at compilationStatus: Ok with compiledSources == currentSourceVersions. Every signal available said the fix was live. The adopted assembly is what made all of those true while being false in the only way that mattered.
What I think the fix is
- Never adopt a prebuilt for a NodeType whose source just changed in this import. The import knows which sources it wrote; that set should force a local compile.
- If adoption stays, it must be verifiable. Record on the NodeType which commit/source-hash the adopted assembly was built from, and refuse adoption when it does not match
currentSourceVersions. TodaycompiledSourcesis populated in a way that makes an adopted assembly indistinguishable from a compiled one. - Say it in the log.
Adopted 2 prebuilt assembly(ies)should name them and say what they were built from — an operator reading that line cannot currently tell it is a warning.
Point 2 is the important one: this class of bug is invisible precisely because the verification I was taught to do (compiledSources vs currentSourceVersions) reported healthy.
Secondary: the hub did not recover
After the forced compile, Crm/Migration's hub stopped responding — the control-plane watcher no longer picks up an actionable request, and get @{instance}/area/Run times out. recycle on the NodeType did not clear it. Possibly a separate defect around compile-while-hub-live; noting it here because it is the state the forced recompile left behind, and it is currently blocking the migration.
Evidence: Admin/Migration/restructure-all-2 on memex.systemorph.com sits at requestedAction: Run, state: Requested, execute: true and is never picked up.
Activity
rbuergi commentedon Aug 30, 2026
Same defect from the CI end — and it already has a home in #2702
I had an agent chasing what turns out to be this, filed as a read-your-own-write hypothesis. This report corrects it to adoption, which is the more dangerous reading, and I have redirected accordingly.
The CI face
NodeTypeCompileParkTest.ParkedNodeType_CompileToolRetry_AfterFix_SettlesOkAndUnparks(core, shard 1 of run 33322993649, on a documentation-only PR whose diff cannot reach any of this):The test parks a NodeType with broken source, writes valid source and awaits that
Update, then asks the MCPcompiletool for a deliberate retry. It fails 4 seconds afterTEST START— a wrong answer, not a slow one — and its output carries the BROKEN source's diagnostics (CS1040 line 58, fromBrokenConfiguration). The retry did not compile the fixed source; it adopted bytes built from the old one.Three siblings fail with it, all the same "fix source → recompile" shape:
ParkedNodeType_RecycleRetry_AfterFix_SettlesOkAndUnparks,ParkedNodeType_SourceFix_AutoRecompilesAndUnparks_WithoutDeliberateRetry,CodeEditRecompileTest.FailedCompile_DirectCompileToolRetry_RunsFreshCompileAndCreatesRelease.In the wild, on a third surface
MeshWeaver.Reinsurancemain, run 33322605771 (RED at 16:29Z, GREEN at 16:35Z on the same commit):So the same adoption path produces: a false CI red on satellites, a false CI red on core, and permanent data loss on a client portal. That is not three flakes.
Consolidation, not a fourth issue
memexportal logs,Crm/Migration, 17:11Z:[MergeGuard] Crm/Migration: refused stale/reordered cross-hub write to 'compilationStatus'and… to 'lastModified', thenMeshNode Conflict … 2 field(s) changed on the owner since the writer's base and nothing was applied.The requirement that matters most
The reporter's sharpest finding is not the stale bytes — it is that every staleness detector said the fix was live:
compilationStatus: OkandcompiledSources == currentSourceVersions, which is the exact check the Crm operator docs teach people to run after a deploy. The MVID only moved (ed21acce…→07d5ca73…) once a compile was forced.So a fix that merely makes the compile correct is insufficient. An operator must be able to tell an adopted assembly from a compiled one. The proposal in the body — record the commit/source-hash the adopted bytes were built from, and refuse adoption when it disagrees with
currentSourceVersions— is the part that closes the data-loss path, and the MVID shift is the regression test's assertion.Separately: the wedge reported at the end of this issue is NOT compile-while-hub-live
memexportal logs at 17:11:21.408Z:plus, on the other replica,
[UpdateRemote] NO-OP … contentType=JsonElement — lambda returned unchanged; check the lambda's content-type pattern match. The grant is missing, so the control-plane request is never written; recycle tore the hub down anyway; and where a write does reach, the node reads as an untypedJsonElementso the lambda's pattern match silently no-ops (the.As<T>()trap-door, same shape as #2729). Worth not splitting out as its own compile-while-hub-live defect.rbuergi commentedon Aug 30, 2026
Not the same defect as the
NodeTypeCompileParkTestCI red — measured, so the two do not get conflatedNothing here disputes this issue: the sync/adoption defect and its data loss are real and the
proposed fix (never adopt a prebuilt for a NodeType whose source changed in this import; make an
adopted assembly distinguishable from a compiled one) stands.
I was pointed at this issue as the explanation for the four "fix the source, then recompile" tests
that failed in run
33322993649shard 1 (PR #2807). It is not — those are #890, and the logsettles it, so recording it here to stop the two being merged:
ParkTest/*node. A real compile ran.
ParkTest/CompileRetry's output does contain thebroken source's
CS1040 line 58— from the first compile at16:43:45.406, before the testwrote the valid
Configuration. The retry ran at16:43:46.528and the compile tool returnedNullReferenceExceptionfromCci.MetadataWriterpluscanary=BELOW-ROSLYN. Adjacency in thecaptured output made the two look like one event; the timestamps separate them.
16:42:42.375, 7 of 7 compiles in that process that reachedRoslyn's metadata writer failed identically over 6 m 15 s and none succeeded, while compiles that
needed only diagnostics kept returning correct
CS####. Full measurement:CI: one Roslyn Emit poisons the whole process — every later compile NREs in Cci.MetadataWriter.GetConsolidatedTypeParameters, shard ends exit=124 #890 (comment)
Where the two do touch, and it is worth stating because it argues for point 2 of this issue:
both are "the recorded state says the compile succeeded/failed on grounds it never actually
established". #890's half is now fixed in the failure direction — an emit the canary proved the
process could not do is recorded as
Unavailable(not evaluated) instead ofError, so it no longerwrites a verdict nothing formed and no longer stops the automatic re-drive. The success direction —
an adopted assembly reporting
compilationStatus: OkwithcompiledSources == currentSourceVersionswhile being built from different bytes — is exactly the same class of unearned claim, and is this
issue's to fix.
rbuergi commentedon Aug 30, 2026
Diff shape for the adoption fix — posted BEFORE writing it, because it is cross-repo and one premise needs correcting first
1. Confirmed at code level, and the two halves are in the same file
PrebuiltAssemblySeeder.IsAlreadyAdoptedsays, verbatim:…and 100 lines later
SeedsetsRequestedSourceStampAt, whose contract (NodeTypeDefinition.cs)is that the owner then writes
CompiledSources = CurrentSourceVersions.So the seeder defers staleness to the compile watcher, and the same adoption makes the compile
watcher's staleness test true by construction. Nobody compares bytes to source text at any point.
That is why both detectors read clean: they are not broken, they are answering a question the
adoption already answered for them.
The gate is
DeclineReason(frameworkMvid)(framework API-surface hash — deliberately stableacross internal merges) ∧ the per-type dependency record ∧ the
IsAlreadyAdoptedskip. None is afunction of the source text, and I confirmed the bundle carries nothing source-derived either:
PluginBundleClientpasses onlymanifest.FrameworkMvidanda.DependenciesintoSeed.2. So the fix is cross-repo — there is nothing to compare against today
node-repo-publish-bake.ymlin every node repo): record a per-entryfingerprint of the source set the bytes were built from (the source hash, or the commit).
PluginBundleClient→Seed→ onto the node besideRequestedSourceStampAt.Seed— for the exact reasonRequestedSourceStampAtexists (Prebuilt adoption stamps CompiledSources from a field its own activation is still publishing — the adopted build is born dirty and the next release request compiles it anyway #1834): the seeder writes CROSS-HUB and its lambda sees the mirror's snapshot, which
predates the first-activation write of
CurrentSourceVersionsthat its own subscribe triggers.Only the owner holds both authoritatively.
The good news: there is exactly one place to change, and it is pure —
NodeTypeCompilationHelpers.ApplyAdoptedSourceStamp, deliberately shared by all three writers thatcan fulfil the request (the sources watcher's publication, the release-request dispatch, and
InstallAdoptedSourceStampWatcher) "so two of them can never disagree about what adopted andcurrent means". Turning it from assert to check fixes all three at once, and it is
unit-testable with no hub and no timing:
CurrentSourceVersionsCompiledSources = CurrentSourceVersionsIsDirty=false, the lieCompilationStatus = Pendingso the live source compiles locallycompiledSources == currentSourceVersionsstops being something an operator can be taught to trustThe legacy row is the one that needs a scope call, and it is why I am posting rather than pushing:
refusing unproven bundles breaks every bundle published to date and the node-repo CI gates that
depend on prebuilt fetches, so the rollout has to be producer-first, with the consumer tolerant
until the fleet has re-baked. That is a maintainer decision, not mine.
3. Two items from the escalation I could not verify, flagged rather than assumed
the release-request watcher's "satisfied by the existing current build" branch — which is
precisely the branch
ApplyAdoptedSourceStamp's contract exists to satisfy (IsDirtyfalse byconstruction). Worth its own line here, because "there is no operator recovery verb" is a
different severity from "the adoption is wrong".
before a control plane can act on it. Note that the proposed
Pendingflip gives that for freein the disagreement case — the type is not
Ok, so nothing can arm against it.4. 🚨 Correction:
NodeTypeCompileParkTestis NOT the regression for thisI was asked to treat it as the unit-level regression that "already fails without the fix". It is
not, and using it as the proof would produce a guard that checks nothing.
Those tests never adopt anything — there is no adoption line for any
ParkTest/*node anywhere inthe shard log — and their failure in run
33322993649is #890: the retry ran a real compile andRoslyn threw
NullReferenceExceptionfromCci.MetadataWriterwithcanary=BELOW-ROSLYN. Thebroken source's
CS1040 line 58in the same test's output is the first compile at16:43:45.406, 1.1 s before the retry; adjacency in the captured output is what makes them looklike one event. Measurement:
#890 (comment)
Those tests will pass and fail identically with or without the adoption fix. This fix needs its
own regression — the natural one is pure and cheap:
ApplyAdoptedSourceStampgiven a fingerprintthat disagrees with the live snapshot must not stamp, with the MVID shift as the integration-level
assertion that the bytes actually changed.
Related work already landed
#2816 fixes the failure direction of the same "unearned claim" class — an emit the process could
not perform was recorded as
Errorand stamped with a verdict-inputs token, which then stoppedevery automatic retry. Same shape as this issue's point 2 (a record asserting something nothing
established), opposite direction. It does not touch adoption.
rbuergi commentedon Aug 30, 2026
🚨 Retraction: the park-test failures I posted here are NOT this defect
Earlier in this thread I offered
NodeTypeCompileParkTest.ParkedNodeType_CompileToolRetry_AfterFix_SettlesOkAndUnparksand its three siblings as "the CI face" of stale-prebuilt adoption, and the ReinsuranceAocConfigurationred alongside them. That attribution was wrong. A dedicated investigation has measured it, and the correction matters because it would have sent the adoption fix looking for a regression test that cannot detect it.What those five failures actually were: #890 — one Roslyn
Emitpoisoning the whole process. From the job log of run 33322993649, after 16:42:42.375:CS1040,CS0246)Parse and bind stayed healthy; emit was dead, permanently. The
exit=124shard timeout is a consequence, not a second problem.Both hypotheses I put forward are dead, with proof:
CS1040 line 58I read as "the retry compiled the old source" came from the first compile at16:43:45.406, before the test wrote the validConfiguration. The retry ran at16:43:46.528and returnedNullReferenceExceptionwithcanary=BELOW-ROSLYN. Adjacency in the captured output made two events look like one — and I built an argument on that adjacency.ReleaseRequestWatcherHighWaterTestwas a timing casualty of the shard cap — no. Its compile threw 174 ms afterTEST START. The 50-second wait was waiting for anOkthe process could no longer produce. fix: four silent defects from the issue sweep (#2729, #2731, #2793, #2797) #2800's re-split does not touch it.So: I was wrong twice here, and the peer heuristic I corrected earlier ("all five are cap casualties") was also wrong, in the other direction. All five are one event, and that event is #890.
What this does and does not change for THIS issue
Unchanged: the adoption defect is real, confirmed in code, and caused the data loss. The mechanism stands — adoption asserts rather than checks, the gate keys on identity + node version, neither of which is a function of the source text, and the owner then stamps
CompiledSources = CurrentSourceVersions.Changed, and important for whoever writes the fix:
NodeTypeCompileParkTestnever adopts anything and would pass or fail identically with or without an adoption fix. Using it as the regression proof would ship a guard that checks nothing — the exact failure mode this repo keeps paying for. The adoption fix needs a test that actually exercises adoption.Better news from the same investigation:
ApplyAdoptedSourceStampis one pure function shared by all three writers, so changing it from assert to check fixes all three call sites at once and is unit-testable directly — a far better home for the regression than any park test.rbuergi commentedon Aug 30, 2026
Refined diff shape (three-way), plus two findings that change it — still not pushed
Working from the scope call: don't refuse legacy bundles, mark them; refuse only a stamp that
demonstrably disagrees; the field must be readable on the node, not in a log. Agreed on all three.
Two things surfaced while shaping it that need a decision before code.
🚨 Finding 1 — the table and constraint 1 contradict each other on the legacy row
The table says legacy ⇒ adopt, record
Unverified. Constraint 1 says stop stampingCompiledSources = CurrentSourceVersionson an unverified adoption. Applied to the legacy rowthose are not the same instruction, and the difference is a fleet outage:
IsDirtyis exactlyCompiledSources != CurrentSourceVersions. Not stamping ⇒ every legacy-bundletype is
IsDirtyon arrival ⇒ the release watcher's "satisfied by the existing current build"branch (which requires
!IsDirty) stops absorbing ⇒ every install recompiles every type — the43 activations / 13.5 s of boot the prebuilt lane exists to remove. And on a
Modules:RequirePrebuiltmesh it is worse than slow: a local compile is refused by design (
PrebuiltRequiredException), sonot stamping would park every legacy-bundle type. That is the outage refusal was rejected for,
arriving through a different door.
So constraint 1 is right for the mismatch row and unsafe for the legacy row. My proposal:
CompiledSourcesCompilationStatusBuildProvenanceOkAdoptedVerifiedPending→ real local compileAdoptedRefusedOkAdoptedUnverifiedThe legacy row keeps the staleness answer it has today but stops the provenance claim being
invisible — which is the load-bearing requirement as you stated it (visibility, not refusal). If you
want the stricter reading instead, it has to be producer-first with a fleet re-bake before the
consumer tightens, and that is a bigger programme than this fix.
On the middle row: "refuse" means fall back to a local compile, which is the existing decline path
(
Seedemittingfalsealready means "compile normally"). On a require-prebuilt mesh that fallbackis forbidden, so a stale bundle parks the type. That is the correct trade — a parked type beats
silently running last week's code over live client data — but it is loud and should be stated in the
PR, not discovered.
🚨 Finding 2 — "force-release is a no-op under a sealed bundle" is not what the code does, and #2813 itself contradicts it
I checked both absorb branches in
InstallReleaseRequestWatcher. Both explicitly guard on force:A forced request therefore falls through to the COMMIT path and flips
CompilationStatus = Pending,which dispatches a real compile. And this issue's own report says the same thing happened in
practice: "I then forced a compile via the
compileverb. AfterwardscompiledSourcesmatchedcurrentSourceVersionsand the MVID changed (ed21acce…→07d5ca73…)".So there is a recovery verb, and it worked on the affected node. If a peer session observed force
being inert somewhere else, the code says the cause is not the release watcher. The candidate that
does explain it is
Modules:RequirePrebuilt: on such a mesh the forced compile is refused and thetype parks, so force looks inert. That is a genuinely missing recovery verb, but only on
require-prebuilt meshes, and it is a different defect from this one. I did not want to write "no
recovery verb exists" into a PR on a claim the code contradicts — if the observation is real, please
point me at the deployment and node and I will pin it as its own issue rather than fold it in here.
Mechanics, once the rows are settled
node-repo-publish-bake.ymlin every node repo): a per-entry source fingerprint.PluginBundleClient→PrebuiltAssemblySeeder.Seed(…, sourceFingerprint)→ writtenonto the node beside
RequestedSourceStampAt. TodaySeedreceives onlymanifest.FrameworkMvidand
a.Dependencies— nothing source-derived — so this is the cross-repo half.Seed— the Prebuilt adoption stamps CompiledSources from a field its own activation is still publishing — the adopted build is born dirty and the next release request compiles it anyway #1834 reason: the seeder writes CROSS-HUB andits lambda sees the MIRROR's snapshot, which predates the first-activation write of
CurrentSourceVersionsthat its own subscribe triggers. Only the owner holds both authoritatively.NodeTypeCompilationHelpers.ApplyAdoptedSourceStamp— pure, anddeliberately shared by all three writers that can fulfil the request (sources-watcher publication,
release-request dispatch,
InstallAdoptedSourceStampWatcher) "so two of them can never disagreeabout what adopted and current means". Assert → check fixes all three at once.
adoptedSourceFingerprintandbuildProvenancemust be addedto
NodeTypeOperationalContent's list (stripped on export, preserved from the live node on import,ignored by change detection) — otherwise an authored value would forge a provenance claim, which is
the same class of bug as the one being fixed.
Regression — on the pure function, all three rows mutation-proved
ApplyAdoptedSourceStampgiven (a) a matching fingerprint stamps and recordsAdoptedVerified;(b) a disagreeing fingerprint does not stamp and flips
Pending— the data-loss row, and theone whose mutation must fail loudest; (c) an absent fingerprint stamps but records
AdoptedUnverified, neverVerified. Plus the reporter-side assertion the incident actually needed:a node reading
Ok+compiledSources == currentSourceVersionsmust not be indistinguishablefrom a compiled one —
buildProvenanceis what makes that stateable.🚨 To be explicit, since it caused a wrong attribution earlier:
NodeTypeCompileParkTestis notthis fix's regression and must not be cited as one. It never adopts; its failures in run
33322993649are #890.Ready to implement on a word from the maintainer on Finding 1's row and on whether Finding 2's
observation is real.
rbuergi commentedon Aug 30, 2026
🚨 Retraction #2: "force-release is a no-op under a sealed bundle" — the code and this issue both contradict it
I relayed that claim from another session and amplified it into "operators have no recovery verb at all". Both halves are wrong, and this issue's own report is the refutation.
InstallReleaseRequestWatcher's both absorb branches explicitly guard!def.RequestedReleaseForce, so a forced request falls through to the COMMIT path and dispatches a real compile.ed21acce…→07d5ca73…)". The recovery verb exists and worked on the affected node.I passed a peer's observation on as established fact because it fitted the shape I already had. That is the second retraction I owe this thread and the third unverified claim I have relayed today; recording it here rather than quietly deleting it, because a data-loss issue is the worst place for a confident wrong statement to sit.
If that behaviour was genuinely observed, the one configuration that would produce it is
Modules:RequirePrebuilt, where the forced compile is refused by design and the type parks — which looks identical to "force did nothing" from outside. That is a real gap, but a different defect on require-prebuilt meshes only, and it belongs in its own issue with the deployment and node named. I have asked for those.Correction to my own proposed fix, from the same review
My earlier scope note said an unverified adoption must stop stamping
CompiledSources = CurrentSourceVersions. That would cause the outage it was meant to prevent.IsDirtyisCompiledSources != CurrentSourceVersions, so withholding the stamp makes every legacy-bundle type dirty on arrival, the!IsDirtyabsorb branch stops firing, and every install recompiles everything — and on aModules:RequirePrebuiltmesh, where a local compile is refused by design, it parks every legacy-bundle type.The corrected shape, which keeps the load-bearing requirement (visibility, not refusal) without that blast radius:
currentSourceVersionsBuildProvenance = VerifiedBuildProvenance = AdoptedUnverifiedAn operator can then tell an adopted assembly from a compiled one, and a verified adoption from an unverified one, before a control plane acts on it — which is what turns "adoption plus an armed control-plane node is a loaded gun" back into something disarmable on sight. Withholding the stamp was never necessary for that; a provenance field is.
Regression home:
ApplyAdoptedSourceStamp— one pure function shared by all three writers, unit-testable without a mesh. All three rows mutation-proved, especially the middle one.rbuergi commentedon Aug 30, 2026
🚨 Retracting my Finding 2 — the peer session was right, I was wrong, and the mechanism is worse than either of us had
I wrote that "force-release is a no-op under a sealed bundle" was contradicted by the code, because
both absorb branches in
InstallReleaseRequestWatcherguard on!def.RequestedReleaseForce. Thatpart is true and it is also irrelevant, because I stopped looking one layer too early.
RequestedReleaseForceis honoured by the release watcher — and then silently discarded by thecompile watcher. On a
Pendingflip, before any Roslyn pass:RequestedReleaseForcedoes not appear anywhere in that region. So a forced request re-adopts thesame sealed bytes and returns: same assembly, same MVID,
Ok, request recorded as handled.DispatchOrPark()— the only route to Roslyn — is reached only when adoption misses.That is exactly the reported symptom, and the reporter's wording was exact: "under a sealed
bundle" is the resolving case. This issue's own "I forced a compile and the MVID changed" is the
missing case. Both observations are correct; the discriminator is whether
SeedForTypesresolvesan artifact at that moment. I had two true reports and treated the one that did not fit as an error —
which is the same failure I flagged in the attribution above, committed by me.
Filed as #2818 with the mechanism, the proposed fix (an explicit force must suppress on-demand
re-adoption), and the require-prebuilt consequence.
This matters for the shape below: the three-way's middle row refuses a stale adoption and drives a
local compile. Until #2818 lands, an operator who sees
AdoptedUnverifiedon a node still has noverb that rebuilds it while its bundle resolves. So #2818 is not a nice-to-have beside this fix — it
is the recovery path this fix's diagnosis points at, and the two should land together or in that
order.
My earlier comment asking whether the observation was real is withdrawn; no one needs to hunt for a
deployment and node.
rbuergi commentedon Aug 30, 2026
The diff, as agreed — posted before pushing so the Crm session sees it first
Branch
fix/2813-adoption-provenance, cut frommainat2160d6525. Not pushed.The three-way, exactly as scoped
One pure function, three writers, all fixed at once.
BuildProvenance.Compiledis the zero value, soevery record written before this field existed reads as the honest default.
Three design points that were not obvious, and where they landed
1. The fingerprint must be CONTENT, not versions.
CurrentSourceVersionsis{path → LastModified.UtcTicks}— mesh-local mtimes the producer cannot know and does not have (thebake writes zeros). A fingerprint over ticks would never match and every adoption would be refused.
So both sides use
PartitionSourceFingerprint— content-based, order-independent, already inMesh.Contract, already the static-repo importer's change detector.2. The comparison cannot happen in
Seed, for the same #1834 reason the stamp request exists:the seeder writes CROSS-HUB and its lambda sees the MIRROR's snapshot, which predates the
first-activation write its own subscribe triggers. So the owner publishes its own
CurrentSourceFingerprintin the same update asCurrentSourceVersions, andApplyAdoptedSourceStampcompares two strings already on the node — staying pure, no new I/O at anyof the three writers.
3. The live fingerprint is computed only when it can be USED — when the node carries an adopted
fingerprint or a pending stamp request:
Otherwise every source emission for every NodeType would pay a SHA-256 over the whole serialised
source set on a path that today reads only timestamps, for an answer nothing consumes. A
locally-compiled type pays nothing.
Two things the repo's own guards caught, worth recording
NodeTypeCompileStatehad to carry all three fields.StateMembers_AreExactlyTheMaskedOperationalMembersfailed with "'adoptedSourceFingerprint' is masked by the sync seams but missing from
NodeTypeCompileState — the satellite would silently drop it". Exactly the paired-contract guard
this repo builds, doing its job on a field I had added to one half.
Seedis public surface, so this is an OVERLOAD, not a signature change — adding an eighthoptional parameter is a binary break the compatibility gate is right to refuse. The new parameter
carries no default, so existing seven-argument calls still bind unambiguously. Fallout: five (then
six) bare
<see cref="…Seed"/>became CS0419-ambiguous; all now carry explicit signatures, and arepo-wide grep confirms none is left.
Tests — all three rows mutation-proved
AdoptedBuildProvenanceTest, 6 cases. Reverting the three-way to the old unconditional stamp fails5 of 6, and the messages name each row:
The one that still passes is the enum-default case, which the three-way does not decide — correct.
Two of the six are not row assertions and are the ones I would keep if the design changes:
IsDirty == false— i.e. it pins that the stamp is KEPT. If someonelater "tightens" this to withhold it, that test fails with the reason written out: every install
recompiles everything, and on a
RequirePrebuiltmesh every legacy-bundle type parks.AVerifiedAndAnUnverifiedAdoption_AgreeOnEveryOldSignal_AndDifferOnlyInProvenance— thereporter-side assertion the incident actually needed. Two nodes identical on
CompilationStatus,IsDirtyandCompiledSourcesmust still be distinguishable. If provenance ever stops carryingthat, this is what says so.
What this does NOT do — named, not buried
🚨 Every adoption reads
AdoptedUnverifieduntil the bake stamps fingerprints. The refusal rowis armed and correct but cannot fire yet. Nothing writes
sourceFingerprintinto a bundle today,so every adoption genuinely is unverifiable and a field saying so is TRUE — a uniform value here
is the honest state, not a bug, and nobody should later read it as one. The producer half is
node-repo-publish-bake.ymlplus wherever the manifest'sFrameworkMvidis written; the day abundle carries a fingerprint the refusal row starts discriminating with no further change.
Consumer-first is safe precisely because legacy = adopt+mark.
No recovery verb while a bundle resolves — that is Force-release is silently discarded: the compile watcher re-adopts the prebuilt bundle instead of compiling, so there is no verb that forces a build of the live source #2818, found while shaping this:
RequestedReleaseForceis honoured by the release watcher and then silently discarded by thecompile watcher, which re-adopts the same sealed bundle and settles "without a Roslyn pass".
An operator who sees
AdoptedUnverifiedcan see the problem and still cannot force a rebuild —which is exactly where the Crm session is standing.
I am NOT folding it into this change set, deliberately. The fix is ~10 lines (skip the
on-demand adoption branch when the pending definition carries the force flag, go straight to
DispatchOrPark()). Its regression is not: nothing intest/registers anIPrebuiltAssemblyConsumertoday, so proving "a forced trigger reaches Roslyn while an unforcedone still settles by adoption" means standing one up on a Monolith mesh with real bundle bytes.
Shipping a behaviour change to the compile watcher's hot path without that test is the exact move
this repo has spent the evening catching. So: provenance lands first, the recovery verb is Force-release is silently discarded: the compile watcher re-adopts the prebuilt bundle instead of compiling, so there is no verb that forces a build of the live source #2818
and it is still open, and I would rather it got a real test than a ride-along.
A dependency worth stating: the fingerprint's meaning rests on
NodeSources.GetSourcesnotprojecting away
content— which that helper already documents as load-bearing ("a projectionomitting
contentwould make the adapter emit NULL::jsonb … and the type would compile againstempty sources with no error anywhere"). If that ever regresses, both sides would hash empty
content and match. It would break compilation far more loudly first, but it is a real coupling.
Verification
Say the word and I will push it; I would rather #2818 went first, and I would like the Crm session to
say whether
AdoptedUnverifiedon their nodes is enough to unblock the hold or whether they need theproducer half too.
🚨 One residual I did NOT decide unilaterally — please rule on it
On the refusal row I return
def with { … }, notstamped— soCompiledSourcesis untouchedand the staleness question stays open. But
Seedhas already run by then, and it stamps the adoptedbuild's assembly coordinates (
LatestAssemblyCollection/LatestAssemblyPath/LastCompiledVersion/
LatestAssemblyMvid). Those I leave alone.So between the refusal and the fresh compile completing, the rejected bytes are still what serves.
That is strictly better than today — today they serve forever with no signal, and now the node says
AdoptionRefusedand a compile is running — but it is not nothing.Clearing the coordinates on refusal would stop the stale assembly serving immediately. I did not do
it, because the consequence is availability, not correctness:
Modules:RequirePrebuiltmesh"Stop serving code we have proven is stale" is a defensible reading and may well be the right one for
a data-integrity fix — but on a require-prebuilt mesh it converts a marked-stale type into a dead one,
and that is a product call about which failure a client should get. Given #2813 is a data-loss issue I
lean toward clearing, but I am not making that trade on my own.
rbuergi commentedon Aug 30, 2026
PR is up — #2821, CI running
Branch
fix/2813-adoption-provenance, basemain. It came upDIRTYon open (#2816 had landed on the same two files); both sides were pure additions at the same anchor, so the resolution keeps both — verified byEmitPipeline.csandEmitDeadProcessIsNotAVerdictTest.csshowing zero diff against main, and by Graph.Test going 1583 → 1589 after the merge, which is #2816's six tests arriving intact.Post-merge, locally: 5 projects clean at
-c Release -warnaserror; Graph.Test 1589 passed, PluginCatalog.Test 698 passed, Documentation.Test 121 passed.Holding it for the relay — the reviewing session asked to read the legacy path first, which is
ALegacyBundle_StillAdoptsAndStamps_ButIsNeverRecordedAsVerifiedplus theif (def.AdoptedSourceFingerprint is not { Length: > 0 })branch inApplyAdoptedSourceStamp. That test assertsIsDirty == falseon purpose: it is the tripwire for anyone later "tightening" the legacy row, and it fails with the parking consequence written out in the message.The two things I most want challenged there:
IsDirty == false— i.e. this change does not make unverified adoptions re-compile. It only makes them say they were never checked. If the hold on the Crm space needs them to actually rebuild, this change alone does not do that and Force-release is silently discarded: the compile watcher re-adopts the prebuilt bundle instead of compiling, so there is no verb that forces a build of the live source #2818 is the blocker, not this.buildProvenancefield readingAdoptedUnverifiedeverywhere an assembly was adopted.fix(#2813): a refused adoption CLEARS CompiledSources — and the test …
rbuergi commentedon Aug 30, 2026
The recovery verb is up: #2824 (fixes #2818) — a FORCED release now skips on-demand prebuilt adoption in the compile watcher and compiles the live source, and the force is spent by that compile. The provenance half (verify/refuse an adoption against the source) stays with #2821. Found alongside in #2824: a type that already had a build could be stranded at Pending by an adoption reported but never written back — the landed-predicate now requires the Ok stamp a real Seed writes.
rbuergi commentedon Aug 30, 2026
Reopening — #2821 merging did not fix the defect in this issue's title
#2821 (
a6c5e87aa) closed this automatically via its closing keyword. That is wrong, and the PR itself says why in its own body: it changes what an adopted build says, not what it does.This issue's headline is "GitSync adopts a stale prebuilt assembly over the source it just synced — silently running last week's code." Check it against what actually shipped:
compiledSources == currentSourceVersions?RequirePrebuiltmesh)BuildProvenance = AdoptedUnverifiedOnly the last row changed. And because nothing writes a
sourceFingerprintinto a bundle yet, every adoption in the fleet readsAdoptedUnverified— the refusal row is armed and idle. That is the honest state and a real improvement, but it is visibility, not prevention.What still has to land before this issue is genuinely closed:
RequestedReleaseForceand re-adopts without a Roslyn pass, so there is still no operator verb that forces a build of the live source on any mesh where the bundle resolves. That is the recovery path this incident needed and did not have.AdoptedUnverifiedtype, aCrm/Migration-shaped node still fires against exactly the assembly nobody verified.Reopening rather than leaving a closed issue that reads as "the data loss is handled". The document that was permanently lost is still the measure of this one, and nothing shipped today would have prevented it — it would only have made it visible to an operator who thought to look.
ci(bake): a FALSE REFUSAL is red on the PR lane, not invisible (#2813)
review(#4641): seven corrections — a restart is not the gap, and a FO…
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-08-30)
Two corrections and one hazard, from the reporter's follow-up probe
1. The hub is UP. The NODETYPE is not resolving.
A read (rather than another write) settles it —
get @Admin/Migration/restructure-all-2/area/Runreturns a rendered response, not a timeout:That list is only the generic node areas — no
Run, noTests, nothing fromCrm/Migration's ownAddMigrationLayoutAreas()/AddMigrationControlPlane(). The instance activates and serves; it just does not know it is aCrm/Migration. The type's configuration is never applied, so no watcher exists to pick up an actionable request.This is the same fact as the portal-log lines from the other end —
[UpdateRemote] NO-OP … contentType=JsonElement — lambda returned unchangedandEnrichWithNodeType slow path for 'Crm/Migration' faulted (TimeoutException). The type never registers, so every instance falls back to generic behaviour and every typedUpdatelambda misses its pattern match.Stating it as "the hub is down" would send the next reader after grain lifetime, and the hub is demonstrably alive. Correcting my earlier comment on this issue accordingly.
2. Adoption + an armed control-plane node is a loaded gun
The reporter's queued run was sitting at
requestedAction: Run, execute: true. If the type re-registers against the stale adopted assembly, that fires the pre-fix code unattended — stripping document bodies again, with nobody watching. They have setrequestedAction: Noneand written the reason into the node's description so it cannot fire and whoever finds it knows why.This is the fix requirement arriving from a second direction: the window between "the type re-registers" and "the operator notices which assembly it got" is unattended execution of whichever bytes won, and the operator has no signal that distinguishes them. Any fix must make the adopted-vs-compiled distinction visible before a control plane can act on it — not only auditable afterwards.
3. Why forcing a sync is not the recovery
A
git_hub_sync updateruns assystem-securityand would re-register the type — but it can re-adopt the same stale prebuilt, and with a control-plane node in the space that is exactly the combination above. So the obvious recovery is the dangerous one. The space is being held deliberately.Also worth its own line
The permanently-lost document (
ClientA/EslLifeCycleProcess) had one version in history and content indexing off, so there was no second copy anywhere. That is a deployment setting, not a code path — a portal holding client documents with neither version depth nor an index has nothing to restore from. It is the difference between what was recovered here and what was not.rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-08-30)
Resolving the open refusal-row question — and the answer moves the interlock to EXECUTE time
From the session that owns this incident, and it reframes the question I posed:
That is right, and it demotes the clear-or-keep question I had been treating as central. The first-order requirement is that
BuildProvenancebe readable by a control plane, so a watcher can refuse to act on an unverified type. ACrm/Migration-shaped node that fires on type registration would otherwise still fire — against exactly the assembly the loader refused to verify. That interlock, not any load-time decision, is what would have saved the four documents.The load-time row still resolves as a conditional, now as the second line of defence:
RequirePrebuilt)Critical, and mark the type visibly unverified — clearing there trades a silent wrong answer for a certain outage, and an outage on a client's production mesh is not obviously the kinder failureMaking it conditional keeps the dangerous branch explicit rather than resting on an assumption about how a flag is set. 🚨 And it must not rest on "
RequirePrebuiltis set nowhere" — I measured it absent from pod env, deployment inline env and ConfigMap on memex and memex-cloud, which is two instances and says nothing aboutclient-b,client-i, local installs, or any external instance the registry serves.A neighbouring signal that is NOISE — do not build on it
The same session has retracted (Crm#33) a staleness check they had shipped into their own AGENTS.md: "the Release is what instances load, so a mismatch means the portal is not running your change." Falsified by their next sync —
Crm/Offercame back with all three fields mismatched:…and served perfectly: title, DRAFT pill, all 7,111 chars.
latestReleasePathlags routinely — all fifteen of their types have stale release pointers and all fifteen are healthy.So release-vs-NodeType disagreement is not what distinguished the broken type from the healthy ones, and a check of that shape would fire constantly on healthy meshes. The source-fingerprint-on-the-bundle comparison this fix uses is the right signal precisely because it does not have that property. Recording it here so the next reader does not reach for the cheaper-looking neighbour.
Sequencing
The incident owner is not waiting on this fix — their migration is complete, done by script, with every write verified by read-back — and they have said
AdoptedUnverifiedeverywhere is the honest state and preferable to a field that pretends. So the provenance change lands on its own merits, with #2818 (no operator verb forces a build of the live source) and the execute-time interlock both named as open follow-ups rather than folded in untested.