MeshWeaver applies CQRS at every layer: queries route through a read-side index optimised for fan-out search; reads of a specific node go directly to the owning hub for authoritative, lag-free state; writes are RFC 7396 JSON-merge patches applied by that same hub; and operations are named request types that keep implementation details private. Picking the wrong channel produces subtle consistency bugs — stale content, lost updates, or silent overwrites. This page tells you exactly which channel to use, when, and why. Caller hub / Blazor view Read-side Index Query / GetQuery eventually consistent Sets / shell projections path · name · nodeType · version project only Owning Hub per-node actor authoritative state GetMeshNodeStream Persistence Postgres / Cosmos / Memory Patch Write RFC 7396 JSON-merge patch Query GetMeshNodeStream index sync stream.Update → merge patch Queries find sets (eventually consistent); GetMeshNodeStream reads a single node's live content from its owning hub.

The five primitives at a glance

Intent Primitive
Bind a UI control to a node Declare a path-bound control (new MeshNodeThumbnailControl { NodePath = path }) or JsonPointerReference. The Blazor view subscribes via IMeshNodeStreamCache — layout-area code never loads the node. See Data Binding.
Find a set of nodes mesh.Query<T>(request) — reactive, live, composes with Select/Where/Subscribe. (A live collection goes through workspace.GetQuery(id, …); the QueryAsync shape survives only as a test-only bridge in MeshWeaver.Fixture.)
Read a known node's content (one-shot) workspace.GetMeshNodeStream(path).Where(n => n is not null).Take(1).Timeout(...) — same stream, completed after the first emission
Subscribe to a node's live updates hub.GetMeshNodeStream(path) / workspace.GetMeshNodeStream(path)
Write to a node workspace.GetMeshNodeStream(path).Update(node => updated).Subscribe(...) — the framework ships the merge patch
Perform an operation on a node Named request type handled on the owning hub — e.g. ExecuteScriptRequest, MoveNodeRequest, ImportRequest

Read this once and remember it: queries are for sets. A query that happens to return exactly one row is still a query — and still carries the same consistency caveats.

🚨 One mesh node by path → GetMeshNodeStream, never GetRemoteStream<MeshNode>

The single canonical API for reading or writing one mesh node by path is hub.GetMeshNodeStream(path) / workspace.GetMeshNodeStream(path) (extension methods in MeshWeaver.Mesh.Contract). It routes every reader and writer through the shared IMeshNodeStreamCache — one process-wide upstream per path, so writes are visible to all readers. Read by subscribing to the handle (IObservable<MeshNode>, Content already typed for you); write via .Update(current => current with { … }).Subscribe(...) (cold observable — the side effect only runs on Subscribe).

workspace.GetRemoteStream<MeshNode, MeshNodeReference>(addr, …) and the GetRemoteStream<MeshNode>(addr) collection form throw InvalidOperationException — the single-node remote reduce does not converge (divergent mirror streams, writes invisible to readers), so Workspace.ThrowIfMeshNode refuses them at the call site. The only sanctioned callers are the cache's own upstream and the MeshNode reduce-callback plumbing, which use the internal GetRemoteStreamUnchecked overload.


Why queries are not for reading content

Queries route through a read-side index — a cached projection that is eventually consistent. In production the lag is single-digit to tens of milliseconds, but that window is long enough to break any pattern that requires read-your-writes:

That lag is acceptable for browsing and autocomplete. It is lethal for content access.

Layout areas should bind, not fetch. The lag problem disappears entirely when the GUI subscribes directly to GetMeshNodeStream(path) — the view shows the authoritative current state and re-renders on every change. See Data Binding for the bind-by-path pattern.

GetMeshNodeStream(path) goes straight to the owning hub's workspace — the source of truth. No staleness. Subscribing also activates the hub if it was cold.


🚨 Query .Content is always stale — never read it

mesh.Query<MeshNode> and the lower-level IStorageAdapter.Query(...) enumerate MeshNodes by reading the read-side index (as does the test-only QueryAsync bridge over the same call). The returned objects technically have a .Content property — but it must never be read. The catalog is eventually consistent and the Content column lags every committed write by the index-refresh window.

Bright-line rules — no exceptions:

What you have What you do What you must NOT do
A query to enumerate paths / names / nodeTypes mesh.Query<MeshNode>(req).Take(1).Select(c => c.Items.Select(n => n.Path)) Read n.Content
A known path, want the live MeshNode workspace.GetMeshNodeStream(path) adapter.Query($"path:{path}") and read .Content
A known path, want a one-shot read workspace.GetMeshNodeStream(path).Where(n => n is not null).Take(1).Timeout(...) Anything that goes through the index
Recursive subtree operation (Copy, Move, Delete…) hub.Post(CopyNodeRequest / MoveNodeRequest / DeleteNodeRequest, WithTarget(sourcePath)) — the owning hub uses GetMeshNodeStream internally Load every node from the query result and write each one

Treat MeshNode.Content on a query row as if the column does not exist. Project to the metadata you need — Path, Name, NodeType, Icon, LastModified, Version, State — and stop. If your call site needs Content, you are at the wrong layer: either reshape it to use GetMeshNodeStream, or send the work to the owning hub via a named request type.

🚨 Select only what you need — no whole-node loads

A query is a shell projection, not a node loader. Before writing Query<MeshNode>, ask "which fields do I actually consume?" and add a select: clause to pull only those. The whole-MeshNode shape is a historical convenience that defeats partition routing, balloons memory, and invites the stale-Content antipattern.

The most common consumer — "is this set up to date?" — needs only (path, version). That is enough to compare against a cached snapshot and decide "nothing changed, skip the work" vs. "something changed, recompile." You do not load the nodes themselves to answer this question.

// ❌ Wrong — loads every descendant node (Content and all) to ask one yes/no question.
mesh.Query<MeshNode>($"namespace:{root} scope:descendants nodeType:Code")
    .Take(1)
    .Subscribe(c => needsRecompile =
        c.Items.Any(n => n.Version != cachedVersions[n.Path]));

// ✅ Right — project (path, version), compare against snapshot.
mesh.Query<MeshNode>(
        $"namespace:{root} scope:descendants nodeType:Code select:path,version")
    .Take(1)
    .Select(c => c.Items.Any(row =>
        !cachedVersions.TryGetValue(row.Path!, out var prev) || row.Version != prev))
    .Subscribe(stale => { /* … */ }, ex => logger.LogWarning(ex, "staleness probe failed"));

Field cheat-sheet:

Question select: clause
"Does anything MATCH?" (a set — never one known path) select:path
"Is anything stale?" select:path,version
"Render a tree / list / picker" select:path,name,nodeType,icon
"Show last-modified column" select:path,name,lastModified
"Compute access shells" select:path,nodeType,mainNode

