Repository navigation
A deploy makes custom-NodeType per-node hubs answer Not found instead of waiting — in-flight writes are silently discarded #667
Description
Repo: Systemorph/MeshWeaver (per-node hub activation / NodeType assembly reload)
Environment: https://memex.meshweaver.cloud, observed 2026-07-27 07:40–08:20 UTC
Severity: every rolling deploy is a window in which reads lie and in-flight writes are lost
Summary
While a NodeType's assembly is being reloaded — which happens on every deploy, because each NodeType recompiles against the new framework build the first time it is touched — reads of nodes of that type return Not found rather than waiting for activation or surfacing the activation failure. A transient recycle is therefore indistinguishable from genuine absence.
That false negative is not cosmetic. Existence checks built on it take the wrong branch, and any write that takes the CreateOrUpdateNode update path fails with no initial state arrived … within 30s, discarding the caller's whole operation. A user lost a course install to exactly this on 2026-07-27.
Timeline (all UTC, 2026-07-27)
| Time | Event |
|---|---|
| 07:42:29 | Continuous Delivery (main) run starts |
| 07:43:56 | User redeems coupon FREEAGENTS, entitlement written |
| 07:44:20–07:44:28 | Store/Plugin install copies 93 nodes into sglauser/AgenticEngineering/** |
| 07:44:40, 07:44:42, 07:46:20 | Three further CD runs start and are cancelled (overlapping pushes) |
| ~07:44–07:49 | Install faults: no initial state arrived for '…/Exercise/CharacterizeThenTame' within 30s — the whole install is discarded, no manifest written |
| 07:49:00 | Workspace root sglauser/AgenticEngineering created (second attempt) |
| 07:53:12 | Store/Install last successful compile, framework 1e69fc09cc8a4daf89b25729bcf52a45 |
| 07:58:08 | CD run completes successfully — new image rolls out |
| 08:01:06 | Store/Plugin compiles, framework 26336dc0eb984541b16252988e09c1e6 |
| 08:14:19 | Edu/CourseInvite enters Compiling |
| 08:16:23 | Edu/Exercise recompiles |
| 08:1x | get @Edu/Exercise returns the node, then returns Not found; @AgenticEngineering/MyExercises returns Not found; area renders on the AgenticEngineering hub time out |
The user's install ran inside the deploy window. Everything below is a consequence of nodes being read while their NodeType assembly was being replaced.
Evidence — all measured directly
- A read flipped to
Not foundacross a recompile.get @Edu/Exercisereturned the full node; the identical path with the identical identity returnedNot foundroughly two minutes later, spanning the 08:16:23 recompile. Nothing was deleted in between. - Custom-NodeType nodes ghost; core-type nodes do not.
get @AgenticEngineering/MyExercises(nodeType: Edu/CourseInvite) →Not found, while the same node is returned by theAgenticEngineering/*child listing, and every siblingMarkdownnode in the same partition under the same gating reads fine. - Area renders time out.
get @AgenticEngineering/area/Subscribe→Delivery to 'AgenticEngineering' failed: No response received in hub cache/vqYmlVnVS0KqrWZufo2T_A within 00:01:00 for request SubscribeRequest → target AgenticEngineering.get @AgenticEngineering/area/Install→ timed out. - The query index and the authoritative read disagree.
sglauser/AgenticEngineeringis present in the index (version 13,lastModified 2026-07-27 07:49:00) butget @sglauser/AgenticEngineeringreturnsNot found. compilationStatusis not reliably terminal.Store/Installsat atCompilingfrom 08:18:49 with its last success at 07:53:12. On a local dev portal,Latex/Documenthas sat atPendingsince 2026-07-21 while serving normally. A caller cannot use this field to tell whether a type is usable.
Why this matters beyond flaky reads
Store/Plugin/Source/Localizer already carries a defensive workaround for exactly this failure mode:
/// The node type of a viewer's localized-course home root — a CORE type (Markdown) on
/// purpose: its hub is always ready, so the one node the viewer navigates to can never ghost.
public const string HomeRootNodeType = "Markdown";Pinning one node to a core type is not a fix — it only protects the node whose type the author controls. The install still walked into the same wall on an Edu/Exercise node it does not own:
Update aborted: no initial state arrived for
'sglauser/AgenticEngineering/Introduction/Exercise/CharacterizeThenTame' within 30s.
and lost the entire run (see the companion Store/Plugin issue). A hub reporting Not found for a node that exists also silently corrupts every existence check built on it — EnsureHomeRoot skipping a create it should have made, MissingAfterInstall mis-verifying, Entitlements.IsEntitled reading a transient false and bouncing a paying user to the paywall.
Ruled out — please do not re-derive this
The differing compiledFrameworkVersion values across NodeTypes look like two portal builds serving at once. They are not. compiledFrameworkVersion records the framework build at the moment each NodeType was last compiled, so a fleet on a single build legitimately shows a spread of values.
Verified on a single-instance local portal (https://memex.localhost:8443), one process, one build:
| NodeType | last compiled | compiledFrameworkVersion |
|---|---|---|
ClaimsDeepfield/Treaty |
2026-07-24 08:34 | 85a498ecb6b54807a8d3391507caecef |
UWDeepfield/Section |
2026-07-24 08:34 | 85a498ecb6b54807a8d3391507caecef |
ClaimsDeepfield/LossEntry |
2026-07-24 12:35 | 85a498ecb6b54807a8d3391507caecef |
Latex/Document |
2026-07-21 06:48 | 06b11840d9dd4ac8a93d4502a505514f |
Same-build compiles share a hash; the type last compiled three days earlier carries the previous one. The prod split (Store/Install 1e69fc09… at 07:53:12 vs Store/Plugin 26336dc0… at 08:01:06) is explained entirely by the CD run that succeeded at 07:58:08 between them. There is no mixed fleet and no recompile ping-pong; recompiling after a deploy is the designed behaviour. The bug is what happens to readers while it does.
Suggested fix
- A per-node hub mid-activation must never resolve a read to
Not found. Reporting absence for a node that exists is what turns a routine deploy into a data bug. The read should await initial state (bounded) or surface the activation failure explicitly — a false negative is the one answer it must not give. - Make assembly reload non-destructive to in-flight callers. A recompile triggered by a deploy should not tear down activations that are servicing requests; drain, or serve the previous assembly until the new one is ready.
- Do not raise the 30 s guard in
CreateOrUpdateNode. The guard is correctly reporting that the hub never came up; increasing it only makes the failure slower and widens the window in which a caller's work is lost. - Consider whether
compilationStatusshould be a reliable readiness signal — todayCompilingandPendingboth persist long after the type is (or is not) usable.
How to reproduce
All steps are read-only.
A. The ghosting (needs a deploy or a forced recompile in flight)
- Pick a node of a custom NodeType, e.g.
AgenticEngineering/MyExercises(Edu/CourseInvite). get @{that path}→ returns the node.- Trigger a recompile of its NodeType (a deploy does this automatically; otherwise force a release on the NodeType node).
- Re-run the identical
getwhilecompilationStatusisCompiling.
Expected: the node, or an explicit activation error.
Actual:Not found.
B. Custom types ghost, core types do not
get @AgenticEngineering/*— the listing includesAgenticEngineering/MyExercises(Edu/CourseInvite).get @AgenticEngineering/MyExercises→Not found.get @AgenticEngineering/Introduction(Markdown, same partition, same gating) → returns fine.
C. Area renders on an affected hub
get @AgenticEngineering/area/Subscribe
Actual:No response received in hub cache/… within 00:01:00 for request SubscribeRequest.get @AgenticEngineering/area/Install→ times out.
D. Index vs authoritative disagreement
search path:{viewer}/AgenticEngineering→ returns the node (version 13).
get @{viewer}/AgenticEngineering→Not found.
E. The downstream data loss
- During the same window, trigger a
CreateOrUpdateNodethat takes the update path against a custom-NodeType node — e.g. re-run a Store/Plugin install (/{plugin}/Install→ Get) over an already-copiedEdu/Exercisesubtree.
Actual:Update aborted: no initial state arrived for '…/Exercise/CharacterizeThenTame' within 30s, and the caller's whole operation is discarded.
F. Confirm the framework-hash spread is a red herring (see "Ruled out")
- On any single-instance portal,
getseveral NodeTypes and comparecompiledFrameworkVersionagainstlastCompileSucceededAt. Types compiled against the same build share a hash; types last compiled before the most recent deploy carry the older one. - Correlate a prod hash change with
gh run list --repo Systemorph/MeshWeaver— a successfulContinuous Delivery (main)run sits between the two compile times.
Activity
sierragolflima commentedon Aug 6, 2026
Update: substantial work has landed, and the deploy window is narrowed but not closed.
TimeoutException(target hub probed) instead of the same null a not-found emits;ReadTimeoutBehavior.Throwis the default (GetMeshNode: a timed-out read is not "node not found" #661).NodeTypeBakeGate/NodeTypeBakeStatus, bake health check), and the proactive sweep is enabled on AKS (PreWarm__DynamicTypes: "true").Still open:
PreWarm__GateReadiness: "false", enabled then reverted 2026-08-02 over Cross-silo posts lose AccessContext: static content 500s on ~50% of requests with 2 replicas #694 cross-silo-timeout residue), so pods still take traffic mid-sweep.NotFoundduring activation still emits null by contract — no await-activation was added for that path.Keeping open for those two.
sierragolflima commentedon Aug 7, 2026
Status update — the read-side (this issue's primary ask) is fixed; re-scoping to the write-side 30 s discard
Reviewed against current
main. This issue's primary ask — "a read mid-activation must never resolve toNot found" — is resolved. Two surfaces now prevent the silent not-found:GetMeshNodedefaults toReadTimeoutBehavior.Throw: a timed-out read raises aTimeoutExceptionnaming the path, elapsed time, and the in-flight snapshot of both the reader's and the owning hub's state — it no longer emits the samenullthat the genuine-absence path returns (callers must opt intoEmitNullfor "indeterminate ⇒ absent"). Landed in GetMeshNode: a timed-out read is not "node not found" #661 ("GetMeshNode: a timed-out read is not 'node not found'") and GetMeshNode timeout: probe the target hub too, not just the reader #663 (probe the target hub too). A real routingNotFoundstill returns null — correct, that is true absence.Pending/Compiling), the instance hub's activation awaits the compile settling — the no-progress wall-clock is disarmed the moment a compile is observed, so the wait is bounded by the compile finishing rather than a timeout. A genuinely stuck type surfaces a typed compile-error overlay plus a self-heal recycle — never a not-found (NodeTypeEnrichmentHelpers.WaitForCompileSettled).(For the record: this issue was filed 2026-07-27 08:51 UTC, after #661/#663 merged — it was the follow-up asking for the deeper root fix, and the read half of that is now in place.)
What remains — this issue now tracks the write-side discard
The in-flight-write-discarded consequence is not fixed — only window-shrunk. The
CreateOrUpdateNodeUPDATE path (HandleCreateOrUpdateNodeRequest→ApplyUpdateViaStream→GetMeshNodeStream(path).Update(...)) still carries the unchanged 30 s guard —"Update aborted: no initial state arrived for '{path}' within 30s"(MeshNodeStreamExtensions.cs). If a target instance hub's NodeType recompile-await exceeds 30 s (a cold-pod recompile of many types legitimately can — the pre-warm budget is up to ~5 min per type), the write is still discarded — exactly the "lost course install" reported here.The startup
DynamicTypePreWarmerfront-loads recompiles but deliberately does not gate the readiness probe, so the pod can serve before the bake completes — the recompile-during-serve window is shrunk, not eliminated. There is no drain, serve-previous-assembly, or retry-across-recompile on the write path. (#854 is a per-caller workaround for the same family, confirming the systemic discard is still live.)Re-scoping this issue to that remaining defect: a
CreateOrUpdateNodeUPDATE whose target hub is mid-NodeType-recompile must not silently discard the caller's write at 30 s — it should await the activation (bounded by the compile, as the read side now does) or surface the failure, rather than aborting. Per the original ask #3, the fix is not to raise the 30 s guard.Merge pull request #923 from Systemorph/fix/648-activation-durable-fi…