Merge branch 'worktree-agent-ae3652f34db9ac0b4' into fix/flow-debugger-arch-review

This commit is contained in:
Joseph Doherty
2026-09-06 14:12:32 -04:00
2 changed files with 95 additions and 1 deletions
@@ -179,6 +179,8 @@ public class InstanceActor : ReceiveActor
/// 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.
/// Armed at ISSUE, disarmed on a failed/timed-out write (arch-review F6, <see cref="DisarmTrace"/>);
/// a successful write's echo consumes it.
/// </summary>
private readonly Dictionary<string, (Guid SourceExecutionId, DateTimeOffset ExpiresAt)> _traceArms = new();
// Maps each tag path to every attribute canonical name that references it.
@@ -339,6 +341,17 @@ public class InstanceActor : ReceiveActor
Receive<TagValueUpdate>(HandleTagValueUpdate);
Receive<SubscribeTagsResponse>(HandleSubscribeTagsResponse); // S4/UA6: real retry-aware handler
Receive<RetryTagSubscribe>(msg => SendTagSubscribe(msg.ConnectionName));
// Arch-review F6: a traced write the DCL rejected (or that timed out) drops its own arm,
// so an unrelated later update is not stamped as that write's device echo.
Receive<DisarmTrace>(msg =>
{
if (_traceArms.TryGetValue(msg.AttributeName, out var arm)
&& arm.SourceExecutionId == msg.SourceExecutionId)
{
_traceArms.Remove(msg.AttributeName);
}
});
Receive<ConnectionQualityChanged>(HandleConnectionQualityChanged);
// Handle alarm state changes from Alarm Actors (Tell pattern)
@@ -756,17 +769,29 @@ public class InstanceActor : ReceiveActor
// 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.
// never touch the map, so production behaviour is unchanged. The arm is taken at ISSUE
// and dropped again on a failed/timed-out write (arch-review F6, via the DisarmTrace
// self-message below); a successful write's echo consumes it.
Guid? armed = null;
if (command.Trace && command.SourceExecutionId is { } traceSource)
{
_traceArms[attributeName] = (traceSource, DateTimeOffset.UtcNow.AddSeconds(_options.TraceArmWindowSeconds));
armed = traceSource;
}
// Captured for the continuation: it runs OFF the actor's dispatcher, so it must never
// touch _traceArms directly — it Tells self instead.
var self = Self;
// 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))
.ContinueWith(t =>
{
var succeeded = t.IsCompletedSuccessfully && t.Result.Success;
if (!succeeded && armed is { } armedSource)
self.Tell(new DisarmTrace(attributeName, armedSource));
if (t.IsCompletedSuccessfully)
return new SetStaticAttributeResponse(
correlationId, instanceName, attributeName,
@@ -2112,6 +2137,16 @@ public class InstanceActor : ReceiveActor
/// </summary>
private sealed record RetryTagSubscribe(string ConnectionName);
/// <summary>
/// Self-message (arch-review F6): a traced data write FAILED, so drop its trace arm — but only
/// if it is still the armed one. The write's continuation runs off the dispatcher and must not
/// touch <see cref="_traceArms"/> itself; matching on <paramref name="SourceExecutionId"/> keeps
/// a late failure from stealing an arm a newer traced write has since taken.
/// </summary>
/// <param name="AttributeName">Canonical name of the attribute whose arm is to be dropped.</param>
/// <param name="SourceExecutionId">Execution id of the writer that took the arm.</param>
private sealed record DisarmTrace(string AttributeName, Guid SourceExecutionId);
/// <summary>Self-tell that fires the coalesced static-override persist flush (P4, arch-review WP1.5).</summary>
private sealed class FlushStaticOverrides
{