When the projection is not enough — you actually need Content for a specific path (compiler input, document viewer, edit form) — fetch that one node through workspace.GetMeshNodeStream(path). One authoritative read per path, never a subtree-wide content load.

🚨 On a synced query, select: is the switch that loads Content — and omitting it fails silently

The advice above is written for the untyped query surface, where a select: turns each row into a Dictionary<string, object>. workspace.GetQuery / hub.GetQuery are typed on MeshNode, and there the projection behaves differently: the row stays a MeshNode (the object-level projection is deliberately skipped — handing a dictionary to a MeshNode-typed caller is what wedged every select:-carrying query on memex, 2026-08-05), but the SQL is still narrowed.

On that path exactly one column is conditional: content. Everything else — path, name, nodeType, icon, order, lastModified, version, state, mainNode — is projected whether or not you name it. So on a synced query the field list is documentation, and the single bit that changes behaviour is whether content appears:

// ✅ Shell read — the consumer embeds by Path and labels by Name/Order, never touches Content.
hub.GetQuery($"course-modules:{p}",
    $"path:{p} scope:children nodeType:Module select:path,id,name,order");

// ✅ Content-bearing read — `content` named DELIBERATELY.
hub.GetQuery($"ai-settings:{user}",
    $"path:{path} nodeType:AiSettings select:path,id,name,nodeType,content");

// ❌ Reads ContentAs<ModuleConfiguration>() but never asked for content.
//    The adapter emits NULL::jsonb AS content; every ContentAs<T>() returns null.
//    No error, no warning, no empty result — the card summaries are just blank.
hub.GetQuery($"course-modules:{p}",
    $"path:{p} scope:children nodeType:Module select:path,name");

Rule. Give every GetQuery an explicit select:. If any consumer of that stream reads Content, content must be in the list. When you cannot prove the whole downstream chain is content-free, leave the query unprojected — the full node is the conservative default, because a wrong projection fails silently while an absent one only costs bytes.

Corollary — the projection travels with the cache ID, and the ID wins. The synced-query cache is keyed by id alone: GetQuery(id, queries) returns the already-registered stream and ignores queries on a hit. Two call sites that share an id but differ in their select: therefore resolve to whichever subscribed first — a metadata-only reader can starve a content reader of its content, intermittently, depending on render order. Keep the query strings byte-identical wherever an id is shared (the course-overview and module-page readers of course-modules:{coursePath} are the worked example), and scope ids per module — uw-nodes-in:{ns}, never a bare nodes-in:{ns} that a sibling module will also mint.

The recompile design that this rule supports is described in NodeTypeCompilation — the NodeType keeps a {sourcePath → version} snapshot from the synced query, and a divergent emission triggers re-fetch and recompile. Nothing in the catalog row's Content is consulted.

🚨 Never query to ask "does this path exist?" — a stale negative redoes finished work

Same shape as staleness below, and it bites harder because the answer is acted on by writers. The query index trails the durable store by design, so a search/path: probe can answer "absent" for a node that exists — measured 2026-08-25 (#2229): a search reported two just-minted _App tiles missing while a direct read returned both, minutes after they landed.

Question Read it with
"Does {partition}/_App/{id} exist?" GetMeshNodeStream(path) / a direct path read
"Should I create it, or is it already there?" Neither — use CreateOrUpdateNodeRequest, which reads persistence itself
"Which children does this parent have?" A query — a stale negative in a listing is harmless

Severity depends on the write the check guards, and it is worth knowing which case you are in:

The compounding case to watch for: a write whose reply was lost looks identical to a write that never happened, and a query that then answers "absent" appears to confirm it. On 2026-08-25 that pair armed two mesh-wide sweeps forty seconds apart. Check existence by path before retrying anything.

🚨 Staleness lives on the owner — never query to check "is this stale?"

A query is for finding sets of things. "Is this specific thing up to date?" is a question about one thing, and the answer belongs on that thing as a property — never re-derived by querying.

Pattern Where it lives
IsDirty / NeedsRebuild / IsStale flag Property on the owning node (set by its own hub)
Synced subscription that maintains the flag The owning hub's Initialize hook
Snapshot the flag is computed against Stored on the node itself (survives restart)
Consumer wanting to know "is X stale?" Read the property. Never query.

The cleanest demonstration is the NodeType recompile detector:

// In the NodeType's hub WithInitialization — observable pattern, no await,
// no Take(1) on the source subscription (we want to keep listening!).
config.WithInitialization(hub =>
{
    var workspace = hub.GetWorkspace();
    var self = hub.Address.ToString();

    // Two synced queries — Source files and Test files. Path-keyed dedup,
    // Replay(1).AutoConnect(1) upstream sharing, provider fan-out.
    // select:path,version keeps the rows light. Persistent subscription —
    // every emission recomputes.
    var sources = workspace.GetQuery($"{self}:sources",
        $"nodeType:Code namespace:{self}/Source scope:descendants select:path,version");
    var tests = workspace.GetQuery($"{self}:tests",
        $"nodeType:Code namespace:{self}/Test scope:descendants select:path,version");

    Observable.CombineLatest(sources, tests, (s, t) =>
            s.Concat(t).Select(n => (n.Path!, n.Version))
                       .ToImmutableSortedSet())
        .Subscribe(current =>
        {
            // Compute IsDirty against the snapshot stored on the node itself.
            workspace.GetMeshNodeStream(self).Update(node =>
            {
                var snapshot = (node.Content as NodeTypeDefinition)?.CompiledSources
                    ?? ImmutableSortedSet<(string, long)>.Empty;
                var dirty = !current.SetEquals(snapshot);
                return node with { /* IsDirty = dirty */ };
            }).Subscribe(_ => { },
                         ex => logger.LogWarning(ex, "dirty flag update failed"));
        });
});

Why this is load-bearing:

A central InvalidateCache(path) invalidator outside the owning hub — even when wired to the change feed — is the wrong layer. Move the watcher into the owning hub and let it maintain its own dirty flag.

Live reference implementation: NodeTypeCompilationHelpers.InstallCompileWatcher (src/MeshWeaver.Graph/Configuration/).

The "send the work to the owning hub" pattern (Copy / Move / Delete)

Recursive subtree operations look superficially like "query → load each → do something" — that is the pattern that leaks Content reads and stale state. The correct shape sends one request to each affected node's hub, where the handler uses GetMeshNodeStream (or the workspace's MeshNodeReference reducer) to obtain the authoritative state before acting.

