Repository navigation
ExecuteScript dispatch answers the caller — nine swallow points closed (#841) - #913
Conversation
Fixes #841. The dispatch could die with no Activity node and no failure mode visible to anyone. Swallow points found and closed: 1. MeshOperations.cs:3740 — hub.Post fire-and-forget. "Dispatched" meant only "Post did not throw"; every ExecuteScriptResponse{Success=false} and every routing DeliveryFailure went to a caller with no callback and was dropped. 2. MeshOperations.cs:3736 — the caller RECONSTRUCTED activityPath as {partition}/_Activity/{id} while the owning hub resolves the parent via ActivityParentPath / DefaultActivityParentPath / {viewer}. With any of those, a fully SUCCESSFUL run left the caller polling a path that can never exist; it also raced CreateNode, so even the default case 404s if polled immediately. 3. CodeNodeType.cs:172 — partitionDefaultStream.Take(1) with no timeout and no empty-guard. PartitionRegistry.GetPartition is a live pathless query; if it never converges the chain stops dead — no node, no response, no log. That is the wedges-to-zero violation. 4-8. CodeNodeType create/self-read/not-executable/stamp/FailActivity sinks — response-only, no logging, faults flattened to .Message, one bare catch {}. 9. McpMeshPlugin.cs:781 — the tool promised "blocks until the kernel signals completion" while timeoutSeconds was entirely unused. ExecuteScript now uses hub.Observe<ExecuteScriptResponse> inside Observable.Defer, bounded by the caller's timeoutSeconds, and returns the hub's AUTHORITATIVE activity path. Success=false yields status Error with the reason; OnError yields errorType + message + detail AND LogError with the exception object, so type and stack reach the logger — the standard set by #892. "Dispatched" now means an Activity node exists. CodeNodeType gets two sinks, RefuseDispatch (Warning) and FaultDispatch (LogError with the exception), both of which always answer the caller and always log; three distinct refusal verdicts replace the conflated one; the bare catch logs. New extracted ResolveActivityParent applies .Take(1).DefaultIfEmpty().Timeout(15s) naming the partition, so exactly one of OnNext/OnError always reaches the subscriber. Fail-without (src reverted, tests unchanged): 6 failed / 0 passed — the viewer-routed case reported the guessed path, the happy path errored with "No node found at rbuergi/_Activity/..." because the guess raced creation, and the three refusal cases all reported "Dispatched" instead of "Error". Reverting only ResolveActivityParent's body: the stall test TIMED OUT after 30s (the silent stop reproduced) and the empty-completion test threw "Sequence contains no elements". Pass-with: AI.Test 77/77, ResolveActivityParent 5/5, Documentation 1/1, Markdown.Export 19/19; independently re-verified 10/10 on the new classes after merging main. Release -warnaserror clean. Exposed a separate defect, filed as #912 and NOT fixed here: CSharpFileParser.SerializeCodeConfiguration writes only the code, so IsExecutable and ActivityParentPath are lost on every FS/blob/GitSync round-trip — a cross-repo file-format decision. A labelled characterization test pins the truthful current behaviour. Closes #841 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes #841 by making ExecuteScript dispatch observable and truthful: the caller now waits (bounded) for an acknowledgement from the owning Code-node hub, receives the authoritative activityPath on success, and gets an explicit error verdict (plus better logging) on all refusal/fault paths instead of silent drops.
Changes:
MeshOperations.ExecuteScriptnow dispatches viahub.Observe<ExecuteScriptResponse>(cold viaObservable.Defer), bounded bytimeoutSeconds, and returns the owning hub’s authoritative activity path.CodeNodeTypedispatch handler now logs and replies on every refusal/fault path, adds bounded/guarded activity-parent resolution, and improves failure reconciliation for foreign-language runs.- Adds/updates tests + documentation to pin the “no silent swallow” contract and clarify that
execute_scriptreturns dispatch acknowledgement (not completion).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.AI.Test/McpReadYourWritesTest.cs | Updates ExecuteScript tests; adds characterization coverage for FS round-trip data loss now that refusals are observable. |
| test/MeshWeaver.AI.Test/ExecuteScriptDispatchDiagnosticsTest.cs | New diagnostics-focused test suite pinning the nine swallow points (ack path, authoritative activity path, refusal/fault surfacing + logging). |
| src/MeshWeaver.Mcp/McpMeshPlugin.cs | Updates MCP tool docs to reflect dispatch-ack semantics and error surfacing. |
| src/MeshWeaver.Graph/Configuration/CodeNodeType.cs | Adds bounded activity-parent resolution and dispatch-side sinks that always both reply and log; improves failure reconciliation/logging. |
| src/MeshWeaver.Documentation/Data/Architecture/ScriptExecution.md | Updates docs to reflect dispatch-verdict JSON and “activity node is source of truth” semantics. |
| src/MeshWeaver.AI/MeshOperations.cs | Reworks ExecuteScript to await ack, surface failures, and return authoritative activity paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var ops = new MeshOperations(Mesh); | ||
| var result = await ops.ExecuteScript($"@Scripts/{id}", timeoutSeconds: 10) | ||
| var result = await ops.ExecuteScript($"@Scripts/{id}", timeoutSeconds: 30) | ||
| .FirstAsync().ToTask(); |
| new TimeoutException( | ||
| $"Activity-parent resolution stalled: the PartitionDefinition lookup for " | ||
| + $"partition '{partitionRoot}' produced no answer within " | ||
| + $"{lookupBudget.TotalSeconds:F0}s. The script was NOT started and no Activity " | ||
| + "node was created."))) |
| /// <returns>Dispatch-status JSON.</returns> | ||
| [McpServerTool(Title = "Execute a Code node", Destructive = true, Idempotent = false, OpenWorld = false)] | ||
| [Description("Runs an executable Code node's C# through the kernel (Microsoft.DotNet.Interactive) and returns stdout / return value / errors. The target node must have `CodeConfiguration.IsExecutable == true`. Blocks until the kernel signals completion (side-effects — e.g. mesh.CreateNode calls inside the script — have happened by the time this returns). Use to run import/test scripts from MCP without needing a UI click.")] | ||
| [Description("Runs an executable Code node's C# on the mesh kernel. The target node must have `CodeConfiguration.IsExecutable == true`. Returns as soon as the run has been ACCEPTED, not when it finishes: `{status:'Dispatched', activityPath}` — poll `get @{activityPath}` for the live log messages and the terminal status (Running → Succeeded/Failed), which is also where a script's own exception surfaces. Any dispatch failure (node missing or unreadable, not executable, activity could not be created, request undeliverable) comes back as `{status:'Error', message}` — a 'Dispatched' reply always means a real activity exists. Use to run import/test scripts from MCP without needing a UI click.")] |
Test Results (shard 4)1 201 tests 1 196 ✅ 4m 38s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 629 tests 1 438 ✅ 5m 35s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)672 tests 672 ✅ 5m 13s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 225 tests 1 225 ✅ 6m 50s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 726 tests 1 724 ✅ 7m 31s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 11 files 11 suites 5m 38s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
Test Results 65 files 65 suites 35m 27s ⏱️ Results for commit 824b93f. ♻️ This comment has been updated with latest results. |
|
CI red on That test is known-sensitive: Before re-running: check whether it is failing on 🤖 Generated with Claude Code |
…-observable-dispatch
Summary
Fixes #841 — the dispatch could die with no Activity node created and no failure mode observable by anyone.
Nine swallow points
MeshOperations.cs:3740hub.Postfire-and-forget — "Dispatched" meant only "Post did not throw". EverySuccess=falseresponse and every routingDeliveryFailurereached a caller with no callback and was dropped.MeshOperations.cs:3736activityPathas{partition}/_Activity/{id}, while the owning hub resolves the parent viaActivityParentPath/DefaultActivityParentPath/{viewer}. With any of those, a fully successful run left the caller polling a path that can never exist — and it racedCreateNode, so even the default case 404s if polled immediately.CodeNodeType.cs:172.Take(1)on a live pathless query with no timeout, no empty-guard — if it never converges the chain stops dead: no node, no response, no log. The wedges-to-zero violation.CodeNodeType.cscreate / self-read / not-executable / stamp /FailActivity.Message, one barecatch { }.McpMeshPlugin.cs:781timeoutSecondswas entirely unused.Fix
ExecuteScriptnow useshub.Observe<ExecuteScriptResponse>insideObservable.Defer(cold), bounded by the caller'stimeoutSeconds, and returns the hub's authoritative activity path.Success=false→Error+ reason;OnError→errorType+message+detailandLogErrorwith the exception object, so type and stack reach the logger — the diagnosability standard set by #892. "Dispatched" now means an Activity node exists.CodeNodeTypegets two sinks —RefuseDispatch(Warning) andFaultDispatch(LogError with the exception) — both of which always answer the caller and log; three distinct refusal verdicts replace the conflated one; the barecatchlogs. New extractedResolveActivityParentapplies.Take(1).DefaultIfEmpty().Timeout(15s)naming the partition, so exactly one of OnNext/OnError always reaches the subscriber.Verification
Fail-without (src reverted, tests unchanged): 6 failed / 0 passed — the viewer-routed case reported the guessed path; the happy path errored "No node found at rbuergi/_Activity/…" because the guess raced creation; all three refusal cases reported
Dispatchedinstead ofError. Reverting onlyResolveActivityParent's body: the stall test timed out after 30 s (the silent stop reproduced) and the empty-completion test threw "Sequence contains no elements".Pass-with: AI.Test 77/77 ·
ResolveActivityParent5/5 · Documentation 1/1 · Markdown.Export 19/19. Independently re-verified 10/10 on the new classes after merging current main.-c Release -warnaserrorclean on all touched projects.Notes
ExecuteScriptnow blocks for the dispatch ack (previously returned instantly), bounded bytimeoutSeconds; the ack is posted right afterCreateNode— ~200 ms in-process.Admin/Partitionquery now fails loudly instead of hanging.Mesh.Observe<ExecuteScriptResponse>directly — contract unchanged, but they need real infra, so CI is their first run.CSharpFileParser.SerializeCodeConfigurationwrites only the code, soIsExecutable/ActivityParentPathare silently lost on every FS/blob/GitSync round-trip — a cross-repo file-format decision. A labelled characterization test pins the truthful current behaviour.Closes #841
🤖 Generated with Claude Code