feat(site-runtime): outbound API/DB/notification audit rows ride the flow-trace collector

ScriptRuntimeContext caches a trace sink (null on an untraced run, so the
production path is byte-identical) and threads it into the ExternalSystem /
Database / Notify helpers and AuditingDbConnection→AuditingDbCommand (onAudit).
All eight outbound emit sites hand the SAME AuditEvent instance to the sink
first, then to the writer/forwarder. FlowTraceCollector gains Completed +
Complete(): ScriptRunLauncher closes the collector where it builds the reply
snapshot, so un-awaited calls finishing after the run returned never mutate a
list the reply was built from. Cap (500) unchanged. Plan 2026-09-06 D5.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 01:51:19 -04:00
parent 865c4be651
commit f394f58405
6 changed files with 464 additions and 19 deletions
@@ -48,6 +48,14 @@ internal sealed class AuditingDbCommand : DbCommand
/// </summary>
private readonly Guid? _parentExecutionId;
/// <summary>
/// Flow Debugger (plan 2026-09-06 D5): optional observer invoked with the SAME
/// <see cref="AuditEvent"/> instance <see cref="EmitAudit"/> hands the writer,
/// collector-first — the run's inline trace collector. Null when untraced, which
/// leaves the emit path byte-identical.
/// </summary>
private readonly Action<AuditEvent>? _onAudit;
private readonly ILogger _logger;
private DbConnection? _wrappingConnection;
@@ -63,6 +71,7 @@ internal sealed class AuditingDbCommand : DbCommand
/// <param name="logger">Logger for diagnostics and warnings.</param>
/// <param name="executionId">Unique identifier for this script execution.</param>
/// <param name="parentExecutionId">Optional identifier of the parent execution (for routed calls).</param>
/// <param name="onAudit">Optional observer of every emitted <see cref="AuditEvent"/> (the Flow Debugger trace collector).</param>
// Parameter ordering: executionId sits immediately after the ILogger,
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
// DatabaseHelper, AuditingDbConnection). parentExecutionId is a trailing
@@ -76,7 +85,8 @@ internal sealed class AuditingDbCommand : DbCommand
string? sourceScript,
ILogger logger,
Guid executionId,
Guid? parentExecutionId = null)
Guid? parentExecutionId = null,
Action<AuditEvent>? onAudit = null)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
@@ -87,6 +97,7 @@ internal sealed class AuditingDbCommand : DbCommand
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_executionId = executionId;
_parentExecutionId = parentExecutionId;
_onAudit = onAudit;
}
// -- Forwarded surface ------------------------------------------------
@@ -410,6 +421,8 @@ internal sealed class AuditingDbCommand : DbCommand
try
{
// Flow Debugger: collector first (same instance), then the writer.
_onAudit?.Invoke(evt);
var writeTask = _auditWriter.WriteAsync(evt, CancellationToken.None);
if (!writeTask.IsCompleted)
{