diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs
index d412572e..40a86b40 100644
--- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Actors/DeploymentManagerActor.cs
@@ -954,7 +954,18 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
{
var result = t.Result;
return new RouteToCallResponse(
- request.CorrelationId, result.Success, result.ReturnValue,
+ request.CorrelationId, result.Success,
+ // The routed script's return value crosses the Central↔Site
+ // PROCESS boundary inside this response. A script's natural
+ // `return new { ... }` is an anonymous type, which Akka's
+ // cross-process serializer cannot round-trip — the reply is
+ // silently dropped and the caller's Route.To().Call() Ask
+ // times out even though the script succeeded. Project the
+ // value to a plain CLR graph (Dictionary/List/primitive) that
+ // round-trips the wire and reproduces the same JSON shape on
+ // the inbound side (which JsonSerializer-serializes it for the
+ // HTTP body / ReturnDefinition validation).
+ NormalizeRoutedReturnValue(result.ReturnValue),
result.ErrorMessage, DateTimeOffset.UtcNow);
}
return new RouteToCallResponse(
@@ -972,6 +983,59 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
}
}
+ ///
+ /// Projects a routed script's return value to a cross-process-serializable plain
+ /// CLR graph ( / /
+ /// string / long / double / bool / null) via a JSON round-trip. A script's natural
+ /// return new { ... } is a compiler-generated anonymous type that Akka's
+ /// cross-process serializer cannot reconstruct on the receiving node, so the
+ /// reply is silently dropped and the caller's
+ /// Ask times out. The projected graph round-trips the wire and re-serializes to the
+ /// same JSON shape the inbound API expects. Returns
+ /// unchanged on a (non-expected) serialization failure so a quirky value still has a
+ /// chance to deliver rather than being forced to null.
+ ///
+ private object? NormalizeRoutedReturnValue(object? value)
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ try
+ {
+ var json = System.Text.Json.JsonSerializer.Serialize(value);
+ using var doc = System.Text.Json.JsonDocument.Parse(json);
+ return FromJsonElement(doc.RootElement);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex,
+ "Failed to normalize routed script return value of type {Type} for transport; sending as-is",
+ value.GetType().Name);
+ return value;
+ }
+ }
+
+ ///
+ /// Converts a to a plain CLR value
+ /// (string / long / double / bool / null, or nested Dictionary / List) — never a
+ /// (which is itself not cross-process
+ /// serializable). Companion to .
+ ///
+ private static object? FromJsonElement(System.Text.Json.JsonElement e) => e.ValueKind switch
+ {
+ System.Text.Json.JsonValueKind.Object =>
+ e.EnumerateObject().ToDictionary(p => p.Name, p => FromJsonElement(p.Value)),
+ System.Text.Json.JsonValueKind.Array =>
+ e.EnumerateArray().Select(FromJsonElement).ToList(),
+ System.Text.Json.JsonValueKind.String => e.GetString(),
+ System.Text.Json.JsonValueKind.Number => e.TryGetInt64(out var l) ? l : e.GetDouble(),
+ System.Text.Json.JsonValueKind.True => true,
+ System.Text.Json.JsonValueKind.False => false,
+ _ => null,
+ };
+
///
/// Reads attribute values from a deployed instance for a Route.To().GetAttribute(s)
/// call (or a central Test Run bound to the instance). Asks the Instance Actor