A capped events read now pages: the source's truncation flag triggers a
session-bound cursor (time inclusive + tie skip), oversized tie clusters
are paged within via a bounded half-open over-fetch, and release/unknown/
cross-kind points behave as for Raw.
IHistorianDataSource.TimestampResolutionTicks (default 1; QuestDB 10). A drained tie
cluster now steps off T by the resolution, so a forward one-value-per-page read on a
microsecond backend no longer re-reads the same sample forever (C1). QuestDB AtTime
results carry the client's original request time (M3); regression for the NEXT seek
skipping a tombstone (M1). Conformance backend wires the real writer and updater.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- A DateTime.MinValue start or end on DeleteRawModified is refused
BadInvalidArgument before the backend.
- Tests: anonymous without the HistoryUpdate role denied (with it allowed),
UpdateTagAttributes enabling historization sets HistoryWrite, a throwing
updater is BadInternalError, the MinValue bounds case; corrected the
test helper's comment on how the SDK base seeds unadmitted slots.
- GatewayHistorianValueWriter remarks describe the recorder's real status
capture (and that it does not persist on the gateway).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The DroppedUnsupportedKind counter still counts every value; only the Debug
line is latched (cleared by the next captured value), so a Gateway-backend
comms loss no longer logs one line per tag.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
A reverse read (start > end) with a small NumValuesPerNode resumed through
the tie-cluster path, which drained the cluster and stepped FORWARD to
T + 1 tick - outside a reverse window - so the read ended with GoodNoData
after one sample. SliceTieCluster now steps down to T - 1 tick for a
reverse read (terminal at the excluded end), and the node manager slices
the over-fetched cluster in the read's own direction so the skip count is
not applied from the wrong end. Forward paging is unchanged.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- OtOpcUaNodeManager overrides HistoryUpdateData / HistoryDeleteRawModified / HistoryDeleteAtTime,
block-bridging to the optional IHistorianDataUpdater under the shared HistoryRead batch limiter +
deadline. Insert/Replace/Update map to HistoryUpdateMode; Remove, IsDeleteModified, events and
structure data are BadHistoryOperationUnsupported. Per-item codes stitched back in request order
(MinValue SourceTimestamp => BadInvalidTimestamp, untypeable value => BadTypeMismatch); success
clears the base's pre-seeded Unsupported error.
- HistoryWrite AccessLevel bit OR-ed in for historized nodes when an updater is wired (raw + UNS);
it is dispatch-load-bearing in the SDK base. OtOpcUaSdkServer.SetHistorianDataUpdater; the hosted
service assigns it from an optional HistorianUpdaterHolder before SetSink, clears it on stop.
- AuthorizeHistoryUpdate: ACL on => scoped OpcUaOperation.HistoryUpdate decision (replaces the role
check); ACL off => #563 key re-check + new OpcUaDataPlaneRoles.HistoryUpdate role, conferred by
ApiKeyInterimRoleMapper only from a Cluster-scoped HistoryUpdate grant (WriteOperate does not).
- AdminUI ACL editor offers NodePermissions.HistoryUpdate.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Review round 1: the #481 FanCommsLossToDependencyMux fan-out DOES reach the recorder. Per the
controller ruling it is historized as an Empty/BadNoCommunication outage marker when the backend
supports Empty and counted in DroppedUnsupportedKind otherwise; remarks and the counter doc now say
so. DefaultSupportedKinds is a FrozenSet. The drop-leg test now runs under the default (gateway)
kinds, and the Empty leg pins status 0x80310000.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The recorder now appends the typed HistorianValue and the mux's own StatusCode instead of a
double + hard-coded Good, and drops any kind not in the backend's SupportedKinds (resolved
optionally from IHistorianBackendProvider; absent => {Double, Int64, Boolean}, today's behaviour).
RecorderStatus.DroppedNonNumeric -> DroppedUnsupportedKind; GoodQuality const removed; stale
"mux carries no quality" remarks rewritten (comms-loss node stamps are not historized).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Since #516 a DriverConfig change is stop + respawn, so DriverSpawnPlan.ToApplyDelta
was always empty and the in-place path had no producer. #663 and #664 were both
latent bugs in it. This deletes it:
- host: ApplyChildDelta, _pendingDelta, HandleApplyResult and HandleDeltaApplied
- plan: DriverSpawnPlan.ToApplyDelta
- actor: the ApplyDelta, ApplyResult and DeltaApplied messages, the Connected
reinit handler and the mid-connect AdoptConfigDuringInit
- tests: the actor tests that drove the seam
IDriver.ReinitializeAsync and each driver's implementation and unit tests stay.
Comments and docs that cited AdoptConfigDuringInit as the reason a driver must
release prior state at init now cite the IDriver contract and overlapping
retries, which still apply.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
AdoptConfigDuringInit re-runs InitializeAsync(newConfig) and acks success, but
Calculation and Galaxy ignored the JSON there: Calculation kept its old tag
set, and Galaxy connected on its old endpoint. Calculation now rebuilds its tag
table from a config with a body (sharing AdoptConfig with ReinitializeAsync and
stopping any running timer scheduler first). Galaxy, which cannot hot-swap its
options, refuses a non-equivalent one the way ReinitializeAsync does. The actor
now sends DeltaApplied once the adopted init succeeds, so a dependency
consumer's mux interest is re-registered. Unreachable from deploys since #516.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
A Connected ApplyDelta called ReinitializeAsync and kept the actor's data and
native-alarm handles, though every driver's reinit drops the driver-side
subscriptions behind them (#630). The reconcile only re-subscribes a missing
handle, so the driver went silent until a reconnect. Release both at the
driver before the reinit and re-subscribe both in a finally. Unreachable from
deploys since #516; pinned by a two-case test, each case failing against a
different wrong fix.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- Reconcile retries of a failing alarm subscribe back off (skip 2^(n-1)-1
ticks, cap 7 => >= every 4 min at the 30 s tick) so a hanging subscribe
cannot hold the mailbox every tick; the stack is logged once per streak.
- The unreachable handler re-attach logs at Warning (it only fires on a
regression).
- OpcUaClient: a failed SubscribeAlarmsAsync detaches its notification
handler and bounds the session cleanup to 5 s under _gate;
OnEventNotification drops events from subscriptions not registered in
_alarmSubscriptions, so a stray that survives cleanup cannot deliver.
- The retry test also covers 3 failures (crosses the backoff); its tick
positive control documents its dependence on the stub's changing health.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
A data re-subscribe (every deploy's bulk SetDesiredSubscriptions push to a
kept driver) dropped the prior data subscription through the full
DetachSubscription, which also unhooked OnAlarmEvent and forgot the alarm
handle. On any deploy that did not respawn the driver, the alarm
subscription kept logging "established" while every event reached nobody
until a reconnect or respawn. The data path now uses DetachDataSubscription;
only a lost connection and PostStop detach both.
The #488 health-poll reconcile only looked at data, so an alarm subscribe
that failed on (re)connect gated the feed for the life of the connection.
ReconcileAlarmSubscription now re-attaches a missing handler and
re-subscribes a stale/missing alarm handle every tick.
OpcUaClient removes a half-built alarm subscription from its session when
SubscribeAlarmsAsync throws, so the new retries cannot accumulate.
Tests: actor-level data-resubscribe + reconcile-retry tests and an
end-to-end NativeAlarmFeedSurvivesDeployTests through DriverHostActor; all
three fail against the unfixed actor.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
After an OpcUaClient respawn an upstream inactive+unacked condition went out as
Retrigger+Clear (duplicate Activated+Cleared rows, Active flicker), and a
first-sight inactive+acked one as Unspecified (an Activated row for an inactive
alarm; a retained-Active host condition never cleared).
- New AlarmTransitionKind.InactiveUnacknowledged: one projection, rows only for
what changed (Activated+Cleared from acked, Cleared from active+unacked, none
when already inactive+unacked).
- OpcUaClient uses it for moves into inactive+unacked; first sight of an
inactive condition is InactiveUnacknowledged or Clear+Acknowledge, flagged.
- Projector: a flagged Acknowledge on an already-acked condition writes no row.
- E2E tests through DriverHostActor.ForwardNativeAlarm (red-proven).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Item 2 — controller-discovered UDT members are writable on both transports once
browse-committed and authored ReadWrite (writability is the operator's: the commit
lands AccessLevel Read with no writable key). The write names the member (one CIP
symbolic segment per member), its own type, its own bytes. BIT members are supported:
Z-35162 (read-only probe) exposes a UDT BIT member as a 1-byte 0xC1 BOOL, so the
controller applies the bit and the hidden host's other bits are never in the request.
- AbCipDriver: pre-write width check on BOTH transports (libplctag patched part of
its read buffer and sent it all): BadTypeMismatch, Bool over a wider value
BadNotSupported; nothing sent, handle kept, health untouched, warn once.
- A runtime NotSupportedException is a refusal: no longer latches Degraded.
Item 3 — whole-UDT writes refused BadNotSupported before any runtime exists, by
decision (a structure write overwrites hidden hosts + padding), documented.
ab_server patch 0003: serve member paths from dotted flat tags (UdtSeg.*), and
accept libplctag's pad byte on 1-byte writes (stock ab_server refused every
libplctag BOOL/SINT write with 0x03). New AbCipUdtMemberWriteTests (both
transports), AbCipDiscoveredMemberWriteTests, native request-byte theory, and a
browse-commit mapper pin. Docs: AbCip.md Member writes / Whole-UDT writes / Known
gaps, fixture docs, CLAUDE.md AB section.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
M1 drop-out is a no-row Unspecified+IsRestatement update (projector case); M2 ack judged over every declared
limit; L1 ActiveAcknowledged for an already-acked new limit; L2 Bad Acked/severity carried/skipped; L3 host
end-to-end test; L4 BOOL member-by-member tests + mixed ALMD/ALMA; L5 detection requires no plain Acked.
ALMD active bit is InAlarm per Rockwell's ALMD reference; InFaulted is input health.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- AlarmTransitionKind.ActiveAcknowledged (appended): active AND acked applied as ONE projection.
Galaxy ActiveAcked snapshot entries and OpcUaClient inactive->(active,acked) use it; the
projector writes 'Activated' only if not already active, 'Acknowledged' only if not acked.
- AlarmEventArgs.IsRestatement (optional, default false): set only by Galaxy snapshot entries
and OpcUaClient first sight. A flagged Retrigger on an Active+unacked condition with the same
message and severity, or a flagged Clear on an inactive condition, writes no /alerts row.
- NativeAlarmProjector reports the history rows per transition; ForwardNativeAlarm writes one
row per entry. Unflagged sources (FOCAS/MTConnect/AbCip, live Galaxy) are unchanged.
- Tests: projector rules each with a must-write control; OpcUaClient transition table;
Galaxy feed/driver mapping; Galaxy + OpcUaClient end-to-end through DriverHostActor.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
HistoryRead is served server-side by IHistorianDataSource; nothing ever dispatched
to a driver's IHistoryProvider. Removes IHistoryProvider, the OpcUaClient history
passthrough, DriverCapability.HistoryRead (+ resilience tier rows and AdminUI row),
HistoryReadStatusException and its node-manager catches, and the tests that only
covered them. Shared result types and HistoryAggregateNotSupportedException stay
(moved to Historian/HistoryReadContracts.cs). A stored ResilienceConfig still
carrying a HistoryRead key parses (diagnostic only; new test).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
CentralCommunicationActor: while the PSK provider is unresolved or the first
ClusterNode load failed, re-arm RefreshContacts every 2 s (max 15) until the
first publish; Information log on first publish; 'NEVER be published' Error
now only after the retry budget is exhausted. Corrected false comments.
Tests: late provider (Grpc+Dps), failed first load, retries stop, bounded budget;
ControlPlaneStarterOrderTests back to the default 60 s refresh.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The tests probe a free port (bind 0, release) and start the SDK server later; under a
highly parallel run another test can take the port in between and the SDK fails with
'Failed to establish tcp listener sockets for Ipv4 and IPv6.'. OpcUaHostStart.StartAsync
builds a fresh host + server on a fresh port per attempt (up to 5) when the failure is a
bind failure; all 45 start sites in OpcUaServer.Tests, OpcUaServer.IntegrationTests and
Runtime.Tests use it. Per-attempt GUID ApplicationUris are hoisted out of the host factory
because a changed URI against a reused PKI store fails with 'Cannot access private key'.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
R1: NativeAlarmProjector treats Raise-on-Active as a re-statement only when the message matches what
the condition shows; a different alarm appearing during a respawn un-acks and writes its alerts row.
FOCAS condition messages are a deterministic function of the matched set (tested across fresh driver
instances and reordered lists); Runtime E2E now covers both the same alarm and a new one during a
respawn. Residual (source message determinism) documented in AlarmTracking.md.
R2: a non-OK cnc_rdalmmsg2 return is thrown as FocasWireException{Rc} and classified as a CNC
refusal: conditions get a quality-only BadNotSupported once, one Warning, polling continues, and
driver health is untouched (no 90 s watchdog reconnect loop on a production control). Only transport
faults degrade (falsification control in the test).
R3: alarm-path health is tracked apart from the value path and combined in GetHealth: worst state,
later LastSuccessfulRead. A good value read can no longer mask a failing alarm read (no per-poll
/hosts flip) and a good alarm read cannot mask a value Degraded.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
DriverHostActorNativeAlarmAckRoutingTests: Applied does not mean the child DriverInstanceActor is
Connected, and an ack reaching a still-Connecting child is dropped by design — the single
positive-control ack was itself dropped (4/20 under DOTNET_PROCESSOR_COUNT=1 x8, still failing
with the presence budget dilated to 45 s, so not a budget problem). SpawnHostAndApply now waits
for SubscribeAlarmsAsync, which only the Connected state calls. ScriptedAlarmEngine
OnEvent_emission_happens_outside_evalGate: 2 s blocking poll -> 15 s (5/20 under starvation).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
MAJOR-1: a deploy that changes a driver's config respawns it, and the fresh instance's empty told
record re-sent Raise for every still-active alarm, un-acking acknowledged conditions and writing a
duplicate "Activated" alerts row. NativeAlarmProjector now treats Raise on an already-active
condition as a re-statement (keeps Acked; ForwardNativeAlarm skips the alerts row); only Retrigger
(or inactive->active) un-acks. Sources whose upstream ack state is authoritative send Retrigger for
an unacked re-statement: OpcUaClient on first sight (no prior told), Galaxy for an unacked active
snapshot entry. Runtime E2E drives a FRESH FOCAS driver after a redeploy and asserts the ack survives.
MAJOR-2: a failed FOCAS alarm read annotates each subscribed condition BadNoCommunication once per
outage (#626 quality-only path) with one Warning (MINOR-6), lifted to Good on recovery; failed alarm
reads degrade driver health with LastSuccessfulRead frozen (successful ones advance it), so an
alarm-only driver trips the #527 comms-loss watchdog.
MINOR-3 CLAUDE.md #630 bullet; MINOR-4 split line; MINOR-5 a non-empty cnc_rdalmmsg2 payload that
fits neither stride throws; MINOR-7 value-tag + parsed-address tables are built and swapped, never
cleared in place under the retained poll engine. Docs corrected (FOCAS.md, AlarmTracking.md, CLAUDE.md).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
An ALARM: address (*, <Type>, <Type>/<n>, <Type>/<lo>-<hi>, T<code>, OTHER) on a raw tag that
also carries the cross-driver alarm object is ONE condition with ConditionId == RawPath and a
Kind (Raise / Retrigger / Clear), so DriverHostActor.ForwardNativeAlarm routes it and
NativeAlarmProjector activates it. Fixes all seven triage defects:
1. SubscribeAlarmsAsync treated the host's RawPaths as device hosts -> nothing polled.
2. Decorated "{host}#{Type}:{Number}" ConditionId -> every event dropped.
3. No Kind -> projector kept prior state.
4. WireFocasClient.ReadAlarmsAsync answered [] on failure -> mass clear + un-acked re-raise.
It now throws; a failed device read keeps every condition's state.
5. Acks lost on reconnect -> told-record dedup (MTConnect #626 pattern) held for the driver's
lifetime; same-set re-subscribe / re-init re-raise nothing; changed set prunes leavers only.
6. Subscribe threw while AlarmProjection.Enabled=false -> the tag is the opt-in; flag is a no-op.
7. Alarm tags could not be authored -> ALARM: grammar (FocasAlarmSelector, Contracts) backs the
editor, the address picker, the deploy-time inspector and driver init.
Severity: authored alarm.severity -> exact OpcUaSeverity; else the 16i-era type bucket. The type
table stays the only one (moved to Contracts, documented 16i-era; 30i codes not invented; raw
T<code> selector + raw code in every message). Read-only: ack is server-side. Also clears the
value table on a re-initialise (#630 gap that let a removed/moved tag keep reading).
E2E: Runtime.Tests FocasNativeAlarmEndToEndTests drives the real driver through the real
DriverHostActor and asserts the condition goes Active then inactive.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- v2-ci.yml: upload-artifact@v4 -> @v3 + continue-on-error (act_runner reports as GHES and
v4 refuses GHES, failing every run); runner-capacity diagnostic step; AKKA_TEST_TIMEFACTOR=3
on the test step.
- Runtime/ControlPlane harnesses: akka.test.timefactor from AKKA_TEST_TIMEFACTOR
(TestTimeFactor), single-expect-default 15s; ControlPlane gains PresenceBudget.
- Leaked spinner threads: the infinite-loop timeout tests (Calculation, Host.IntegrationTests
RoslynVirtualTagEvaluator, 3x Core.Scripting) pinned a core for the rest of the test process
(abandoned, never stopped). Now a bounded 5 s wall-clock spin; outer compile-inclusive bound 30 s.
- Real test races fixed: ScriptedAlarmEngine push-during-drain (Inactive is also the start
state + persist-before-update => wait for the Cleared save); DriverReconnectE2e (both nodes
host the driver; Created[] is last-writer-wins and ForceReconnect is a no-op while
Connecting); ControlPlaneStarterOrder (lost-race heal waited for a 60 s refresh tick).
- Fixed sleeps -> poll-for-condition: ReconnectSupervisor LastError, Modbus deadband steps
(wait for observed reads), Modbus dispose hygiene precondition, FOCAS probe-loop log.
- Short presence budgets -> PresenceBudget in the Runtime/ControlPlane tests seen failing on
Gitea; Calculation Settle 10 -> 30 s. No assertion removed, nothing skipped.
- infra/README.md §10 CI; stale TreatWarningsAsErrors comment in Directory.Build.props.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Strict width+kind eligibility for sum-batched scalars (others read singly); latch only on ADS
1793/1794, split 1792/1797/1798 without latching; 32 KiB per-command budget and 500-item ceiling;
short sum response handled per item. Diagnostics: opt-in diagnosticsEnabled (default off), reuse the
probe's ReadState, sample after the host-state transition, JitterMs renamed HeadroomMs, compact
per-device line so many devices fit the 16-entry cap, unavailable-symbol latch cleared on reconnect.
AdminUI: min/max on sum-read max, diagnostics checkbox. Pure planner extracted and unit-tested.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Sum-read (#310): batches a poll's reads per device into sum commands capped at sumReadMaxItems,
per-item ADS errors mapped per tag, falls back to individual reads when the target refuses the
command. Diagnostics (#314): ADS state, app name, online-change count, task cycle/exec/jitter via
IDriverDiagnostics. Re-implemented against the current raw-byte read path (stranded auto/driver-gaps
commits predate it). Sum-write not ported. Unit tests only; live gate pending a route.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Nothing read the mapping table beyond the startup validator; v3 UNS projection is
explicit via UnsTagReference. Legacy keys in stored DriverConfig are ignored (pinned).
Reverts the docker-dev opc-plc seed workaround from fdee129f.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
60 s labelled a slow-but-clean large rebuild degraded. The deadline moves to a
shared Commons DeployTimings constant that ConfigPublishCoordinator reads, and
the node's timeout is deadline - 15 s margin, so the held ack still lands in
time. A timed-out ack's reason now says the outcome was not reported in time,
distinct from a real sink failure.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe