feat(auditlog): thread ParentExecutionId through S&F for retry-loop cached rows

The store-and-forward retry loop emits the per-attempt and terminal cached
audit rows (ApiCallCached/DbWriteCached Attempted, CachedResolve) via
CachedCallLifecycleBridge from a CachedCallAttemptContext, not from the
script context. The ExecutionId rollout (Task 4) already threaded ExecutionId
and SourceScript through this path; ParentExecutionId — the spawning
inbound-API request's ExecutionId — was not, so those retry-loop rows had
ParentExecutionId = null even for an inbound-API-routed run.

Thread it additively as a sibling at every carry point ExecutionId passes
through:

- StoreAndForwardMessage gains ParentExecutionId (Guid?).
- StoreAndForwardStorage adds a nullable parent_execution_id column via the
  same idempotent PRAGMA-probed ALTER TABLE migration; rows persisted by an
  older build read back null (back-compat). The defensive Guid.TryParse read
  helper (ParseExecutionId) is renamed ParseGuidColumn and reused for both
  columns so a corrupt value cannot abort the retry sweep.
- StoreAndForwardService.EnqueueAsync gains an optional parentExecutionId
  param, stamped onto the buffered message and surfaced on the
  CachedCallAttemptContext built in the retry loop.
- CachedCallAttemptContext gains ParentExecutionId.
- CachedCallLifecycleBridge.BuildPacket sets AuditEvent.ParentExecutionId
  from the context, beside the existing ExecutionId.
- IExternalSystemClient.CachedCallAsync / IDatabaseGateway.CachedWriteAsync
  gain an optional parentExecutionId param; ScriptRuntimeContext's CachedCall
  / CachedWrite helpers pass _parentExecutionId.

All threading is additive — ParentExecutionId is Guid? everywhere, null for
non-routed runs, and old buffered S&F rows still deserialize with the new
field null.
This commit is contained in:
Joseph Doherty
2026-05-21 17:58:11 -04:00
parent 150ba5e63f
commit c00603e2a4
15 changed files with 581 additions and 51 deletions

View File

@@ -86,7 +86,8 @@ public class DatabaseGateway : IDatabaseGateway
CancellationToken cancellationToken = default,
TrackedOperationId? trackedOperationId = null,
Guid? executionId = null,
string? sourceScript = null)
string? sourceScript = null,
Guid? parentExecutionId = null)
{
var definition = await ResolveConnectionAsync(connectionName, cancellationToken);
if (definition == null)
@@ -132,7 +133,12 @@ public class DatabaseGateway : IDatabaseGateway
// the retry-loop cached-write audit rows carry the same provenance
// the script-side cached rows do.
executionId: executionId,
sourceScript: sourceScript);
sourceScript: sourceScript,
// Audit Log #23 (ParentExecutionId Task 6): thread the spawning
// inbound-API request's ExecutionId onto the buffered row so the
// retry-loop cached-write audit rows correlate back to the
// cross-execution chain. Null for a non-routed run.
parentExecutionId: parentExecutionId);
}
/// <summary>

View File

@@ -88,7 +88,8 @@ public class ExternalSystemClient : IExternalSystemClient
CancellationToken cancellationToken = default,
TrackedOperationId? trackedOperationId = null,
Guid? executionId = null,
string? sourceScript = null)
string? sourceScript = null,
Guid? parentExecutionId = null)
{
var (system, method) = await ResolveSystemAndMethodAsync(systemName, methodName, cancellationToken);
if (system == null || method == null)
@@ -152,7 +153,12 @@ public class ExternalSystemClient : IExternalSystemClient
// buffered row so the retry-loop cached-call audit rows carry
// the same provenance the script-side cached rows do.
executionId: executionId,
sourceScript: sourceScript);
sourceScript: sourceScript,
// Audit Log #23 (ParentExecutionId Task 6): thread the spawning
// inbound-API request's ExecutionId onto the buffered row so
// the retry-loop cached-call audit rows correlate back to the
// cross-execution chain. Null for a non-routed run.
parentExecutionId: parentExecutionId);
return new ExternalCallResult(true, null, null, WasBuffered: true);
}