fix(audit): populate ParentExecutionId on alarm-triggered script runs
M5.4 T4 threaded a `parentExecutionId` parameter through
AlarmActor.SpawnAlarmExecution → AlarmExecutionActor → ScriptRuntimeContext,
but every call site passed null — so alarm on-trigger runs were silently always
execution-tree roots, contradicting the "tag-cascade coverage is complete"
claim in CLAUDE.md and Component-AuditLog.md.
Source the id where a spawner genuinely exists: a static attribute write issued
by a site script (`Instance.SetAttribute`) or by an inbound API request
(`Route.To(...).SetAttributes(...)`, whose ParentExecutionId was already carried
to the site and then dropped). The id rides site-locally through three additive,
nullable fields — no wire, proto or central schema change:
ScriptRuntimeContext.SetAttribute / RouteToSetAttributesRequest.ParentExecutionId
→ SetStaticAttributeCommand.SourceExecutionId
→ AttributeValueChanged.SourceExecutionId (InstanceActor static-write path)
→ AlarmActor.SpawnAlarmExecution → AlarmExecutionActor → ScriptRuntimeContext
All four computed trigger types participate. Expression triggers evaluate off
the dispatcher, so the writer of the newest value folded into the snapshot is
captured *with* the snapshot and echoed home on ExpressionEvalResult /
ExpressionEvalFailed — a change arriving mid-flight cannot mis-attribute the
raise.
Deliberately still roots (documented, not deferred): alarms fired by Data
Connection Layer values (external device data has no spawning execution — this
includes the device echo of a script write to a *data-sourced* attribute, so
only static writes cascade), and ScriptActor value-change/conditional/
expression/timer trigger runs (a timer tick has no spawner; a WhileTrue/interval
run has no single identifiable write).
Tests: new SiteRuntime.Tests/Actors/AlarmCascadeParentExecutionTests pins all
three hops — SetAttribute stamps the run's ExecutionId, InstanceActor publishes
it on the change (and publishes null when absent), and ValueMatch/HiLo/
Expression alarms parent the on-trigger run to the writer while a DCL-originated
change leaves it a root.
Docs: CLAUDE.md and Component-AuditLog.md corrected from "complete" to the true
behaviour; Component-SiteRuntime.md gains an "Audit correlation of an on-trigger
run" section with the hop table and the by-design root cases.
This commit is contained in:
@@ -4,12 +4,28 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
|
||||
/// Command to set a static attribute value on an Instance Actor.
|
||||
/// Updates in-memory state and persists the override to SQLite.
|
||||
/// </summary>
|
||||
/// <param name="CorrelationId">Per-operation correlation id.</param>
|
||||
/// <param name="InstanceUniqueName">Unique name of the target instance.</param>
|
||||
/// <param name="AttributeName">Canonical name of the attribute to write.</param>
|
||||
/// <param name="Value">Canonical string form of the new value.</param>
|
||||
/// <param name="Timestamp">UTC timestamp of the command.</param>
|
||||
/// <param name="SourceExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the <c>ExecutionId</c> of the
|
||||
/// execution issuing this write — a site script run
|
||||
/// (<c>ScriptRuntimeContext.SetAttribute</c>) or the inbound API request behind a
|
||||
/// <c>Route.To(...).SetAttributes(...)</c>. Additive and nullable; <c>null</c>
|
||||
/// when the write has no audited originating execution (central Test Run,
|
||||
/// deployment tooling, tests). The Instance Actor stamps it onto the resulting
|
||||
/// <c>AttributeValueChanged</c> so an alarm the write trips can record it as the
|
||||
/// on-trigger script run's <c>ParentExecutionId</c>.
|
||||
/// </param>
|
||||
public record SetStaticAttributeCommand(
|
||||
string CorrelationId,
|
||||
string InstanceUniqueName,
|
||||
string AttributeName,
|
||||
string Value,
|
||||
DateTimeOffset Timestamp);
|
||||
DateTimeOffset Timestamp,
|
||||
Guid? SourceExecutionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Response confirming that a static attribute was set.
|
||||
|
||||
@@ -1,9 +1,43 @@
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
|
||||
/// <summary>
|
||||
/// A single attribute value change on a deployed instance — published to the
|
||||
/// site-wide stream and routed to the interested child actors (Script Actors,
|
||||
/// Alarm Actors) of the owning Instance Actor.
|
||||
/// </summary>
|
||||
/// <param name="InstanceUniqueName">Unique name of the instance the attribute belongs to.</param>
|
||||
/// <param name="AttributePath">Path-qualified canonical name of the attribute.</param>
|
||||
/// <param name="AttributeName">Canonical name of the attribute.</param>
|
||||
/// <param name="Value">The new value.</param>
|
||||
/// <param name="Quality">Quality of the new value (<c>Good</c>/<c>Bad</c>/<c>Uncertain</c>).</param>
|
||||
/// <param name="Timestamp">UTC timestamp of the change.</param>
|
||||
/// <param name="SourceExecutionId">
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the <c>ExecutionId</c> of the
|
||||
/// script execution / inbound API request whose write produced this change, or
|
||||
/// <c>null</c> when the change did not originate from an audited execution —
|
||||
/// which is the case for every value that arrives from the Data Connection
|
||||
/// Layer (external device data), for deployment-time seeding, and for the
|
||||
/// confirmed value of a data-sourced write (the device echo arrives on the
|
||||
/// subscription long after the writing execution ended).
|
||||
///
|
||||
/// <para>
|
||||
/// The Alarm Actor records this id as the <c>ParentExecutionId</c> of the
|
||||
/// on-trigger script run the change fires, so a script-initiated write that
|
||||
/// trips an alarm chains under the writing execution in the audit tree.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Deliberately site-local: the field is NOT projected onto the gRPC
|
||||
/// <c>DebugAttributeValueDto</c> / <c>SiteStreamEvent</c> wire shapes — the
|
||||
/// cascade is resolved entirely inside the site node that owns the Instance
|
||||
/// Actor, and central reads the linkage from the audit rows instead.
|
||||
/// </para>
|
||||
/// </param>
|
||||
public record AttributeValueChanged(
|
||||
string InstanceUniqueName,
|
||||
string AttributePath,
|
||||
string AttributeName,
|
||||
object? Value,
|
||||
string Quality,
|
||||
DateTimeOffset Timestamp) : ISiteStreamEvent;
|
||||
DateTimeOffset Timestamp,
|
||||
Guid? SourceExecutionId = null) : ISiteStreamEvent;
|
||||
|
||||
@@ -102,6 +102,18 @@ public class AlarmActor : ReceiveActor
|
||||
private bool _evalInFlight;
|
||||
private bool _evalPending;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the
|
||||
/// <see cref="AttributeValueChanged.SourceExecutionId"/> of the most recent
|
||||
/// change folded into <see cref="_attributeSnapshot"/>. Expression triggers
|
||||
/// evaluate a whole snapshot off the dispatcher, so there is no single
|
||||
/// "triggering change" in scope when the boolean result comes back — this
|
||||
/// field is the latest writer, captured into the in-flight evaluation by
|
||||
/// <see cref="StartExpressionEvaluation"/> and echoed home on
|
||||
/// <see cref="ExpressionEvalResult"/>. Touched only on the actor thread.
|
||||
/// </summary>
|
||||
private Guid? _latestSourceExecutionId;
|
||||
|
||||
/// <summary>
|
||||
/// The exact dictionary instance this actor was seeded from
|
||||
/// at construction. The Instance Actor must pass a private snapshot here, not
|
||||
@@ -110,6 +122,18 @@ public class AlarmActor : ReceiveActor
|
||||
/// </summary>
|
||||
internal IReadOnlyDictionary<string, object?>? SeedAttributesReference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (ParentExecutionId tag-cascade): the
|
||||
/// <c>parentExecutionId</c> handed to the most recently spawned
|
||||
/// <see cref="AlarmExecutionActor"/> — i.e. the execution whose attribute
|
||||
/// write fired this alarm, or <c>null</c> when the firing change came from
|
||||
/// the Data Connection Layer (external data has no spawning execution).
|
||||
/// The spawned actor builds its own <see cref="ScriptRuntimeContext"/>
|
||||
/// internally, so this is exposed for regression coverage of the cascade
|
||||
/// contract (mirrors <see cref="SeedAttributesReference"/>).
|
||||
/// </summary>
|
||||
internal Guid? LastOnTriggerParentExecutionId { get; private set; }
|
||||
|
||||
// Rate of change tracking
|
||||
private readonly Queue<(DateTimeOffset Timestamp, double Value)> _rateOfChangeWindow = new();
|
||||
private readonly TimeSpan _rateOfChangeWindowDuration;
|
||||
@@ -241,6 +265,10 @@ public class AlarmActor : ReceiveActor
|
||||
if (_triggerType == AlarmTriggerType.Expression)
|
||||
{
|
||||
_attributeSnapshot[changed.AttributeName] = changed.Value;
|
||||
// (ParentExecutionId tag-cascade): remember which
|
||||
// execution (if any) wrote the newest value folded into the snapshot,
|
||||
// so the off-dispatcher evaluation can attribute its raise to it.
|
||||
_latestSourceExecutionId = changed.SourceExecutionId;
|
||||
}
|
||||
else if (!IsMonitoredAttribute(changed.AttributeName))
|
||||
{
|
||||
@@ -252,7 +280,7 @@ public class AlarmActor : ReceiveActor
|
||||
{
|
||||
if (_triggerType == AlarmTriggerType.HiLo)
|
||||
{
|
||||
HandleHiLoTransition(EvaluateHiLo(changed.Value));
|
||||
HandleHiLoTransition(EvaluateHiLo(changed.Value), changed.SourceExecutionId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -274,7 +302,7 @@ public class AlarmActor : ReceiveActor
|
||||
_ => false
|
||||
};
|
||||
|
||||
ApplyTriggeredState(isTriggered);
|
||||
ApplyTriggeredState(isTriggered, changed.SourceExecutionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -292,7 +320,14 @@ public class AlarmActor : ReceiveActor
|
||||
/// Expression path (via <see cref="HandleExpressionEvalResult"/>). Edge state
|
||||
/// (<see cref="_currentState"/>) is touched only on the actor thread.
|
||||
/// </summary>
|
||||
private void ApplyTriggeredState(bool isTriggered)
|
||||
/// <param name="isTriggered">The evaluated truth value of the alarm condition.</param>
|
||||
/// <param name="sourceExecutionId">
|
||||
/// (ParentExecutionId tag-cascade): the execution that
|
||||
/// wrote the value which produced this evaluation, recorded as the
|
||||
/// on-trigger script run's <c>ParentExecutionId</c>. Null when the value
|
||||
/// came from the DCL (external data has no spawning execution).
|
||||
/// </param>
|
||||
private void ApplyTriggeredState(bool isTriggered, Guid? sourceExecutionId = null)
|
||||
{
|
||||
if (isTriggered && _currentState == AlarmState.Normal)
|
||||
{
|
||||
@@ -313,7 +348,7 @@ public class AlarmActor : ReceiveActor
|
||||
// Spawn AlarmExecutionActor if on-trigger script defined
|
||||
if (_onTriggerCompiledScript != null)
|
||||
{
|
||||
SpawnAlarmExecution(AlarmLevel.None, _priority, string.Empty);
|
||||
SpawnAlarmExecution(AlarmLevel.None, _priority, string.Empty, sourceExecutionId);
|
||||
}
|
||||
}
|
||||
else if (!isTriggered && _currentState == AlarmState.Active)
|
||||
@@ -339,7 +374,13 @@ public class AlarmActor : ReceiveActor
|
||||
/// edge (i.e., when entering an alarm band from the normal band) — not on
|
||||
/// level escalations like Hi→HiHi or Low→LowLow.
|
||||
/// </summary>
|
||||
private void HandleHiLoTransition(AlarmLevel newLevel)
|
||||
/// <param name="newLevel">The newly evaluated HiLo band.</param>
|
||||
/// <param name="sourceExecutionId">
|
||||
/// (ParentExecutionId tag-cascade): the execution that
|
||||
/// wrote the value driving this transition; recorded as the on-trigger
|
||||
/// script run's <c>ParentExecutionId</c>. Null for DCL-originated values.
|
||||
/// </param>
|
||||
private void HandleHiLoTransition(AlarmLevel newLevel, Guid? sourceExecutionId = null)
|
||||
{
|
||||
if (newLevel == _currentLevel) return;
|
||||
|
||||
@@ -383,7 +424,7 @@ public class AlarmActor : ReceiveActor
|
||||
&& newLevel != AlarmLevel.None
|
||||
&& _onTriggerCompiledScript != null)
|
||||
{
|
||||
SpawnAlarmExecution(newLevel, priority, message);
|
||||
SpawnAlarmExecution(newLevel, priority, message, sourceExecutionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,6 +584,11 @@ public class AlarmActor : ReceiveActor
|
||||
var snapshot = new Dictionary<string, object?>(_attributeSnapshot); // point-in-time copy, actor thread
|
||||
var expression = _compiledTriggerExpression;
|
||||
var self = Self;
|
||||
// (ParentExecutionId tag-cascade): capture the writer of
|
||||
// the newest value in THIS snapshot alongside the snapshot itself, so a
|
||||
// later change arriving while the evaluation is in flight cannot
|
||||
// mis-attribute the raise this evaluation produces.
|
||||
var sourceExecutionId = _latestSourceExecutionId;
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
try
|
||||
@@ -568,8 +614,8 @@ public class AlarmActor : ReceiveActor
|
||||
}
|
||||
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach,
|
||||
_scheduler ?? ScriptExecutionScheduler.Shared(_options)).Unwrap().PipeTo(self,
|
||||
success: r => new ExpressionEvalResult(r),
|
||||
failure: ex => new ExpressionEvalFailed(ex));
|
||||
success: r => new ExpressionEvalResult(r, sourceExecutionId),
|
||||
failure: ex => new ExpressionEvalFailed(ex, sourceExecutionId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -582,7 +628,7 @@ public class AlarmActor : ReceiveActor
|
||||
_evalInFlight = false;
|
||||
try
|
||||
{
|
||||
ApplyTriggeredState(msg.Result);
|
||||
ApplyTriggeredState(msg.Result, msg.SourceExecutionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -607,7 +653,7 @@ public class AlarmActor : ReceiveActor
|
||||
_logger.LogError(msg.Cause,
|
||||
"Alarm {Alarm} trigger-expression evaluation task faulted on {Instance}; treated as false",
|
||||
_alarmName, _instanceName);
|
||||
HandleExpressionEvalResult(new ExpressionEvalResult(false));
|
||||
HandleExpressionEvalResult(new ExpressionEvalResult(false, msg.SourceExecutionId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -667,13 +713,14 @@ public class AlarmActor : ReceiveActor
|
||||
/// <param name="priority">The firing alarm priority.</param>
|
||||
/// <param name="message">The firing alarm message.</param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// The execution id of
|
||||
/// the context that fired this alarm, recorded as the on-trigger script run's
|
||||
/// <c>ParentExecutionId</c> so the alarm-triggered run chains under its firing
|
||||
/// context in the audit tree. The alarm subsystem currently has no Guid-typed
|
||||
/// firing id, so the only call sites pass <c>null</c> (the on-trigger run is a
|
||||
/// root). The parameter exists so a future firing-id can flow without
|
||||
/// touching the actor wiring.
|
||||
/// (ParentExecutionId tag-cascade): the <c>ExecutionId</c>
|
||||
/// of the execution whose attribute write fired this alarm — a site script
|
||||
/// run (<c>Instance.SetAttribute</c>) or the inbound API request behind a
|
||||
/// <c>Route.To(...).SetAttributes(...)</c> — recorded as the on-trigger
|
||||
/// script run's <c>ParentExecutionId</c> so the alarm-triggered run chains
|
||||
/// under its firing execution in the audit tree. <c>null</c> when the firing
|
||||
/// value arrived from the Data Connection Layer: external device data has no
|
||||
/// spawning execution, so that on-trigger run is correctly a tree ROOT.
|
||||
/// </param>
|
||||
private void SpawnAlarmExecution(
|
||||
AlarmLevel level, int priority, string message, Guid? parentExecutionId = null)
|
||||
@@ -682,6 +729,10 @@ public class AlarmActor : ReceiveActor
|
||||
|
||||
var executionId = $"{_alarmName}-alarm-exec-{_executionCounter++}";
|
||||
|
||||
// Record what the on-trigger run was parented to (null = root); read by
|
||||
// the tag-cascade regression tests, which cannot see inside the child.
|
||||
LastOnTriggerParentExecutionId = parentExecutionId;
|
||||
|
||||
// The on-trigger script body runs on the dedicated
|
||||
// ScriptExecutionScheduler, not the shared .NET thread pool.
|
||||
var props = Props.Create(() => new AlarmExecutionActor(
|
||||
@@ -697,7 +748,7 @@ public class AlarmActor : ReceiveActor
|
||||
_logger,
|
||||
// Per-script timeout from the on-trigger script (null = global).
|
||||
_onTriggerExecutionTimeoutSeconds,
|
||||
// The firing context's execution id (null today).
|
||||
// The firing execution's id — null for DCL-originated changes.
|
||||
parentExecutionId,
|
||||
// Scheduler seam (#18): share this alarm's scheduler override with the
|
||||
// spawned on-trigger script body (null = process-wide shared).
|
||||
@@ -829,9 +880,12 @@ public class AlarmActor : ReceiveActor
|
||||
/// <summary>
|
||||
/// Piped back to self from an off-dispatcher trigger-expression evaluation (P1);
|
||||
/// carries the boolean truth value so the Normal↔Active transition runs on the
|
||||
/// actor thread.
|
||||
/// actor thread, plus the <c>SourceExecutionId</c> captured with the evaluated
|
||||
/// snapshot (ParentExecutionId tag-cascade) so a raise is attributed
|
||||
/// to the execution that actually wrote the value it evaluated — not to a
|
||||
/// later change that arrived while the evaluation was in flight.
|
||||
/// </summary>
|
||||
private sealed record ExpressionEvalResult(bool Result);
|
||||
private sealed record ExpressionEvalResult(bool Result, Guid? SourceExecutionId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Piped back to self when the off-dispatcher evaluation TASK itself faults
|
||||
@@ -839,8 +893,10 @@ public class AlarmActor : ReceiveActor
|
||||
/// during shutdown) — the inner body's catch never sees that. Without this
|
||||
/// mapping the actor receives an unhandled Status.Failure and _evalInFlight
|
||||
/// stays true forever, permanently parking the expression trigger (N2).
|
||||
/// The captured <c>SourceExecutionId</c> rides along so the synthesized
|
||||
/// false result keeps the same attribution the successful path would have had.
|
||||
/// </summary>
|
||||
internal sealed record ExpressionEvalFailed(Exception Cause);
|
||||
internal sealed record ExpressionEvalFailed(Exception Cause, Guid? SourceExecutionId = null);
|
||||
}
|
||||
|
||||
internal enum RateOfChangeDirection { Either, Rising, Falling }
|
||||
|
||||
@@ -30,12 +30,13 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
/// <param name="logger">Logger for execution diagnostics.</param>
|
||||
/// <param name="executionTimeoutSeconds">The on-trigger script's per-script execution timeout in seconds. Null or non-positive falls back to the global <see cref="SiteRuntimeOptions.ScriptExecutionTimeoutSeconds"/>.</param>
|
||||
/// <param name="parentExecutionId">
|
||||
/// ParentExecutionId tag-cascade: the execution id of
|
||||
/// the context that fired this alarm, threaded into the on-trigger script's
|
||||
/// <see cref="ScriptRuntimeContext"/> as its <c>ParentExecutionId</c> so the
|
||||
/// alarm-triggered run chains under its firing context. Null today (no
|
||||
/// Guid-typed firing id exists yet) — the run is a root, but the plumbing
|
||||
/// is in place for a future firing id.
|
||||
/// ParentExecutionId tag-cascade: the <c>ExecutionId</c> of
|
||||
/// the execution whose attribute write fired this alarm, threaded into the
|
||||
/// on-trigger script's <see cref="ScriptRuntimeContext"/> as its
|
||||
/// <c>ParentExecutionId</c> so the alarm-triggered run chains under its
|
||||
/// firing execution. Null when the firing value came from the Data
|
||||
/// Connection Layer (external data has no spawning execution) — that
|
||||
/// on-trigger run is a tree root.
|
||||
/// </param>
|
||||
public AlarmExecutionActor(
|
||||
string alarmName,
|
||||
@@ -115,9 +116,8 @@ public class AlarmExecutionActor : ReceiveActor
|
||||
// ParentExecutionId tag-cascade: the
|
||||
// alarm on-trigger run mints its own fresh ExecutionId (the
|
||||
// ctor's `?? NewGuid()` fallback) and records the firing
|
||||
// context's id as its ParentExecutionId — null today, so the
|
||||
// run is a root, but the plumbing exists for a future
|
||||
// firing id.
|
||||
// execution's id as its ParentExecutionId — null (a root)
|
||||
// only when the firing value came from the DCL.
|
||||
parentExecutionId: parentExecutionId,
|
||||
// WaitForAttribute (spec §4.4): thread the alarm on-trigger
|
||||
// script's per-script execution-timeout token so a
|
||||
|
||||
@@ -1826,7 +1826,14 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
||||
var asks = request.AttributeValues
|
||||
.Select(kvp => instanceActor.Ask<SetStaticAttributeResponse>(
|
||||
new SetStaticAttributeCommand(
|
||||
correlationId, request.InstanceUniqueName, kvp.Key, kvp.Value, DateTimeOffset.UtcNow),
|
||||
correlationId, request.InstanceUniqueName, kvp.Key, kvp.Value, DateTimeOffset.UtcNow,
|
||||
// (ParentExecutionId tag-cascade): the routed
|
||||
// write's originating execution is the inbound API request
|
||||
// that issued Route.To(...).SetAttributes(...) — already
|
||||
// carried on the request as ParentExecutionId. Stamping it
|
||||
// here lets an alarm this write trips chain its on-trigger
|
||||
// script run under the inbound execution.
|
||||
SourceExecutionId: request.ParentExecutionId),
|
||||
TimeSpan.FromSeconds(30)))
|
||||
.ToArray();
|
||||
|
||||
|
||||
@@ -462,7 +462,12 @@ public class InstanceActor : ReceiveActor
|
||||
command.AttributeName,
|
||||
command.Value,
|
||||
"Good",
|
||||
DateTimeOffset.UtcNow);
|
||||
DateTimeOffset.UtcNow,
|
||||
// (ParentExecutionId tag-cascade): carry the writing
|
||||
// execution's id onto the change so a child Alarm Actor this write
|
||||
// trips can chain its on-trigger script run under the writer. Null for
|
||||
// writes with no audited origin (central Test Run, deployment tooling).
|
||||
SourceExecutionId: command.SourceExecutionId);
|
||||
|
||||
PublishAndNotifyChildren(changed);
|
||||
|
||||
|
||||
@@ -151,10 +151,13 @@ public class ScriptRuntimeContext
|
||||
/// <summary>
|
||||
/// (ParentExecutionId): the spawning execution's
|
||||
/// <see cref="_executionId"/> when this script run was spawned by another
|
||||
/// execution — for an inbound-API-routed call this is the inbound request's
|
||||
/// per-request execution id. <c>null</c> for normal (tag-change /
|
||||
/// timer-triggered) runs and nested <c>CallScript</c> invocations. The
|
||||
/// routed script still mints its OWN fresh <see cref="_executionId"/>; this
|
||||
/// execution — the inbound request's per-request execution id for an
|
||||
/// inbound-API-routed call, the caller's id for a nested
|
||||
/// <c>CallScript</c>/<c>CallShared</c>, and the writing execution's id for an
|
||||
/// alarm on-trigger run fired by a script- or inbound-API-initiated attribute
|
||||
/// write. <c>null</c> for genuinely top-level runs: timer-triggered scripts,
|
||||
/// script value-change triggers, and alarms fired by DCL (external) data. The
|
||||
/// spawned script still mints its OWN fresh <see cref="_executionId"/>; this
|
||||
/// field records the spawner so a spawned execution's audit rows can point
|
||||
/// back at the execution that spawned it.
|
||||
/// </summary>
|
||||
@@ -514,7 +517,12 @@ public class ScriptRuntimeContext
|
||||
{
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var command = new SetStaticAttributeCommand(
|
||||
correlationId, _instanceName, attributeName, value, DateTimeOffset.UtcNow);
|
||||
correlationId, _instanceName, attributeName, value, DateTimeOffset.UtcNow,
|
||||
// (ParentExecutionId tag-cascade): stamp THIS run's own
|
||||
// ExecutionId as the write's originating execution. The Instance Actor
|
||||
// carries it onto the published AttributeValueChanged, so an alarm this
|
||||
// write trips records this run as the on-trigger script's parent.
|
||||
SourceExecutionId: _executionId);
|
||||
|
||||
// Ask — mutation serialized through the Instance Actor mailbox; the reply
|
||||
// carries the device-write outcome for data-connected attributes.
|
||||
|
||||
Reference in New Issue
Block a user