feat(audit): M5.4 ParentExecutionId tag-cascade for alarm + nested calls (T4)

This commit is contained in:
Joseph Doherty
2026-06-16 21:42:14 -04:00
parent 209f368cb5
commit 20760014c2
4 changed files with 398 additions and 8 deletions
@@ -247,6 +247,59 @@ public class ScriptRuntimeContext
_siteEventLogger = siteEventLogger;
}
/// <summary>
/// Audit Log #23 (M5.4): this run's own per-execution id. Exposed so a
/// nested <c>Scripts.CallShared</c> can record it as the spawned shared
/// script's <c>ParentExecutionId</c>, forming a true execution tree.
/// </summary>
internal Guid ExecutionId => _executionId;
/// <summary>
/// Audit Log #23 (M5.4): the spawning execution's id for this run (null for
/// a root run). Exposed for test assertions on the execution tree.
/// </summary>
internal Guid? ParentExecutionId => _parentExecutionId;
/// <summary>
/// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): builds a child
/// <see cref="ScriptRuntimeContext"/> for an inline <c>Scripts.CallShared</c>
/// invocation. The shared script runs inline (no actor hop) but is modelled
/// as its OWN execution node in the audit tree: it mints a fresh
/// <see cref="_executionId"/> and records THIS run's <see cref="_executionId"/>
/// as its <c>ParentExecutionId</c>, so <c>B → CallShared(C)</c> yields
/// <c>C.ParentExecutionId == B.ExecutionId</c>. Every other dependency
/// (actors, gateways, audit writer, site id, source node, call-depth) is
/// carried over verbatim from this context.
/// </summary>
/// <param name="childCallDepth">The recursion depth of the shared-script call.</param>
internal ScriptRuntimeContext CreateChildContextForSharedScript(int childCallDepth)
{
return new ScriptRuntimeContext(
_instanceActor,
_self,
_sharedScriptLibrary,
childCallDepth,
_maxCallDepth,
_askTimeout,
_instanceName,
_logger,
_externalSystemClient,
_databaseGateway,
_storeAndForward,
_siteCommunicationActor,
_siteId,
_sourceScript,
_auditWriter,
_operationTrackingStore,
_cachedForwarder,
// Fresh execution id for the shared-script run (omit so the ctor mints one)…
executionId: null,
// …parented to THIS run's execution id (the spawner).
parentExecutionId: _executionId,
sourceNode: _sourceNode,
siteEventLogger: _siteEventLogger);
}
/// <summary>
/// M2.12 (#25): fire-and-forget emission of a <c>script</c> Error site event
/// for a recursion-limit violation. Mirrors the call shape used by
@@ -366,7 +419,14 @@ public class ScriptRuntimeContext
scriptName,
ScriptArgs.Normalize(parameters),
nextDepth,
correlationId);
correlationId,
// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the child
// script run is a NEW execution spawned BY this run. Its parent is
// THIS run's own ExecutionId — NOT the inherited _parentExecutionId.
// So A → CallScript(B) yields B.ParentExecutionId == A.ExecutionId,
// building a true multi-level execution tree rather than flattening
// every nested call under the original inbound spawner.
ParentExecutionId: _executionId);
// Ask the Instance Actor, which routes to the appropriate Script Actor
var result = await _instanceActor.Ask<ScriptCallResult>(request, _askTimeout);
@@ -526,8 +586,14 @@ public class ScriptRuntimeContext
throw new InvalidOperationException(msg);
}
// Audit Log #23 (M5.4 — ParentExecutionId tag-cascade): the shared
// script runs inline, but is modelled as its OWN execution node — a
// child context mints a fresh ExecutionId parented to the caller's
// ExecutionId, so its audit rows chain under the calling run.
var childContext = _context.CreateChildContextForSharedScript(nextDepth);
return await _library.ExecuteAsync(
scriptName, _context, ScriptArgs.Normalize(parameters), cancellationToken);
scriptName, childContext, ScriptArgs.Normalize(parameters), cancellationToken);
}
}