// Caller — fires one request per descendant, never touches Content from the query.
// Pure Rx: no async lambda, no `await foreach`, no Observable.Create(async …).
public IObservable<Unit> DeleteSubtree(string rootPath, IMessageHub hub, IMeshService mesh) =>
    mesh.Query<MeshNode>(
            // 1. Enumerate descendant PATHS only — `select:path`, so .Content never loads.
            $"namespace:{rootPath} scope:subtree select:path")
        .Take(1)
        .SelectMany(change =>
            // 2. Fan out: one DeleteNodeRequest per address. Each owning hub handles its
            //    own delete — using workspace.GetMeshNodeStream(self) if it needs current
            //    state, NOT the stale catalog row.
            change.Items.Select(shell => shell.Path!).Append(rootPath)
                .Select(p => hub.Observe(new DeleteNodeRequest(p),
                    o => o.WithTarget(new Address(p))))
                .Merge())
        .Select(_ => Unit.Default);
// Handler — registered on the owning per-node hub. Reads its OWN content via
// the workspace's MeshNodeReference reducer (the source of truth), not via
// any storage adapter or query.
private static IMessageDelivery HandleCopyNodeRequest(
    IMessageHub hub, IMessageDelivery<CopyNodeRequest> request)
{
    var targetPath = request.Message.TargetPath;
    hub.GetWorkspace().GetStream(new MeshNodeReference())!
        .Select(change => change.Value)
        .Where(node => node is not null)
        .Take(1)
        .Subscribe(self =>
        {
            // Use `self` to materialise the target — never query for it.
            hub.Post(new CreateNodeRequest(self! with { /* re-target */ }),
                o => o.WithTarget(new Address("mesh")));
            hub.Post(CopyNodeResponse.Ok(self!), o => o.ResponseFor(request));
        });
    return request.Processed();
}

The DeleteNodeRequest / MoveNodeRequest / CopyNodeRequest types are defined in src/MeshWeaver.Mesh.Contract/CreateNodeRequest.cs. They route to the source-node's address (or to the mesh hub which forwards). The handler never reaches back through the index for content — it reads its own state through the workspace's MeshNodeReference reducer, which is the only non-stale view of the node.

Summary in one line: Query gives you paths and shells; GetMeshNodeStream gives you live content. There is no third channel.

🚨 A query result is a PROJECTION — never re-create a node from one

The rule above is usually stated as "the query row is stale". It is also incomplete: a query row is not the node. A provider is free to omit fields, and the production provider does — PostgreSqlSqlGenerator.GenerateSelectQuery projects id, namespace, name, node_type, description, category, icon, display_order, last_modified, version, state, content, desired_id, main_node, sync_behavior, exclude_from_context and nothing else. created_by, created_date and last_modified_by are real columns — the storage adapter reads them on a point read and writes them on INSERT — but no query projects them, so every node a query hands back on Postgres carries CreatedBy = null and CreatedDate = default.

That is harmless for a listing and destructive for a re-create, because CreateNodeRequest fills a blank stamp with "now, by the caller" (CreatedDate == default ? now : node.CreatedDate). Move is implemented as copy-to-target + delete-the-source, so a move that fed the create path a query row rewrote the authorship of the whole subtree and, with no version history behind it, destroyed the originals (issue #3263: one subtree move on memex re-stamped ~80 nodes).

So a lifecycle operation that re-creates a node reads it from storage, exactly as the move's delete leg enumerates its paths from storage rather than the catalog (#839):

And the four stamps carry an operation-specific answer, never an incidental one:

Operation CreatedDate / CreatedBy LastModified / LastModifiedBy
Move (CopyNodeRequest.PreserveAuthorship = true) preserved verbatim — the node at the target IS the node that was at the source preserved verbatim — a relocation writes no content
Copy cleared, so the create stamps the copier cleared, so the create stamps the copier

A copy must NOT inherit the original's CreatedBy: AccessContextScope derives the identity it impersonates from exactly that field, so an inherited creator makes owner-scoped work run as someone who never touched the node. If a move should be recorded at all it belongs in the activity log — never on top of who wrote the thing.

That same sentence is why PreserveAuthorship is gated, not merely defaulted off. CopyNodeRequest is a wire message, so the flag is caller-settable, and a caller who can read a node could otherwise mint one — in a place they control — that owner-scoped work then runs AS its author. The handler therefore honours the flag only for a caller holding Permission.Delete on the source's namespace: exactly what MoveNodePermissionAttribute requires of a mover, so the flag confers nothing the platform did not already grant that caller. An UNDETERMINED permission fold is a refusal too, reported as unavailability rather than as a denial. Measured with the gate removed: an Editor (Read | Create | Update | …, no Delete) succeeded in copying an admin's node with its authorship intact — row-level security does not close this, because reading the source is precisely what an Editor may do.

Two facts pin it — PreserveAuthorshipIsGatedTest: the Editor is refused with Unauthorized, and the same Editor's plain copy still succeeds and is stamped for them. Without the second, the first is satisfied by a subject who could not copy at all, and the gate would be indistinguishable from RLS refusing the whole operation.

The same asymmetry had a second consequence, fixed separately: the copy leg's content query also excludes satellite paths (IsExcludedFromResults mirrors PG's per-prefix satellite tables) while the delete leg's ListDescendantPaths enumerates every table of the partition — so a move deleted the _Comment / _Thread / _Access satellites it never copied, and reported success (#3272). Reading from storage does not cure that one: the enumeration is what has to change, and the query is the read row-level security filters. The cure splits the two — storage names which satellite CONTAINERS exist, the RLS-filtered query says what may be carried out of them, and a move that still cannot carry everything REFUSES rather than deleting the remainder. See Moving Nodes.


🚨 No "pedestrian queries" — use synced queries

If a component needs to react to a set of MeshNodes (a list, a filter, a catalog, a picker, a compiler input set), do not call meshService.Query<T> directly. Use the synced-query pattern from Synced Mesh Node Queries:

IObservable<IReadOnlyList<MeshNode>> stream = workspace.GetQuery(
    "stable-cache-id",
    "namespace:Agent nodeType:Agent",
    "namespace:Provider nodeType:LanguageModel scope:descendants");

stream.Subscribe(snapshot => …);

This is the only correct way to consume a live MeshNode collection. For free, you get:

A direct mesh.Query<MeshNode> call from application code is a pedestrian query and is almost always wrong: either you don't need a live subscription (one-shot — use GetMeshNodeStream per path), or you do (use workspace.GetQuery).

IMeshQueryCore is internal — application code cannot reach it at all; it exists for the synced-query implementation and the query engine (both surfaces fan out across every registered IMeshQueryProvider, static-node providers included). Everything user-facing — UI lists, pickers, settings tabs, compiler inputs, recursive operation enumeration — goes through workspace.GetQuery.

