Why an operation request can look inert

A field report on 2026-09-06 (#1320) said the agent-facing half of operation requests does not work: a request created through the API — the MCP create tool, a well-formed OperationRequestContent with requestedAction: "Preview""was created. It never previewed. steps: [] forever, no error, no log, nothing to grep." The decisive difference it named was the missing Script child, and the conclusion drawn was that "the executable half is generated by the creation flow, not by the request node's own watcher".

That conclusion is wrong, and the page you are reading exists so nobody re-derives it. The Script child has exactly one author — OperationRequestControlPlane.Run — and the API path works. What was true is that the control plane could stop watching, permanently and silently, and a request whose watcher has stopped is indistinguishable from one nobody has picked up yet.

The measurement

Measured on the live memex.systemorph.com portal, 2026-09-06, with no browser involved at any point — a request created purely through the MCP create tool (rbuergi/Requests/probe-1320, a plan of one Note step that touches nothing):

What When Elapsed
create (requestedAction: "Preview" already set) 16:18:01.794Z
the watcher started the preview 16:18:01.880Z 86 ms after the create
state: "Ready", plan recorded, Script child written by the control plane 16:18:02.650Z 856 ms after the create
patchrequestedAction: "Preview" again 16:20:34.208Z re-previewed in 677 ms
patchrequestedAction: "Approve" 16:23:48.457Z
state: "Succeeded", message: "Done: 1 step(s) ran." 16:23:49.366Z 909 ms after the approve

So: create → preview → approve → run, entirely through the API, sub-second at every step. The Script child (nodeType: Code, createdBy: system-security) is generated by the node's own watcher on the Preview/Approve transition, exactly as the issue's own suggested shape proposed — it already worked that way.

The request in the field report tells the same story from the other side. Its version history is v1 at 10:35:37 (the create, requestedAction: Preview), v2/v3 at 11:15:20 and 11:15:32 (the reporter forcing Preview → None → Preview), then v4v6 — the control plane's own three writes, whose content timestamps read 15:59:11–12, and whose Script child carries createdDate: 15:59:11.286Z. Nothing ran for four hours and forty-four minutes, and then the whole preview ran in 1.3 seconds. That is not a missing mechanism. That is a mechanism that was not subscribed, and then was.

🚨 A MeshNodeStreamHandle.Update does not re-stamp lastModified, so the version rows v3v6 all carry v3's timestamp. Read the content's own startedAt/previewedAt and the child node's createdDate for when a run actually happened — the version row's lastModified is the node's field, not the write's clock.

The defect: the recovery arm was itself a write

The watcher was one subscription for the life of the per-node hub:

workspace.GetMeshNodeStream()
    .Where(node => node is not null)
    .Select(node => Process(hub, workspace, node!)
        .Catch<Unit, Exception>(exception => Stamp(workspace, node!.Path, c => c.Cleared() with
        {
            State = OperationRequestState.Failed, Error = exception.Message,
        })))
    .Concat()
    .Subscribe(_ => { }, exception => logger?.LogWarning(exception, "watcher stream failed"));

The Catch looks like the guard that keeps the pipeline alive, and its comment said so. It is not: Stamp is a cold write to the mesh, so the recovery can fail on its own. When it does, the error leaves the Catch, Concat forwards it, and the subscription's onError runs — which ends the sequence. From that moment the hub is up, the node is writable, patches land and bump the version, and no action on that request is ever processed again. There is no resubscribe, and there is nothing on the node saying so: the terminal Failed stamp is precisely the write that failed. The only trace is a single LogWarning in one pod's log.

That end state is byte-for-byte the field report: requestedAction still Preview, state absent (so Proposed), steps: [], scriptHash absent, no activityPath, no Script child, no error. And it is cured by the one thing the fix does — a fresh hub. Which is what 15:59 was.

What the control plane guarantees now

OperationRequestControlPlane.Watch is the composition, extracted so the invariant is pinned by a test rather than by review:

Two cases in OperationRequest/Test/OperationRequestTests.cs hold it: AFailedPass_WhoseRecordingAlsoFails_KeepsTheWatcherAlive (the trigger after a failed pass whose recording also failed is still processed) and AFailedPass_IsRecorded_AndTheWatcherCarriesOn (the ordinary path: recorded once, reported once, order preserved). Reverting the inner Catch alone turns the first one red and leaves the other 26 cases green.

What this deliberately does NOT do

It does not declare an interrupted run dead. A request stamped Previewing/Running whose hub then goes away — a portal roll, a grain deactivation, a pod eviction — is still stuck: OperationRequestContent.ShouldRun() is false for both in-flight states, so no action, not even Reject, is ever processed again. That is a second, independent defect and it wants a different fix from the obvious one:

The run is not owned by the control-plane hub. Run dispatches an ExecuteScriptRequest to the Script child's kernel and follows the resulting activity; the kernel keeps going when the request's hub restarts. So a fresh hub that finds an in-flight state must re-attach to ActivityPath and keep folding its frames — not stamp Failed, which would tell the approver the deletion stopped while it is still deleting. Only an in-flight state with no ActivityPath was genuinely never dispatched.

Filing that as its own change is deliberate: getting it wrong makes the page say one thing while the mesh does another, which is worse than the wedge.

Seven other control planes carry the same shape

This is not one type's mistake — it is the shape every RequestedX watcher in the repo was written in, copied faithfully, comment and all ("A failed transition must never wedge the pipeline"). A source scan over every *ControlPlane.cs / *Watcher.cs outside src/ finds the recovery-arm-is-a-write pattern in:

Store/Order OrderControlPlane
Store/Provision ProvisionControlPlane
Store/Enrollment EnrollmentControlPlane
Store/Maintenance MaintenanceControlPlane
Store/Subscription SubscriptionControlPlane
Hosting/InstanceAction InstanceActionControlPlane
Hosting/InstanceRequest InstanceRequestControlPlane

Each is Process(…).Catch(ex => Stamp(…Failed…)).Concat().Subscribe(_ => { }, ex => LogWarning), so each can lose its watcher to a failing write and leave its own node reading as merely pending — an order stuck before fulfilment, a provision stuck at Requested, an enrollment that never grants.

They were left alone in the #1320 change to keep it to the type the issue is about. The fix for each is to route it through the same Watch invariant; the reason to do them together is that a shared helper needs a home both Store/* and Essentials/* can see (Store/Core/Source, which Essentials/OperationRequest already shares), and moving it there is a decision about the Store package's surface rather than about this bug.

Reproducing and measuring locally

The in-mesh test bodies run on this laptop — no Docker, no mesh — through the platform's own gate runner, which compiles each package's Source/ and Test/ with Roslyn and executes every *Tests case:

mw-plugin-test build <plugins-repo-root> Essentials \
  --module <…>/MeshWeaver.AI.dll \
  --module <…>/MeshWeaver.Markdown.Collaboration.dll \
  --module <…>/MeshWeaver.Payments.Stripe.dll

The three --module arguments are not optional: without them four Store NodeTypes fail to compile against a reference set that has no module bundles in it (MeshWeaver.Payments missing, then StripeWebhooks/StripeGateway), Store goes RED, and every package that requires it — Essentials included — is reported blocked, having compiled and tested nothing. A blocked package is not a pass.

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