feat(flow-debugger): carry site-side trace truncation on the route reply (ScadaBridge#54)

ScriptCallResult and RouteToCallResponse gain trailing Truncated/LogsTruncated,
filled from the site FlowTraceCollector after Complete(); RouteToCallResponseDto
fields 8/9 carry them (sitecommand regen). FlowDebugRunner ORs them into
FlowDebugRunResult on the script entry, and on the method entry through a new
optional truncation sink on RouteHelper.WithTraceSink (carried through every
With* clone). Previously a site run whose collector dropped rows came back as a
silently short list. Older sites send false.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 12:37:57 -05:00
parent 5310fd0e26
commit 6d5bb6189f
15 changed files with 484 additions and 155 deletions
@@ -86,6 +86,7 @@ public class RouteHelper
private readonly INodeIdentityProvider? _nodeIdentity;
private readonly bool _trace;
private readonly Action<AuditEvent>? _traceSink;
private readonly Action<bool, bool>? _siteTruncationSink;
private readonly IReadOnlySet<string>? _permittedSiteIdentifiers;
private readonly bool _captureBodies;
private readonly ILogger? _logger;
@@ -107,7 +108,7 @@ public class RouteHelper
INodeIdentityProvider? nodeIdentity = null,
ILogger<RouteHelper>? logger = null)
: this(instanceLocator, instanceRouter, CancellationToken.None, CancellationToken.None, parentExecutionId: null,
auditWriter, nodeIdentity, trace: false, traceSink: null, permittedSiteIdentifiers: null, captureBodies: true, logger)
auditWriter, nodeIdentity, trace: false, traceSink: null, permittedSiteIdentifiers: null, captureBodies: true, logger, siteTruncationSink: null)
{
}
@@ -123,7 +124,8 @@ public class RouteHelper
Action<AuditEvent>? traceSink,
IReadOnlySet<string>? permittedSiteIdentifiers,
bool captureBodies,
ILogger? logger)
ILogger? logger,
Action<bool, bool>? siteTruncationSink)
{
_permittedSiteIdentifiers = permittedSiteIdentifiers;
_instanceLocator = instanceLocator;
@@ -135,6 +137,7 @@ public class RouteHelper
_nodeIdentity = nodeIdentity;
_trace = trace;
_traceSink = traceSink;
_siteTruncationSink = siteTruncationSink;
_captureBodies = captureBodies;
_logger = logger;
}
@@ -150,7 +153,7 @@ public class RouteHelper
/// <returns>A new <see cref="RouteHelper"/> inheriting the given deadline token.</returns>
public RouteHelper WithDeadline(CancellationToken deadlineToken) =>
new(_instanceLocator, _instanceRouter, deadlineToken, _requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger, _siteTruncationSink);
/// <summary>
/// Returns a <see cref="RouteHelper"/> carrying the raw request-abort
@@ -164,7 +167,7 @@ public class RouteHelper
/// <returns>A new <see cref="RouteHelper"/> carrying the given request-abort token.</returns>
public RouteHelper WithRequestAborted(CancellationToken requestAbortedToken) =>
new(_instanceLocator, _instanceRouter, _deadlineToken, requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger, _siteTruncationSink);
/// <summary>
/// Audit Log (ParentExecutionId): returns a <see cref="RouteHelper"/> whose
@@ -179,7 +182,7 @@ public class RouteHelper
/// <returns>A new <see cref="RouteHelper"/> carrying the given parent execution id.</returns>
public RouteHelper WithParentExecutionId(Guid? parentExecutionId) =>
new(_instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, parentExecutionId,
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger, _siteTruncationSink);
/// <summary>
/// Flow Debugger (design 2026-09-05 §5.2): returns a <see cref="RouteHelper"/> whose routed
@@ -190,7 +193,7 @@ public class RouteHelper
/// <returns>A new <see cref="RouteHelper"/> carrying the flag.</returns>
public RouteHelper WithTrace(bool trace) =>
new(_instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger, _siteTruncationSink);
/// <summary>
/// Flow Debugger (design 2026-09-05 §7.1): returns a <see cref="RouteHelper"/> that also
@@ -199,10 +202,15 @@ public class RouteHelper
/// audit store. Null clears the sink.
/// </summary>
/// <param name="traceSink">Receives the audit events in emission order; null = none.</param>
/// <param name="siteTruncationSink">
/// Gitea #54: receives <c>(truncated, logsTruncated)</c> from a routed reply whose SITE-side
/// inline collector dropped rows, so the caller can label a short list as short. Called only
/// when at least one flag is set; null = none.
/// </param>
/// <returns>A new <see cref="RouteHelper"/> carrying the sink.</returns>
public RouteHelper WithTraceSink(Action<AuditEvent>? traceSink) =>
public RouteHelper WithTraceSink(Action<AuditEvent>? traceSink, Action<bool, bool>? siteTruncationSink = null) =>
new(_instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, _trace, traceSink, _permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, traceSink, _permittedSiteIdentifiers, _captureBodies, _logger, siteTruncationSink);
/// <summary>
/// Flow Debugger outbound calls (plan 2026-09-06, D5/D7): the trace sink this helper carries,
@@ -236,7 +244,7 @@ public class RouteHelper
/// <returns>A new <see cref="RouteHelper"/> carrying the scope.</returns>
public RouteHelper WithSiteScope(IReadOnlySet<string>? permittedSiteIdentifiers) =>
new(_instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, _trace, _traceSink, permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, _traceSink, permittedSiteIdentifiers, _captureBodies, _logger, _siteTruncationSink);
/// <summary>
/// Arch-review F11: returns a <see cref="RouteHelper"/> whose always-on <c>RouteCall</c> row
@@ -251,7 +259,7 @@ public class RouteHelper
/// <returns>A new <see cref="RouteHelper"/> carrying the flag.</returns>
public RouteHelper WithBodyCapture(bool captureBodies) =>
new(_instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, captureBodies, _logger, _siteTruncationSink);
/// <summary>
/// Creates a route target for the specified instance.
@@ -262,7 +270,7 @@ public class RouteHelper
{
return new RouteTarget(
instanceCode, _instanceLocator, _instanceRouter, _deadlineToken, _requestAbortedToken, _parentExecutionId,
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger);
_auditWriter, _nodeIdentity, _trace, _traceSink, _permittedSiteIdentifiers, _captureBodies, _logger, _siteTruncationSink);
}
}
@@ -281,6 +289,7 @@ public class RouteTarget
private readonly INodeIdentityProvider? _nodeIdentity;
private readonly bool _trace;
private readonly Action<AuditEvent>? _traceSink;
private readonly Action<bool, bool>? _siteTruncationSink;
private readonly IReadOnlySet<string>? _permittedSiteIdentifiers;
private readonly bool _captureBodies;
private readonly ILogger? _logger;
@@ -301,6 +310,7 @@ public class RouteTarget
/// <param name="permittedSiteIdentifiers">Arch-review F2: site identifiers this call may reach; null = unscoped.</param>
/// <param name="captureBodies">Arch-review F11: false omits the request/response summary on the <c>RouteCall</c> row.</param>
/// <param name="logger">Arch-review F8: logs a Warning when the best-effort <c>RouteCall</c> audit write faults; null = silent.</param>
/// <param name="siteTruncationSink">Gitea #54: receives a routed reply's site-side truncation flags; null = none.</param>
internal RouteTarget(
string instanceCode,
IInstanceLocator instanceLocator,
@@ -314,7 +324,8 @@ public class RouteTarget
Action<AuditEvent>? traceSink = null,
IReadOnlySet<string>? permittedSiteIdentifiers = null,
bool captureBodies = true,
ILogger? logger = null)
ILogger? logger = null,
Action<bool, bool>? siteTruncationSink = null)
{
_permittedSiteIdentifiers = permittedSiteIdentifiers;
_instanceCode = instanceCode;
@@ -327,6 +338,7 @@ public class RouteTarget
_nodeIdentity = nodeIdentity;
_trace = trace;
_traceSink = traceSink;
_siteTruncationSink = siteTruncationSink;
_captureBodies = captureBodies;
_logger = logger;
}
@@ -382,6 +394,7 @@ public class RouteTarget
EmitRouteCall(scriptName, normalized, startedUtc, sw.ElapsedMilliseconds, response.Success,
response.ReturnValue, response.ErrorMessage, response.ChildExecutionId, response.TraceEvents);
ReportSiteTruncation(response);
if (!response.Success)
{
@@ -463,6 +476,23 @@ public class RouteTarget
}
}
/// <summary>
/// Gitea #54: forwards a routed reply's site-side truncation flags to the sink. Best-effort —
/// a misbehaving sink never fails the routed call.
/// </summary>
private void ReportSiteTruncation(RouteToCallResponse response)
{
if (_siteTruncationSink is null || !(response.Truncated || response.LogsTruncated)) return;
try
{
_siteTruncationSink(response.Truncated, response.LogsTruncated);
}
catch (Exception)
{
// Best-effort, like the trace sink.
}
}
private static string? TraceJson(object? value)
{
if (value is null) return null;