feat(site-runtime): InstanceActor trace arm stamps the device echo of a traced write; Trace rides on published changes
Flow Debugger (design 2026-09-05 §5.3): a traced SetStaticAttributeCommand on a data-sourced attribute records (attribute → writer, expiry) for TraceArmWindowSeconds (new option, default 30, validated > 0); the next DCL echo for that attribute is published with the writer's SourceExecutionId + Trace whatever its value, and the arm is consumed once. Static writes publish Trace directly. Untraced writes never arm (pinned). Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
This commit is contained in:
@@ -172,6 +172,15 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
// DCL manager actor reference for subscribing to tag values
|
||||
private readonly IActorRef? _dclManager;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger trace arm (design 2026-09-05 §5.3): attribute → (writer's ExecutionId,
|
||||
/// expiry) recorded when a TRACED script write targets a data-sourced attribute. The next
|
||||
/// DCL value change for that attribute is published stamped with the writer's id and
|
||||
/// <c>Trace</c>, and the arm is consumed. Bounded by the attribute count (one entry per
|
||||
/// attribute, newest wins); no sweep — an unconsumed arm is overwritten or expires in place.
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, (Guid SourceExecutionId, DateTimeOffset ExpiresAt)> _traceArms = new();
|
||||
// Maps each tag path to every attribute canonical name that references it.
|
||||
// A tag path can back more than one attribute (e.g. two composed modules
|
||||
// whose members reference the same PLC node), so a tag value update must
|
||||
@@ -592,7 +601,10 @@ public class InstanceActor : ReceiveActor
|
||||
// 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);
|
||||
SourceExecutionId: command.SourceExecutionId,
|
||||
// Flow Debugger (design §5.2): a traced write's change carries the flag so the
|
||||
// value-change run it spawns is traced and parented to the writer.
|
||||
Trace: command.Trace);
|
||||
|
||||
PublishAndNotifyChildren(changed);
|
||||
|
||||
@@ -742,6 +754,14 @@ public class InstanceActor : ReceiveActor
|
||||
writeValue,
|
||||
DateTimeOffset.UtcNow);
|
||||
|
||||
// Flow Debugger trace arm (design §5.3): only a TRACED write with a known writer arms;
|
||||
// the device echo that follows is stamped in HandleTagValueUpdate. Untraced writes
|
||||
// never touch the map, so production behaviour is unchanged.
|
||||
if (command.Trace && command.SourceExecutionId is { } traceSource)
|
||||
{
|
||||
_traceArms[attributeName] = (traceSource, DateTimeOffset.UtcNow.AddSeconds(_options.TraceArmWindowSeconds));
|
||||
}
|
||||
|
||||
// Ask the DCL and pipe the result back to the original caller. The DCL
|
||||
// returns the failure synchronously so the script can handle it.
|
||||
_dclManager.Ask<WriteTagResponse>(writeRequest, TimeSpan.FromSeconds(30))
|
||||
@@ -1035,6 +1055,17 @@ public class InstanceActor : ReceiveActor
|
||||
// O(1) lookup off the hot ingest path (was a linear FirstOrDefault).
|
||||
_resolvedAttributeByName.TryGetValue(attrName, out var resolved);
|
||||
|
||||
// Flow Debugger trace arm (design §5.3): consume the arm for this attribute — the
|
||||
// echo is stamped whatever its value (a device that clamps or rejects the write
|
||||
// still shows what it did) — unless the arm has already expired.
|
||||
Guid? traceSource = null;
|
||||
var traced = false;
|
||||
if (_traceArms.Remove(attrName, out var arm) && arm.ExpiresAt > DateTimeOffset.UtcNow)
|
||||
{
|
||||
traceSource = arm.SourceExecutionId;
|
||||
traced = true;
|
||||
}
|
||||
|
||||
// A List-typed attribute coerces the incoming OPC UA array
|
||||
// (a CLR array/IEnumerable from the SDK) into a typed List<T>. On an
|
||||
// element-type mismatch we set the attribute's quality to Bad, log a
|
||||
@@ -1045,7 +1076,8 @@ public class InstanceActor : ReceiveActor
|
||||
{
|
||||
HandleAttributeValueChanged(new AttributeValueChanged(
|
||||
_instanceUniqueName, update.TagPath, attrName,
|
||||
typedList, QualityToString(update.Quality), update.Timestamp));
|
||||
typedList, QualityToString(update.Quality), update.Timestamp,
|
||||
SourceExecutionId: traceSource, Trace: traced));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1062,7 +1094,8 @@ public class InstanceActor : ReceiveActor
|
||||
// non-change.
|
||||
PublishAndNotifyChildren(new AttributeValueChanged(
|
||||
_instanceUniqueName, update.TagPath, attrName,
|
||||
currentValue, "Bad", update.Timestamp), evaluateWaiters: false);
|
||||
currentValue, "Bad", update.Timestamp,
|
||||
SourceExecutionId: traceSource, Trace: traced), evaluateWaiters: false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1076,7 +1109,8 @@ public class InstanceActor : ReceiveActor
|
||||
|
||||
HandleAttributeValueChanged(new AttributeValueChanged(
|
||||
_instanceUniqueName, update.TagPath, attrName,
|
||||
value, QualityToString(update.Quality), update.Timestamp));
|
||||
value, QualityToString(update.Quality), update.Timestamp,
|
||||
SourceExecutionId: traceSource, Trace: traced));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user