Canonical patterns to copy (read these before writing your own):

Use case File
Chat agents + models AgentChatClient.Initialize / AgentPickerProjection.ObserveAgentsworkspace.GetQuery($"…:{user}", …)
Harness model list CopilotModelCatalog.Modelsworkspace.GetQuery("LanguageModel\|Copilot", …) projected to IReadOnlyList<string> and data-bound by the picker
Navigation drill / breadcrumb MeshSearchViewHub.GetQuery($"nav-below:{root}:{…}", …) / $"nav-above:{root}"
Sync configuration list GitHubSyncServiceworkspace.GetQuery($"gitsync-cfgs:{spacePath}", …)

If you find yourself reading MeshNode.Content out of a one-shot query to render a UI or feed a compiler, you are at the wrong layer. Wrap the query in workspace.GetQuery and subscribe — the recompile or re-render fires automatically when the underlying nodes change.

🚨 A live set is NEVER a pooled one-shot cached in a field. The tempting anti-pattern for "list of things from somewhere" is ioPool.Run(... ListXAsync ...) into a volatile cached field + an EnsureLoaded() kick-off + a snapshot IReadOnlyList<T> getter. That is wrong twice over: (1) it is a snapshot — it never re-emits when the set changes, so the picker/tab goes stale; and (2) the IoPool leaf runs identity-less (no AccessContext baton on the ThreadPool worker — see ControlledIoPooling → "The pool carries NO AccessContext"), so any node read it does bypasses the subscriber's RLS. Replace it with workspace.GetQuery(...) projected to the shape you want, exposed as IObservable<T> and data-bound. GetQuery is live (re-emits on change), shared (one upstream per id), and carries the subscriber's identity per emission. The Copilot model catalog was migrated exactly this way: ioPool.Run(... CLI ListModelsAsync ...) + EnsureLoaded() + cached field → workspace.GetQuery(...) exposing a live IObservable<IReadOnlyList<string>>.


GetStream is access-checked

workspace.GetMeshNodeStream(path) (server-side) and IMeshNodeStreamCache.GetStream(path) (cache-side, the canonical Blazor read path) both gate on the caller's effective Read permission. The cache evaluates that permission locallyhub.GetEffectivePermissionsPermissionEvaluator's scope walk, no round-trip to the leaf path's hub (the old GetPermissionRequest hop wedged satellite/cell sub-paths that own no hub) — caches the Permission flags per (path, userId) for 30 seconds, and returns an observable that fails with UnauthorizedAccessException when Read is not granted. The shared upstream subscription is opened once per path under the dedicated cache/mesh-node-cache identity, which PermissionEvaluator grants Permission.Read and nothing else (deliberately narrower than ImpersonateAsSystem's Permission.All); per-user enforcement happens at the subscriber boundary.

Revocation propagates within the TTL window. The permission cache is not invalidated reactively — subscribers can keep listening past a revocation event for up to 30 s before the next GetStream issues a fresh probe and surfaces the denial.

Full propagation model: AccessContextPropagation.md. For the case where a node's OWN hub writes with no live caller (a watcher tick, a deferred sync write, a cold-start activation), the node owner is the standing identity — see Owner Injection (and why an empty context is rejected, never faked).


🚨 Content is always typed at the GetMeshNodeStream boundary

Every emission and every Update lambda passing through workspace.GetMeshNodeStream(path?) is round-tripped through the workspace's JsonSerializerOptions — so node.Content is always the registered domain type (e.g. MeshThread, NodeTypeDefinition, AgentConfiguration), never a raw JsonElement. The handle's read path runs a TypedContentObserver between the underlying sync stream and the subscriber; the write path wraps the caller's lambda so the deserialised value goes in and the (re-)serialised JsonElement comes out before the patch lands on the wire.

// ✅ Right — `Content` is the typed MeshThread no matter where the data
//    source stores it (InMemory keeps typed instances; file-system /
//    Postgres / Cosmos round-trip through JSON and would otherwise land
//    as JsonElement).
workspace.GetMeshNodeStream().Update(node =>
{
    if (node.Content is not MeshThread t) return node;   // pattern match Just Works
    return node with { Content = t with { Status = ThreadExecutionStatus.Executing } };
});

Why this matters — the anti-pattern this rule eliminates:

// ❌ WRONG — silently lossy. When Content arrives as JsonElement, the cast
//    fails, the `?? new MeshThread()` fallback overwrites every other field
//    with defaults (Status=Idle, pending={}, etc.), and the next stream.Update
//    persists that default-valued thread. Symptom: tests set Status=Executing,
//    the next AppendUserInput resets it to Idle, the SubmissionWatcher then
//    dispatches a round nobody asked for.
workspace.GetMeshNodeStream().Update(node =>
{
    var thread = node.Content as MeshThread ?? new MeshThread();   // ← silent overwrite
    return node with { Content = thread with { Status = ... } };
});

The handle's deserialisation wrap eliminates the JsonElement case at the boundary. If Content is genuinely absent or wrong-shaped, the pattern match fails cleanly and the lambda returns node unchanged — never a ?? new TFoo() fallback that would clobber the stored content.

Where the wrap lives: MeshNodeStreamHandle.TypedContentObserver (read path) + MeshNodeStreamHandle.Update's wrappedUpdate (write path) in src/MeshWeaver.Mesh.Contract/MeshNodeStreamExtensions.cs. Helpers EnsureTypedContent(node, options) and EnsureSerialisedContent(node, options) are reusable by any other primitive that needs the same shape guarantee.


Where scope walks live

scope:children / scope:descendants / scope:subtree / scope:hierarchy / scope:ancestorsAndSelf / scope:nextLevel are per-provider responsibilities. The mesh level never walks content; it only coordinates fan-out across providers and merges the results. (scope:nextLevel — the populated frontier — is a single Postgres anti-join in the PG provider and a frontier-filter over the descendant walk in the in-memory/static providers.)

Layer Class Walks?
Mesh MeshQuery (top-level), RoutingMeshQueryProvider No. Fans out across providers and partitions, merges per-provider buckets with writable-first ordering, applies post-merge sort/skip/limit/select.
Mesh StaticNodeQueryProvider No walks needed — iterates the in-memory static catalog directly.
Per-provider (SQL) PostgreSqlMeshQuery + PostgreSqlSqlGenerator Yes — pushed down to SQL. path LIKE '<prefix>/%' on the indexed path column for descendants / subtree; namespace = <basePath> for children; in-memory ancestor split + IN-clause for ancestors.
Per-provider (SQL) CosmosMeshQuery + CosmosSqlGenerator Yes — pushed down to Cosmos SQL via CosmosStorageAdapter.QueryNodesAsync.
Per-provider (pedestrian) StorageAdapterMeshQueryProvider (in-memory, file-system, embedded-resource) Yes — composed against IStorageAdapter.ListChildPaths in IObservable form. One instance per IStorageAdapter (i.e. per partition in routed setups).

