Skip to content

ExecuteScript dispatch answers the caller — nine swallow points closed (#841) - #913

Merged
rbuergi merged 2 commits into
mainfrom
fix/841-executescript-observable-dispatch
Aug 7, 2026
Merged

rbuergi merged 2 commits into
mainfrom
fix/841-executescript-observable-dispatch

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #841 — the dispatch could die with no Activity node created and no failure mode observable by anyone.

Nine swallow points

# Location Mechanism
1 MeshOperations.cs:3740 hub.Post fire-and-forget — "Dispatched" meant only "Post did not throw". Every Success=false response and every routing DeliveryFailure reached 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 — and it raced CreateNode, so even the default case 404s if polled immediately.
3 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.
4-8 CodeNodeType.cs create / self-read / not-executable / stamp / FailActivity Response-only, no logging, faults flattened to .Message, one bare catch { }.
9 McpMeshPlugin.cs:781 Tool description promised "blocks until the kernel signals completion"; timeoutSeconds was entirely unused.

Fix

ExecuteScript now uses hub.Observe<ExecuteScriptResponse> inside Observable.Defer (cold), bounded by the caller's timeoutSeconds, and returns the hub's authoritative activity path. Success=false → Error + reason; OnError → errorType + message + detail and LogError with the exception object, so type and stack reach the logger — the diagnosability 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 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.

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 Dispatched instead of Error. Reverting only ResolveActivityParent'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 · ResolveActivityParent 5/5 · Documentation 1/1 · Markdown.Export 19/19. Independently re-verified 10/10 on the new classes after merging current main. -c Release -warnaserror clean on all touched projects.

Notes

  • ExecuteScript now blocks for the dispatch ack (previously returned instantly), bounded by timeoutSeconds; the ack is posted right after CreateNode — ~200 ms in-process.
  • The 15 s parent-lookup timeout is a diagnosability boundary, not a retry: a genuinely stalled Admin/Partition query now fails loudly instead of hanging.
  • Orleans/gRPC suites use Mesh.Observe<ExecuteScriptResponse> directly — contract unchanged, but they need real infra, so CI is their first run.
  • Separate defect exposed, filed as CSharpFileParser serializes only the code — IsExecutable / ActivityParentPath are silently lost on every FS, blob and GitSync round-trip #912 and deliberately not fixed here: CSharpFileParser.SerializeCodeConfiguration writes only the code, so IsExecutable/ActivityParentPath are 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

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>
Copilot AI lite review requested due to automatic review settings August 7, 2026 12:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ExecuteScript now dispatches via hub.Observe<ExecuteScriptResponse> (cold via Observable.Defer), bounded by timeoutSeconds, and returns the owning hub’s authoritative activity path.
  • CodeNodeType dispatch 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_script returns 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.

Comment on lines 351 to 353
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();
Comment on lines +143 to +147
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.")]
@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 201 tests   1 196 ✅  4m 38s ⏱️
   11 suites      5 💤
   11 files        0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 629 tests   1 438 ✅  5m 35s ⏱️
   11 suites    191 💤
   11 files        0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

672 tests   672 ✅  5m 13s ⏱️
 10 suites    0 💤
 10 files      0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 225 tests   1 225 ✅  6m 50s ⏱️
   11 suites      0 💤
   11 files        0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 726 tests   1 724 ✅  7m 31s ⏱️
   11 suites      2 💤
   11 files        0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files     11 suites   5m 38s ⏱️
1 763 tests 1 655 ✅ 108 💤 0 ❌
1 857 runs  1 749 ✅ 108 💤 0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files     65 suites   35m 27s ⏱️
8 216 tests 7 910 ✅ 306 💤 0 ❌
8 310 runs  8 004 ✅ 306 💤 0 ❌

Results for commit 824b93f.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

CI red on MeshWeaver.Layout.Test.DataChangeStreamUpdateTest.DataChangeRequest_ShouldUpdateLayoutAreaViews (shard 2) — not obviously this change: #841 touches MeshOperations, CodeNodeType and McpMeshPlugin, none of which are in the layout-render path.

That test is known-sensitive: SynchronizationStream.OwnerVersion's doc comment cites it by name ("the DataChangeStreamUpdateTest count-view non-emission") as the failure that mixing Host.Version into the init frame produced. So it is version/ordering-sensitive on the layout stream.

Before re-running: check whether it is failing on main independently — several PRs are in flight that touch stream versioning and change-feed delivery (#916 layout-area host dedup, #917/#910 change-feed dispatch). If it is red on main, it belongs to that cluster, not here.

🤖 Generated with Claude Code

@rbuergi
rbuergi merged commit 3fce71d into main Aug 7, 2026
16 checks passed
@rbuergi
rbuergi deleted the fix/841-executescript-observable-dispatch branch August 19, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ExecuteScript dispatch dies silently — no Activity node is ever created, and no failure mode in the chain is observable by the caller

2 participants