Adding a new backend (e.g. blob storage) is local — implement IMeshQueryProvider once, with whatever native pushdown the backend supports. The mesh layer is unchanged. Likewise, when something feels like it belongs at the mesh layer ("discover all partitions", "find nodes matching X across the whole mesh"), it goes in RoutingMeshQueryProvider — never into a per-adapter walker.

Autocomplete follows the same rule. Per-adapter AutocompleteAsync consumes the QUERY stream (already-populated MeshNodes) and scores against the prefix — it never reads paths by hand. Discovering partitions when basePath is empty is RoutingMeshQueryProvider.AutocompleteAsync's job.

GUI-side single-node reads — always through IMeshNodeStreamCache

On the server side, workspace.GetMeshNodeStream(path) is the canonical single-node read primitive. On the GUI side (Blazor views), the equivalent is IMeshNodeStreamCache.GetStream(path) — a process-wide shared handle per path, opened once under the Read-only cache/mesh-node-cache identity, replayed and live-connected. Every visible Blazor view that needs the same node joins the same upstream subscription; writes through cache.Update(path, fn) propagate to all subscribers in order.

Going around the cache is not merely discouraged — workspace.GetRemoteStream<MeshNode, MeshNodeReference>(addr, ...) throws, because a second handle diverges: writes through one would be invisible to readers of the other, and the per-view subscription cost would scale with the number of visible views. Always use the cache.

The list-rendering shape (one Blazor view per id, each binding to its own cache stream) is documented separately: Item-Template + MeshNode Stream Binding. The canonical example is the thread chat view — N visible messages, N cache subscriptions, zero per-message layout-area round-trips.


One-shot reads — compose on GetMeshNodeStream

The canonical pattern for "give me this node's current MeshNode right now" is the same stream, completed after the first useful emission:

workspace.GetMeshNodeStream(path)
    .Where(node => node is not null)
    .Take(1)
    .Timeout(TimeSpan.FromSeconds(10))
    .Subscribe(
        node =>
        {
            // Use node.Content, node.Version, etc. — authoritative, no lag.
        },
        ex => logger.LogWarning(ex, "read failed for {Path}", path));

No Query, no await, no FromAsync bridge, no separate request type. The owning hub activates on subscribe, the first emission is its authoritative current state, and Take(1) completes the subscription.

That is the read for a node that exists. A node that may not exist yet is a different problem, and it has its own pattern — the next section.


🚨 An OPTIONAL node: listing for EXISTENCE, stream for CONTENT

Two rules on this page pull in opposite directions the moment the node might not be there yet, and each one, followed alone, is a real defect that has shipped:

If you… …you hit
point GetMeshNodeStream(exactPath) at an absent node The owner answers an authoritative routing NotFound, which terminates the stream with an error — it cannot wait for the node to appear. Worse, that NotFound opens MeshNodeStreamCache's storm-breaker window on that path, and the breaker fast-fails WRITES too — so the read suppresses the very write it is waiting for.
read a known path's Content out of a query The index is eventually consistent; a query's answer for one path can be minutes old. Waiting on a VALUE through it is waiting on an unbounded lag.

So do not pick a horn. Compose them — the listing answers whether it is there, the owner's stream answers what it says:

// EXISTENCE — a children LISTING. Empty-on-absent, so the watcher may be in place long before
// the node is written, and a stale negative only means waiting a beat longer. select:path —
// nothing here reads Content.
hub.GetQuery($"usage:{parentPath}", $"path:{parentPath}/_Usage scope:children nodeType:TokenUsage select:path")
    // Ordinal, never OrdinalIgnoreCase: mesh paths are case-SENSITIVE. A case-insensitive match
    // lets a DIFFERENT node satisfy the gate, and the point read below then opens on a path
    // that does not exist — exactly the NotFound this gate exists to rule out.
    .Where(nodes => nodes.Any(n => string.Equals(n.Path, target, StringComparison.Ordinal)))
    .Take(1)
    // CONTENT — the OWNER's authoritative stream, opened only now that the node demonstrably
    // exists, so it can neither NotFound nor trip the storm breaker. Live (no Take): later
    // writes to the same node keep arriving.
    .SelectMany(_ => workspace.GetMeshNodeStream(target))
    .Select(node => node.ContentAs<TokenUsage>(hub.JsonSerializerOptions))
    .Where(u => u is not null)
    .Subscribe(u => { /* … */ }, ex => logger.LogWarning(ex, "usage read failed for {Path}", target));

Why the ordering is sound rather than merely convenient: the listing is served by the index, which trails the durable store. So "the index has seen it" implies "the store has it" — the point read opened on that signal cannot be early. The lag that makes a query useless for CONTENT is exactly what makes it safe as a gate for the point read.

This is not hypothetical, and it has cost real time in both directions. ThreadTokenUsageTest used a point read, hit No node found at …/_Usage/… under load — an error, not a timeout, so no budget could save it (#1040) — and was moved wholesale onto the query. It then became a repeat CI offender on the other horn: #1812, #2001, and run 32876073965 are all "the observable emitted nothing at all" on a usage wait, never on the thread/cell waits carrying the identical budget in the same test methods. #2001's fix widened every budget from 10 s to 20 s and the same assertion failed at 20 s — widening a wait is not a repair for an unbounded lag.

Creating it anyway? Then you need no existence check at all — use CreateOrUpdateNodeRequest, which reads persistence itself.

🚨 The GUI's node binding is gated here too — "create it first" was never a complete answer

MeshNodeBindingExtensions.Bind — the seam every node-bound control reads through (LayoutAreaReference.GetMeshNodeDataContextBlazorView.DataBind) — carries the two-half shape above, and that is a framework obligation, not a call-site one. Data Binding still says a node-bound editor should have its node created first, and that advice stands; what it cannot do is make the node STAY there.

#3517 is the proof, and it is worth stating exactly because the obvious reading of it is wrong. 473 fail: lines over four days, on all five memex-cloud pods, 3–5 within a 4 ms window per render pass, from two unrelated spaces:

The sample What it looked like What it actually was
rbuergi/_Draft/Event_…, the "Share ⇒ as email" form a form bound before its node was created the node WAS created first — EmailDraftNodeType.EnsureExists does exactly that — and the user then deleted the document's _Draft subtree with the tab still open
roger.sas2026/_Answers/…/Quiz, a course quiz the same mistake, twice the learner's answers node is written by the FIRST answer on purpose; creating it on render would write a node for everyone who merely looked at the page

So neither sample is a call site that forgot. Any bound node can be deleted from under a live view, and some bound nodes are deliberately absent until the user acts — which is why the tolerance belongs in the seam, where one change fixes every binding, present and future.

🚨 And a try/catch or a .Catch(Observable.Empty) in the view would have been worse than the noise it silenced. Swallowing the DeliveryFailureException hides the fault AND leaves the breaker window open on the path — and the breaker fast-fails WRITES too (MeshNodeStreamCache.UpdateRaw), so the suppressed read goes on suppressing the write the form is about to make. The gate is the fix precisely because the NotFound is then never MINTED.

Two details of the seam's gate that are decisions, not incidentals:

NodeBoundBindingToleratesAnAbsentNodeTest pins both live shapes — bind-then-create, and re-bind-after-delete — and asserts the storm window stays shut, which is the half the log flood hides. Its third arm pins the one way this fix could be worse than the bug and silent: a gate that answers "absent" for a node that is plainly there blanks the control for every viewer, with nothing logged. So it is asserted against the two path shapes whose query routing is not uniform — a satellite path ({x}/_Comment/{id}; the same routing as the {x}/_Thread/{id} a thread composer binds, and a query that does not TARGET a satellite path has its satellite rows excluded by construction) and a partition root (dropped from a scope:descendants listing, kept by an exact read) — each proving the node exists through the OWNER's stream first, so a null from the binding can only be the gate's verdict.

🚨 Do not over-apply this to a genuine SET — the worked counter-example is src/MeshWeaver.Blazor.Portal/Chat/ThreadTokenChip.razor.cs:106. That chip reads content out of the very same {thread}/_Usage scope:children query this section just told you not to read a value from, and it is correct. It is summing a SET — every _Usage/{model} child — to paint a total. A briefly-stale total is a cosmetic artefact on screen; nothing decides anything on it.

"Fixing" it into N point reads would be strictly worse in two ways: N per-node hub activations on every chat render, and — on a set that is legitimately EMPTY, i.e. a thread with no rounds yet — every one of those is an absent-node point read, which is the storm breaker in the first row above, now firing on the render path.

So the rule is not about how many nodes you read. It is about whether a stale answer DECIDES anything:

The value… Read it from
is displayed, and a stale one merely looks briefly wrong the query, content and all
gates something — a wait passes, a branch is taken, a write happens the OWNER's GetMeshNodeStream

ThreadTokenUsageTest was in the second row and using the first row's primitive. The chip is in the first row and using it correctly. Same query, opposite verdicts.


Live updates — stay subscribed on GetMeshNodeStream

Use the same stream when you want to react to writes — render a view, wait for a job to finish, watch progress roll in.

workspace.GetMeshNodeStream(jobPath)
    .Where(node => node?.Content is JobStatus { State: "Done" or "Failed" })
    .Take(1)
    .Subscribe(final =>
        logger.LogInformation("Job finished: {State}",
            ((JobStatus)final!.Content!).State));

The first emission is the current state; subsequent emissions arrive as the hub applies writes. Where(...).Take(1) waits until a condition is true and then completes — no polling loop, no Task.Delay.


Writes — GetMeshNodeStream(path).Update<TContent>(...)

Application code writes through the stream handle; the framework turns the lambda into a patch on the owning hub. A lambda that reads Content names its type — that is the typed overload, and it is the shape to write:

workspace.GetMeshNodeStream(targetPath)
    .Update<MyContent>((node, content) => node with
    {
        // `content` is null ONLY when the node carries no content at all.
        Content = (content ?? new MyContent()) with { Status = "done" },
    })
    .Subscribe(_ => { }, ex => logger.LogWarning(ex, "Update failed for {Path}", targetPath));

🚨 A read may tolerate bad data. A write may not. Do not build one on the other.

ContentAs<T> / As<T> answer null for content they cannot read, because a read must stay bad-data tolerant — a settings tab that throws is worse than one that shows a fail-closed default. That same tolerance destroys data on a write. node.Content as T ?? new T(), or any helper that returns a default for content it could not parse, makes "there is nothing here" and "I could not read what is here" the same answer — and the write then persists a default-valued record over every field the caller never touched.

The typed overload removes the choice: null means ABSENT and only absent, while content that is present but unreadable faults the observable with a MeshNodeStreamException naming the path, the runtime type and a JSON excerpt. The write does not happen, the record is left intact, and the caller's .Subscribe(onNext, onError) — or its .Catch — logs the reason.

Two production instances, both silent until the data was gone:

Reach for the untyped Update(node => …) only when the lambda does not read Content — setting Name, State, a Requested* field on a node whose content it never inspects.

DefaultedContentReadInsideAnUpdateLambdaGuard holds the defaulting shape at zero across src/ and memex/ — the untyped .Update(x => …) lambdas that exist today and every one added tomorrow. It has no allow file: the correction is mechanical and preserves behaviour exactly when the record is readable, so an exemption could only ever mean "this one may keep destroying records".

Where the write is not a stream.Update — a durable compare-and-set on a storage adapter, or a pure decision function feeding one — the same rule applies without the primitive: read null when Content is null, and REFUSE loudly when the content is present and unreadable. Refuse by writing nothing and logging what was withheld, not by throwing, wherever a throw would strand a lock or a claim the caller still has to hand back (BuildNodeType.ReadLockStateOrRefuse and BuildNodeType.ApplyGrant are the two worked examples: the first guards the durable claim LOCK a compare-and-set commits against, the second the mirror publication whose refusal HandBackAStoodDownGrant reads in order to release that lock).

Under the hood the handle diffs current vs update(current) and ships an RFC 7396 JSON-merge patch (PatchDataChangeRequest on the stream protocol) to the owning hub, which merges it against its authoritative state on its single-threaded action block. That plumbing is internal — application code never posts PatchDataChangeRequest/PatchDataRequest itself.

Never go through a query + merge in memory + a full-node write. The index read is stale; the merge loses concurrent writes; the full-node replace overwrites anything you didn't explicitly read. Let the owning hub apply the patch on its authoritative state.


🚨 Creating typed nodes — everything comes from the REGISTRY, nothing from your hand

Creating a node is not assembling JSON. Three registries decide whether your write is even meaningful, and the write boundary enforces all three fail-closed:

  1. The NodeType must name a registered NodeType. A write whose NodeType resolves to nothing is refused with "NodeType 'X' is not registered". There is no "just a string" node type: the name is a claim that a module owns and can activate this node. (The Store contact form shipped writing NodeType: "SalesInquiry" — a name registered nowhere — and every enquiry on every mesh was refused for a month before anything executed the write.)

  2. The content's $type must resolve in the static registry. Cross-hub, your typed CLR content serializes to JSON carrying a $type discriminator, and ContentDiscriminatorValidator refuses any discriminator the mesh root's ITypeRegistry chain cannot resolve for a built-in NodeType — because accepting it would persist an untyped blob that renders empty and cannot be edited. Never hand-assemble a $type string. Use the framework's own content record (new Email { … }, new MarkdownContent { … }) and let serialization write the registered name; when in doubt, read the declared shape at @{NodeType}/schema/.

  3. Every content type a built-in NodeType declares via WithContentType<T>() MUST be in WithGraphTypes (the static registry of functionality). The validator's strict branch assumes exactly that — an omission is invisible for as long as only in-process writers exist (typed content bypasses the guard) and then refuses the first cross-hub writer. That is how Email broke the contact form's notification phase in production (2026-08-12): registered as a NodeType, missing from the registry, undetectable until a compiled plugin queued one.

And the write itself goes through the owning hub, never around it — the canonical verbs are the whole surface: CreateNodeRequest / CreateOrUpdateNodeRequest for new nodes, GetMeshNodeStream(path).Update(...) for edits. The owning hub autonames, stamps, types and validates on its single-threaded action block. This is the same pattern thread creation uses — the thread hub mints the node, names it, and types its content; the caller only says what it wants. If you find yourself constructing a $type by hand or writing a node whose type you invented, you are on the wrong side of the registry.


Upserts (CreateOrUpdateNodeRequest) — single verb, no delete-then-create

When the caller has the full target shape and wants the node to land regardless of whether it already exists (copy / move / import / agentic write-back), use the single-verb upsert:

hub.Observe<CreateOrUpdateNodeResponse>(
        new CreateOrUpdateNodeRequest(targetNode))
    .FirstAsync()
    .Select(d => d.Message)
    .Subscribe(resp =>
    {
        if (!resp.Success) { /* resp.Log + resp.Error */ return; }
        // resp.WasCreated tells you create-vs-update; resp.Log carries audit.
    });

Why a dedicated verb instead of chaining a create and an update yourself:

Bulk upserts (e.g. node-tree copy) compose the per-node observable and merge with bounded concurrency so a wide subtree doesn't open every per-node hub simultaneously on the receiving side:

allNodes
    .Select(node => hub.Observe<CreateOrUpdateNodeResponse>(
            new CreateOrUpdateNodeRequest(BuildTarget(node)))
        .FirstAsync()
        .Select(d => d.Message.Success ? 1 : 0))
    .ToObservable()
    .Merge(maxConcurrent: 16)
    .Sum();

NodeCopyHelper.CopyNodeTree is the canonical example — force=false routes through CreateNodeRequest (skip-on-exists), force=true routes through CreateOrUpdateNodeRequest (always upsert). The same shape applies to import, mirror, and any future "write a batch of MeshNodes from an external source" flow.


Operations — named request types per intent

When you want to do something on a node (rather than read or write its content), define a named request type and handle it on the owning hub. The caller never sees the implementation detail.

Example — run a script on a Code node. The caller doesn't know (or need to know) that the Code hub dispatches to an internal kernel:

// In MeshWeaver.Mesh.Contract — no MeshWeaver.Kernel reference!
public record ExecuteScriptRequest : IRequest<ExecuteScriptResponse>
{
    public string? SubmissionId { get; init; }
}

public record ExecuteScriptResponse
{
    public bool Success { get; init; }
    public string? SubmissionId { get; init; }
    public string? OutputAreaReference { get; init; }
    public string? Error { get; init; }
}

The Code node's hub registers a synchronous handler — it subscribes and returns immediately; the response is posted from inside the callback (never .Current, see "Handlers: reactive chains" below):

// In CodeNodeType.HubConfiguration
config.WithHandler<ExecuteScriptRequest>(HandleExecuteScript)

private static IMessageDelivery HandleExecuteScript(
    IMessageHub hub, IMessageDelivery<ExecuteScriptRequest> request)
{
    // Reactive read of this hub's OWN node — the first emission is its
    // authoritative state. `.Current` would be null on a cold workspace.
    hub.GetWorkspace().GetStream(new MeshNodeReference())!
        .Select(change => change.Value)
        .Where(node => node is not null)
        .Take(1)
        .Subscribe(node =>
        {
            if (node!.Content is not CodeConfiguration code || !code.IsExecutable)
            {
                hub.Post(new ExecuteScriptResponse { Success = false, Error = "..." },
                    o => o.ResponseFor(request));
                return;
            }

            var submissionId = request.Message.SubmissionId ?? Guid.NewGuid().ToString("N");
            var kernelAddress = /* private — derived from hub.Address */;

            // Fire-and-forget dispatch to the (private) kernel.
            hub.Post(new SubmitCodeRequest(code.Code ?? "") { Id = submissionId },
                o => o.WithTarget(kernelAddress));

            hub.Post(new ExecuteScriptResponse
                {
                    Success = true,
                    SubmissionId = submissionId,
                    OutputAreaReference = submissionId
                },
                o => o.ResponseFor(request));
        });
    return request.Processed();   // handler returns immediately
}

The caller fires the request at the node and subscribes for progress:

var delivery = hub.Post(
    new ExecuteScriptRequest(),
    o => o.WithTarget(new Address(codeNodePath)));

hub.Observe(delivery, (d, _) =>
{
    if (d is IMessageDelivery<ExecuteScriptResponse> resp && resp.Message.Success)
    {
        // Subscribe to the output area for progress — still no direct kernel reference.
        workspace.GetRemoteStream<UiControl, LayoutAreaReference>(
            new Address(codeNodePath),
            new LayoutAreaReference(resp.Message.OutputAreaReference!))
            .Subscribe(/* ... */);
    }
    return Task.FromResult(d);
});

Rules for operation handlers:


Handlers: reactive chains, not .Current

Inside a .WithHandler<TRequest>(...) body the handler must not block. State is read reactively — compose with .Select(...) / .Where(...) / .Take(1) / .Subscribe(...). The Subscribe callback fires once the stream emits; the handler returns request.Processed() immediately and the callback later posts the actual response via hub.Post(response, o => o.ResponseFor(request)).

Never .Current / .Current?.Value on a stream. Current is populated after the stream has emitted its first value — inside a handler that just triggered the hub's activation, the workspace hasn't loaded data yet and Current is null. You will ship a wrong answer. The reactive chain avoids this: Subscribe fires once the data is actually there.

// ❌ NEVER
var node = hub.GetWorkspace().GetStream(new MeshNodeReference())?.Current?.Value;

// ✅ ALWAYS
hub.GetWorkspace().GetStream(new MeshNodeReference())
    ?.Select(change => change.Value)
    .Where(node => node is not null)
    .Take(1)
    .Subscribe(node =>
    {
        // handler logic here — post the response inside this callback
        hub.Post(new MyResponse { /* ... */ }, o => o.ResponseFor(request));
    });
return request.Processed();   // handler returns immediately
Inside a handler OK?
hub.Post(...) — fire a message ✅ sync
hub.Observe(delivery, callback) — register; callback fires later ✅ sync
workspace.GetMeshNodeStream(path).Update(fn).Subscribe(...) — apply an update ✅ sync subscribe; write runs on the owner's action block
hub.GetWorkspace().GetStream(ref)?.Select(...).Where(...).Take(1).Subscribe(...) — reactive read
hub.GetWorkspace().GetStream(ref)?.Current?.Value — snapshot read ❌ null on cold workspaces
await anything ❌ never
Observable.FromAsync(...) ❌ hides an await — same bug

Quick decision matrix

Intent Primitive
List nodes under X (paths / metadata only) mesh.Query<MeshNode>(MeshQueryRequest.FromQuery(...)) — project to Path / Name / etc. never read .Content
Does anything match a predicate? Query + check Items.Count
Does node X (a KNOWN path) exist? workspace.GetMeshNodeStream(X) — a query's negative can be minutes stale, and a caller that writes on it redoes finished work (why). Creating it anyway? CreateOrUpdateNodeRequest — no check needed
Give me node X's MeshNode (live) workspace.GetMeshNodeStream(X) — the only non-stale read path
Give me node X, which may not exist yet Children LISTING (select:path) for existence, then GetMeshNodeStream(X) for content (pattern). Neither half alone: a point read of an absent node NotFounds AND storm-breaks the writer; a query's Content for one path can be minutes stale
Give me node X's MeshNode (once) workspace.GetMeshNodeStream(X).Where(n => n is not null).Take(1).Timeout(...)
Keep me updated on node X's MeshNode workspace.GetMeshNodeStream(X) — stay subscribed (no .Take(1))
Patch node X workspace.GetMeshNodeStream(X).Update(node => updated).Subscribe(...)
Replace node X wholesale (create-or-update) hub.Observe<CreateOrUpdateNodeResponse>(new CreateOrUpdateNodeRequest(fullNode)).Subscribe(...)
Run the script on Code node X hub.Post(ExecuteScriptRequest(), WithTarget(X)) + Observe<ExecuteScriptResponse>
Wait until the run finishes workspace.GetRemoteStream on X's output area until a terminal condition
Move/Copy node X (incl. subtree) hub.Post(MoveNodeRequest / CopyNodeRequest, WithTarget(X)) — owning hub reads its own state via GetMeshNodeStream, fans out per-child requests, never queries for content
Delete node X (incl. subtree) hub.Post(DeleteNodeRequest, WithTarget(X)) — recursive variant queries for paths only then fires one DeleteNodeRequest per descendant address
Stream content into node X during execution (AI streaming, long-running output) Push every delta via workspace.GetMeshNodeStream(X).Update(node => node with { Content = ... }).Subscribe(...) — the shared cache handle the readers bind to. See Thread Execution Streaming for the canonical writer + renderer pair.

Anti-patterns

// ❌ Query to get content — stale read, lost-update risk. (Also: `QueryAsync` is a
//    test-only bridge; `await` in hub-reachable code deadlocks the action block.)
var node = await mesh.QueryAsync<MeshNode>($"path:{path}").FirstOrDefaultAsync();
return JsonSerializer.Serialize(node);

// ❌ Same in reactive clothing — still a query, still stale.
return mesh.Query<MeshNode>(MeshQueryRequest.FromQuery($"path:{path}"))
    .Take(1).Select(c => c.Items.FirstOrDefault());

// ❌ Reading Content off a query result — Content is stale (and null unless
//    `select:` named it).
mesh.Query<MeshNode>($"namespace:{parent} scope:subtree").Take(1)
    .Subscribe(c => { foreach (var n in c.Items)
        if (n.Content is JobStatus { State: "Done" }) { … } });   // ← stale Content

// ❌ Wrapping a query in Observable.FromAsync does not fix consistency — and
//    Observable.FromAsync is itself forbidden outside IoPool.
return Observable.FromAsync(ct =>
    mesh.QueryAsync<MeshNode>($"path:{path}").FirstOrDefaultAsync(ct).AsTask());

// ❌ "Recursive operation" by loading every subtree node from a query.
//    Stale Content + N+1 + memory blow-up + bypasses per-node hub validators.
mesh.Query<MeshNode>($"namespace:{root} scope:subtree").Take(1)
    .Subscribe(c => { foreach (var n in c.Items)
        storage.DeleteAsync(n.Path); });    // ← uses stale n; bypasses hub

// ❌ Caller addressing the implementation detail (kernel) directly.
hub.Post(new SubmitCodeRequest(...), o => o.WithTarget(kernelAddress));

// ❌ Async in a handler body.
.WithHandler<FooRequest>(async (hub, req) => { await something; return req.Processed(); })

// ✅ Project to metadata only — `.Path` / `.Name` / `.NodeType`, never `.Content`.
mesh.Query<MeshNode>($"namespace:{parent} scope:subtree select:path")
    .Take(1)
    .Select(c => c.Items.Select(shell => shell.Path!).ToImmutableArray())
    .Subscribe(paths => { /* never read shell.Content */ });

// ✅ Need content for a known path? Subscribe to the owning hub.
workspace.GetMeshNodeStream(path)
    .Take(1)
    .Subscribe(node => { /* node.Content is live, no lag */ });

// ✅ Recursive operation — fan out one request per descendant address;
//    each owning hub does the work with its own live state.
Observable.Merge(paths.Select(p =>
        hub.Observe(new DeleteNodeRequest(p), o => o.WithTarget(new Address(p)))))
    .Subscribe(_ => { }, err => logger.LogError(err, "delete fan-out failed"));

// ✅ One-shot content read — authoritative, same stream as live reads.
workspace.GetMeshNodeStream(path)
    .Where(n => n is not null).Take(1).Timeout(TimeSpan.FromSeconds(10))
    .Subscribe(node => { /* ... */ }, ex => logger.LogWarning(ex, "read failed"));

// ✅ Live updates — Blazor views bind to the same shared handle (see Data Binding).
workspace.GetMeshNodeStream(path);

// ✅ Named operation — caller never references the kernel.
hub.Post(new ExecuteScriptRequest(), o => o.WithTarget(new Address(codeNodePath)));

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.