Commit Graph

4917 Commits

Author SHA1 Message Date
Joseph Doherty 3c0e5ea4d7 perf(scripting): #789 Windows worker pipe I/O blocks on dedicated threads, off the completion port
A .NET pipe opened Asynchronous completes even its synchronous Read/Write
through the thread pool's completion port, so every frame on Windows cost a
completion packet plus a pool work item (and the pool's spin-wait). New
WindowsBlockingPipeStream issues its own overlapped ReadFile/WriteFile with the
OVERLAPPED.hEvent low bit set (no completion packet) and waits on that event on
the calling thread; reads and writes stay concurrent, unlike a synchronous
handle. ScriptWorkerFrameCodec.BlockingStream picks it on Windows and the pipe
itself on Unix, so both ends now use the dedicated blocking reader + synchronous
combining writer on every OS (UseBlockingIo and the async read loops are gone).

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 19:11:10 -05:00
Joseph Doherty f109e5f29e Merge #875 Galaxy initialize 20 s budget (per-driver-type capability defaults)
v2-ci / unit-tests (push) Successful in 1h14m54s
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:44:52 -05:00
Joseph Doherty 95bde828eb fix(galaxy): #875 Galaxy initialize gets a 20 s budget via per-driver-type capability defaults
Gate F (#761) against the VD03 gateway: Galaxy initialize (session open +
platform seed) took 4–5 s against Tier A's 5 s Initialize timeout, so 10 of 40
restarts failed, paid a 10 s retry and left a killed gateway worker.

DriverFactoryRegistry.Register takes optional capabilityDefaults per driver
type (GetCapabilityDefaults); DriverResilienceOptionsParser layers them over the
tier table and under the per-instance ResilienceConfig; the Host wires the
registry into DriverCapabilityInvokerFactory. Galaxy registers Initialize =
20 s (no retry, breaker 2), under the actor's 30 s pessimistic bound.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:44:44 -05:00
Joseph Doherty 2be43aa984 Merge #717 Warm redundancy design spec + plan (DRAFT, awaiting owner approval)
v2-ci / unit-tests (push) Has been cancelled
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:41:22 -05:00
Joseph Doherty 94c71541a1 Merge #761 gate F record (Galaxy session leak: PASS)
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:41:22 -05:00
Joseph Doherty 8d1621e534 docs(review): #761 gate F — 20 Galaxy respawn deploys keep mxgateway.sessions.open flat (VD03 DEV gw)
PASS: gauge baseline 2, +2 with the rig's driver on both centrals, 4 after 20
config deploys (40 respawns; rig opened 39 / closed 37). Final drop after
removal not observed: the Mac's VPN to VD03 dropped right after the settle.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:40:52 -05:00
Joseph Doherty c06769991b docs(plans): #717 Warm redundancy design spec + implementation plan (draft for owner approval)
Spec: per-shape contract (Unset/None/Warm), RedundancySupport + ServerUriArray graph,
ServiceLevel bands, per-service standby behaviour, alarm suppression + ConditionRefresh
(D5), strict first-delivery (D4), transition sequencing, ordered snapshot revisions,
ScadaBridge impact, SDK 2.0.1 re-proof list, open questions. Plan: T0-T13 with wire
tests, docker-dev live gate, rollout/rollback. Docs only.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:19:14 -05:00
Joseph Doherty 5ac5520532 docs: #873 SDK 2.0.1 API-key logins need client clock within 5 min ahead of the server (rollout runbook + security guide)
v2-ci / unit-tests (push) Waiting to run
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 18:08:39 -05:00
Joseph Doherty 43f8b8e85a Merge #860 #818 CI integration fixtures: busy-port retry, per-run names; floor 448
v2-ci / unit-tests (push) Has been cancelled
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 17:51:57 -05:00
Joseph Doherty 6ad643b3c7 Merge #874 OPC UA SDK server log output reaches the host logger
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 17:51:57 -05:00
Joseph Doherty 71740a5a42 ci: #818 raise v2-integration MIN_EXECUTED 440 -> 448 after the first hosted pass (run 1225: NAT tests executed 9 + 12, 525 total)
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 17:51:49 -05:00
Joseph Doherty b114779ba5 fix(ci): #860 integration fixtures survive a busy host port and concurrent runs
Hosted v2-integration run 1224 got past the old fixture-host timeout and
failed one step earlier: "Bind for :::19000 failed: port is already
allocated" — the runner's docker daemon is shared. Fixtures now retry on the
next host port when one is taken (opc-plc keeps host == listen port, as its
advertised endpoint requires) and export the ports actually used; container
names carry the run id so a concurrent run's `down` can't remove them, and a
new `logs` subcommand replaces the workflow's hard-coded name list.

Verified locally: with 19000 squatted, QuestDB moved to 18813/19001 and every
fixture reached ready; logs/down found the suffixed containers.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 16:53:14 -05:00
Joseph Doherty 2e948d3139 fix(opcua): #874 route the server's OPC UA SDK log output to the host logger
The host handed the SDK a provider-less telemetry context, so SDK-side
refusals — identity-token decryption (the SDK 2 RsaEncryptedSecret clock
window, #873), certificate validation, secure-channel faults — left no server
log line (found on windev during the #800 Windows validation). The hosted
service now creates one ServerSdkTelemetry context from its ILoggerFactory
(Warning and above, SDK categories, rate-limited 20/min per category) and
shares it between OtOpcUaSdkServer and the ApplicationInstance; released on
dispose. Test: an untrusted client's rejection reaches the captured log.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 16:37:03 -05:00
Joseph Doherty 88698ca042 docs(deploy): vd03 upgrade-readiness pack b8b4c18f -> master (#793)
v2-ci / unit-tests (push) Waiting to run
Read-only ConfigDb preflight (scripts/preflight/vd03-preupgrade-b8b4c18f-configdb-checks.sql)
covering every deploy gate added since the deployed build: VirtualTag/Calculation effective
triggers (#783), VT->VT reads and VirtualTagDependencyCycle (#791), newly forbidden script
APIs (#792, E2), credential-reference namespace / other-instance / reserved-name rules
(#815 #835 #836 #848 #849 #850), MTConnectMultipleDevices, Sql query/write gates (#781),
Heidenhain, UNS effective-name collisions and the FOCAS renames/value changes. Verified on a
throwaway b8b4c18f-schema database (every check fires on seeded rows) and clean against the
docker-dev config.

Upgrade notes (docs/deploy/vd03-upgrade-b8b4c18f-to-master.md): the EnableSnapshotIsolation
migration and how to apply it by hand, the env-var audit (Security:ApiKeys:Pepper is now
required and absent on the box; JWT key validated at start), SDK 2.0.1 / CA KeyUsage, the
ScadaBridge-visible changes, swap/rollback and post-deploy checks.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 16:13:10 -05:00
Joseph Doherty 363f0d8103 Merge #866 #870 #871 #872: MxStatus detail vocabulary + quality grading, ring event queue, CI races
v2-ci / unit-tests (push) Waiting to run
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:38:09 -05:00
Joseph Doherty 11ea972cd3 test(adminui): #871 reaper not-a-guid database name unique per run
The fixed name lives on the SQL Server shared by every checkout and CI, so a
concurrent run's create or finally-drop could collide (failed once in the
local pre-push suite, 3/3 green on rerun; cause plausible, not proven).

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:38:08 -05:00
Joseph Doherty e57e1324c3 Merge #870 ring-buffer event monitored-item queue
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:20:31 -05:00
Joseph Doherty 36583ee6f4 test(opcua): #870 review — pin the 1000th-newest duplicate-window boundary; serialise the ring-queue switch users
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:20:31 -05:00
Joseph Doherty bcde93056a perf(opcua): #870 ring-buffer event monitored-item queue
Since #855 condition events are queued to folder event items under the node
manager Lock. At a full DiscardOldest queue the SDK's EventQueueHandler
dequeues the head of EventMonitoredItemQueue, a List.RemoveAt(0) shift of the
whole queue (~1 us/item at QueueSize 1000, ~10 us at 10000).

OtOpcUaSdkServer.CreateMonitoredItemQueueFactory now wraps the SDK factory in
RingEventQueueFactory: event queues are RingEventMonitoredItemQueue (O(1)
discard), data-change queues, durable requests and disposal go to the SDK
factory unchanged. A factory set through StandardServer.MonitoredItemQueueFactory
is used as-is.

The SDK queue is storage only (decompiled 2.0.1): DiscardOldest, overflow-event
positions and discarded/overflow counters live in EventQueueHandler, which is
unchanged. The ring reproduces QueueSize 0 initial + BadInternalError enqueue,
drop-oldest at Count == QueueSize, FIFO dequeue, SetQueueSize keep-newest /
keep-oldest, the newest-1000 duplicate window via IsEventRaisedFor, no-op
Dispose. RingEventMonitoredItemQueueParityTests drive both queues, and the SDK
EventQueueHandler over both factories, with the same random sequences.

FolderEventDeliveryPerfTests gains Ring_vs_sdk_event_queue (A/B in one process
via the internal OtOpcUaSdkServer.RingEventQueues switch).

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:15:45 -05:00
Joseph Doherty 1a08b4f746 fix(galaxy): #872 detail-0 failure rows route on category, not detected_by
Every raw detected_by value names an MXAccess layer (requesting/responding
LMX, NMX, automation object); the gateway's own failures arrive as
ProtocolStatus. The 'non-zero source = transport failure' rule made a failure
the responding object detected a comms error. Item 1 of #872 (Warning/Pending
categories) stays open pending a captured row.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:15:05 -05:00
Joseph Doherty 970f2159ad docs(galaxy): IsSuccess checks the status category only
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:14:22 -05:00
Joseph Doherty 875b82175d fix(galaxy): #866 grade OK-status data changes by the item's OPC DA quality
The gateway fills Statuses on every data change (live OnDataChange, value-cache
replay and session snapshot all carry Quality too), and EventPump used the status
row whenever one was present, so Quality was never read: an off-scan, Bad or
Uncertain attribute with an OK status row was published Good. A failure status
row still wins; otherwise the quality byte grades the value (legacy parity:
quality >= 192 Good).

Live, read-only on VD03 (DEV): healthy ScanState and _TestArea attributes stay
Good (0x00000000) via quality 192; the stopped JoePlatform/JoeAppEngine ScanState
now publish BadCommunicationError (0x80050000) instead of generic Bad, with no
"unrecognised quality byte" warning.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:14:11 -05:00
Joseph Doherty 9aaab9ee42 test: #871 fix two hosted-CI races that failed master runs 1218-1220
- MeshPskProviderRefreshTests: GatedResolver.Entered was set by the priming
  refresh and never reset, so Wait() returned before the held refresh reached
  the store and the mid-refresh call-count snapshot raced its increment
  (expected 1, was 2). Hold() now resets it.
- FailedCredentialLockoutTests all-bad burst: the 15 waiters could queue
  before a slot holder ran the increment after BeginAsync (authenticated 0
  vs 5). The wait now covers both conditions; the security assertions are
  unchanged.

Both looped 20x locally, 0 failures.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:10:01 -05:00
Joseph Doherty 71b3b7f35f fix(galaxy): #866 map MxStatus failure details by the MxStatusDetail vocabulary, not as OPC DA quality
The gateway copies MXSTATUS_PROXY.detail verbatim, so a failure row's detail is
always an MxStatusDetail code. FromMxStatus routed any detail <= 0xFF through the
OPC DA quality table: MX_E_PlatformCommunicationError (2) on a stopped platform's
ScanState logged "Unrecognised MXAccess quality byte 0x02" and published generic
Bad (VD03 live capture), MX_E_InvalidObjectId (8) became BadNotConnected,
MX_E_Overflow (20) BadCommunicationError, and a detail in the Uncertain band
published Uncertain for a failure.

Every non-zero detail now maps per the gateway.md vocabulary (timeout,
communication, configuration, type, range, access, initialising), then by
category; the category-fallback warning logs once per (detail, category).

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 15:06:26 -05:00
Joseph Doherty 7a27ef1f67 docs(evidence): 2026-11-11 live gates — Heidenhain iTNC 530 (#829/#853), TwinCAT Z35474 Test Connect (#755/#863), Galaxy H6 ack (#614/#869)
v2-ci / unit-tests (push) Waiting to run
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 14:34:33 -05:00
Joseph Doherty efb7b21508 Merge #855 shared per-report event projection: Lock hold back near pre-#855 at N=50
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 14:34:23 -05:00
Joseph Doherty 74b2ce170b fix(opcua): #855 perf review 4 — registry sweep threshold capped; re-sweep at the cap
- The sweep threshold never rises above the 4096 cap (a threshold the count
  could not reach stopped sweeping, and sharing, for good); at the cap a
  sweep for dead shapes runs after every 256 refusals.
- The per-report "queued" mark is taken after the filter group resolves,
  so a failure there leaves the item unmarked.
- The bucket key also carries TypeDefinitionId and IndexRange.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 14:31:46 -05:00
Joseph Doherty 733dfe94da fix(opcua): #855 perf review 3 — weak filter-group registry; where clause validated; sharing pinned
- The filter-group registry holds groups weakly (the filter->group table is
  the only strong reference), sweeps dead entries amortised and counts only
  live shapes against the 4096 cap; filters past the cap are counted on the
  new meter otopcua.opcua.condition_events.filter_groups_unshared.
- A group validates its where clause too (restored, never-validated filters
  evaluate like validated ones).
- Test: value-equal filters cost one projection per locale set per report.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 14:20:19 -05:00
Joseph Doherty b77bb2ded1 fix(opcua): #855 perf review 2 — where clause in the group bucket key; pin dedupe, modify and keys
- Filter-group buckets include the where clause's (value-based) hash, so
  per-source filters sharing a select list are not all scanned under Lock.
- Locale keys are cached per session array.
- Tests: a notifier reached twice in one walk (Line above two wired
  equipment) gets each event once on both paths (fails without the
  per-report set); queued events re-projected by a modify are identical on
  both paths; locale keys unambiguous and where clauses hash by value.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 14:10:02 -05:00
Joseph Doherty ca0e73fbca fix(opcua): #855 perf review 1 — groups evaluate their own filter clone; lazy fields; tighter equivalence tests
- A filter group owns a deep clone of its first filter, validated once and
  evaluated under the group's lock: no item-owned filter is mutated or raced
  (Validate mutates a select clause).
- The projection is computed in the SDK's order: where clause first, fields
  only for an item that will queue.
- Unambiguous locale key; counter for filters given a private group past the
  registry cap; DeliveryCache getter returns its own allocation.
- UseProjectedEventDelivery=false also asks the role and grant checks per
  item, so the equivalence test compares against the pre-optimisation path.
- Equivalence test: same select list under three where clauses, a filter
  modified mid-run, a session with other locales, an ungranted anonymous
  Server-object item, per-rig counter; plus a small overflowing queue with
  DiscardOldest true and false.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:59:22 -05:00
Joseph Doherty 98d962b24b perf(opcua): #855 — shared per-report projection for condition events under Lock
Condition events delivered to many event items (folder and Server-object)
no longer pay the SDK's per-item QueueEvent(IFilterTarget) work under the
node-manager Lock: the role-permission verdict is shared per (session,
identity), the #826 grant check per identity per hop, and the where clause +
select-clause projection per (value-equal EventFilter, locales); each item
gets its own EventFieldList over the shared read-only fields. The SDK's
QueueEvent body is reproduced under the item's own lock (three private SDK
members, guard-tested); anything outside that shape falls back to the SDK.
The per-report duplicate check replaces the SDK's O(queue) scan.

ProjectedEventDeliveryWireTests compares both paths over the wire.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:42:29 -05:00
Joseph Doherty 1059eb7885 test(heidenhain): live gate asserts integer tool leaves keep their runtime type (#867)
Verified read-only on Hermle59 and Hermle70 (iTNC 530): Tool/Number Int64, Tool/Index and Tool/AxisCode Int32.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:36:13 -05:00
Joseph Doherty c63233b654 Merge #868 fourth review fixes: poison only by SQL error allowlist, memory-counted acks survive restart, Stale-recovered revisions unconfirmed
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:35:42 -05:00
Joseph Doherty ec3b835e4a fix(controlplane,runtime): #868 fourth review — SQL poison allowlist, memory-counted acks survive restart, adopted revisions unconfirmed
M1 IsPoison classified every SQL Server transient as poison:
   Microsoft.Data.SqlClient never sets DbException.IsTransient, so a deadlock
   (1205), timeout (-2 / Win32 258), dropped connection or AG failover was
   counted from memory and the node's row left Applying. Poison is now an
   ALLOWLIST: a DbUpdateException (not concurrency) carrying a SqlException
   whose number is 8152, 2628, 547, 515, 2627 or 2601. Everything else
   retries, bounded by the deadline deferral cap. IsPoison is public for tests.
   Acks counted only from memory (poison branch and the deferral cap) are kept
   in _memoryOnlyAcks and re-sent by PreRestart alongside _unpersisted, so a
   restart before the terminal write does not lose them.
M2 Revisions adopted without an outcome-reporting apply are unconfirmed, so a
   same-revision dispatch re-applies instead of short-circuiting a clean
   Applied over an empty address space: TryRecoverFromStale (restores only the
   revision marker) and RestoreApplied when the artifact read fails (null
   blob, LoadArtifactBlob swallows the error) or the restore throws.
L1 The re-drive comment documents the CreatedAtUtc recompute after a second
   restart / failover.
L2 _currentRevisionDegraded renamed _currentRevisionUnconfirmed; the stale
   TryBootFromCache remark corrected.
L3 docs/Redundancy.md: a Direct node running from cache that still cannot
   reach the ConfigDb acks a same-revision re-dispatch Failed
   (PartiallyFailed, not Sealed); Stale recovery and failed restores are
   unconfirmed too.

Test harness: SqlExceptionFactory builds real SqlExceptions (reflection on
SqlError / SqlErrorCollection / SqlException.CreateException);
ConstraintEnforcingDbFactory now throws DbUpdateException over SQL 2628
(over-length), 547 (poison) and 1205 (transient), plus FailNextContexts.

Tests (all fail on the previous code): poison allowlist theory (1205, 40613,
10054, 4060 not poison) + timeout/concurrency/providerless not poison; a
deadlocked ack write is retried and recorded; a memory-counted ack survives a
coordinator restart; Stale-recovered and failed-restore revisions re-apply on
re-dispatch.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:31:49 -05:00
Joseph Doherty 730bb19e6b Merge #869 Galaxy native alarm ack: unconfirmed gateway OK waits for Galaxy's acknowledged transition, reverts on timeout
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:30:40 -05:00
Joseph Doherty eba016096c Merge branch 'master' into worktree-agent-a720278a57ae41792
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:26:05 -05:00
Joseph Doherty b421c7ca91 fix(runtime): #869 review round 3 — key a native-ack revert to the ack that failed
A pending local ack can outlive its own delivery (sources that send no
Acknowledge back; a Clear now only carries it through), so a later failed
re-ack — or a stale Galaxy confirmation timing out after the alarm re-occurred
and was acked again — could revert an ack it does not own. MarkAcknowledged
now issues a sequence (0 when the ack changed nothing), carried on
RouteAlarmAck and echoed on AlarmAckFailed; TryRevertAcknowledge reverts only
on a matching sequence.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:25:58 -05:00
Joseph Doherty b5d4e25905 Merge #864/#865 Galaxy host status: per-template platform seed, event stream started before probe registration; registry and reconnect races fixed
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:22:33 -05:00
Joseph Doherty 50b7b030a9 Merge #855 folder event delivery perf harness (Category=Perf, opt-in)
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:21:27 -05:00
Joseph Doherty a8f4be616b test(opcua): opt-in perf harness for #855 folder event per-item delivery
FolderEventDeliveryPerfTests (Category=Perf, skipped unless OTOPCUA_EVENT_PERF=1)
reports per-alarm-report node-manager Lock hold and end-to-end delivery latency
for one equipment folder with N = 1/10/50/200 event items, ACL on/off, folder
subscribers publishing or stalled at a full queue; the latency of an i=2253
subscriber while folder subscribers stall; and a QueueSize sensitivity sweep.
Compiles against the pre-#855 tree for a before/after comparison.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:20:58 -05:00
Joseph Doherty 59d33ab938 test(galaxy): #865 review round 7 - pin the superseded-pump fault filter
Adds an escalated-stream-fault test seam and a test: the live pump's fault
escalates (positive controls before and after), a superseded pump's late
fault after a reopen does not; the absence is asserted only once both
pumps' callbacks are known to have run (HasEnded). Verified to fail with
the filter disabled. The filter's comment now says the identity check does
the work and warns about a future session bump while Healthy.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:20:49 -05:00
Joseph Doherty 5001e40690 fix(runtime): #869 review round 2 — a carry-through transition no longer settles a pending local ack
NativeAlarmProjector dropped the pending local ack on EVERY driver transition,
so an operator ack followed by the alarm returning to normal (a Clear carrying
the optimistic ack) inside the confirmation window made a later "not
confirmed" report a no-op — the condition stayed inactive+acked while Galaxy
showed UNACK_RTN. Only a transition that states the ack (Acknowledge,
ActiveAcknowledged, or an un-acking Raise/Retrigger/InactiveUnacknowledged)
now settles it; a Clear / same-message Raise / Unspecified update leaves it
revertible, and the revert projects inactive+unacked.

Tests: projector + host end-to-end (ack, Clear, confirmation fails -> inactive
+ unacked); tightened the confirmed-ack positive controls; pinned that the
confirmation receives the same request instance as AcknowledgeAsync.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:19:52 -05:00
Joseph Doherty 13cc7b8353 fix(galaxy): #865 review round 6 - ignore stream faults from a superseded pump
Each EventPump's fault callback now carries the pump's identity; a fault
from a pump that is no longer the live one (or belongs to an older registry
session) is logged at Debug and not escalated, so a stale pump's late fault
cannot fail a good replay now that a failure during Replaying fails the
attempt. The replay-retry warning includes the fault.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:16:01 -05:00
Joseph Doherty ca6d131078 Merge #868 third review fixes: truncated failure reasons, poison ack writes counted, bounded deadline deferral, cache-booted revision unconfirmed
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:14:17 -05:00
Joseph Doherty 780c87eba3 fix(controlplane,runtime): #868 third review — bounded failure reasons, poison ack writes, cache-booted revisions unconfirmed
1 PersistNodeAck wrote a Failed ack's FailureReason unbounded; the column is
  nvarchar(2048), so on SQL Server the write failed forever, the ack retried
  forever, the deadline deferred forever and every later deploy was refused.
  - One shared bound, Configuration.FailureReasons.Truncate (MaxLength 2048),
    used by the coordinator and DriverHostActor.UpsertNodeDeploymentState
    (exception messages and degraded reasons).
  - A non-transient DbUpdateException (not concurrency, no transient
    DbException / timeout / socket / IO underneath) is POISON: the ack is
    counted from memory with an Error, never retried.
  - Deadline deferral on unpersisted acks is capped at 10 x the retry ceiling;
    past it the outcome is decided from the acks held in memory.
2 A node restarting in the outcome window boots the cached revision (cache is
  written before the outcome) and never acks. Cache-booted revisions
  (BootstrapFromCache / TryBootFromCache) are now marked unconfirmed, so a
  re-dispatch re-applies and acks the real outcome instead of a clean
  short-circuit. docs/Redundancy.md corrected (replay only on the ConfigDb boot
  path).
3 A lost dispatch re-driven near its deadline now gets a FULL deadline (a node
  needs its whole outcome timeout + margin = ApplyDeadline, so any shorter
  remainder timed out nodes that applied). No dispatched-at column: a second
  restart recomputes from CreatedAtUtc and closes it instead (documented).
4 docs/Redundancy.md upgrade note: do not deploy while the central pair is mid
  rolling-upgrade across this change.

Tests (all fail on the previous code): oversized Failed reason truncated (on a
SaveChanges interceptor that enforces the column like SQL Server); rejected ack
write counted from memory; bounded deadline deferral; full deadline on a
near-deadline re-drive; cache-booted revision re-applies on re-dispatch;
oversized degraded reason truncated on the node's own row.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:13:36 -05:00
Joseph Doherty d80bdf6369 fix(galaxy): #865 review round 5 - a failure during the replay fails the attempt
ReconnectSupervisor swallowed a transport failure reported while Replaying,
so a restarted event stream refused/ended a round trip after the replay
began still let the replay go Healthy over a dead stream (and the next
advise's re-report restarted at attempt 1 with no backoff). A failure
reported during Replaying is now remembered and turns that attempt's
success into a retry with backoff. ReplayAsync's synchronous dead-pump
throw yields to a teardown's cancellation.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:12:33 -05:00
Joseph Doherty 77872b1d1c fix(runtime,galaxy): #869 review round 1 — confirmation off the actor scheduler, stop/async-fault tests
- Start the ack confirmation with Task.Run + ConfigureAwait(false) so it never
  depends on the ReceiveAsync handler's ActorTaskScheduler (the PostStop
  abandon-and-revert report must run after the actor stopped).
- DriverInstanceActorAckConfirmationTests: a confirmation that faults after the
  handler returned is reported; one pending at actor stop is reported; a
  confirmed ack reports nothing (positive control).
- Deterministic tracker matching test (Raise/Clear/Retrigger/other alarm do not
  confirm; case-insensitive match does).
- AdminUI min/max on the confirm-timeout field; Galaxy.md documents the
  reconnect, UNACK_RTN-as-Clear and RPC-budget limits.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:11:49 -05:00
Joseph Doherty 8653960ea3 fix(galaxy): #865 review round 4 - replay fails on a dead stream, backoff-aware wait
- ReplayAsync fails when its restarted pump's stream has already ended, so
  the supervisor backs off and stays Degraded instead of going Healthy over
  a dead stream (whose next advise re-report restarted at attempt 1 with no
  backoff - a tight reconnect loop).
- ReconnectSupervisor.AttemptInProgress (reopen through replay); advises
  wait for a stale pump's replacement only while an attempt is running, not
  for a whole backoff (the state still reads Reopening/Replaying there).
- ReconnectSupervisor: _disposed is set under the state lock and re-checked
  inside ReportTransportFailure, so a late report cannot start a recovery
  loop that StopAsync already missed.
- EventPump.HasEnded is a flag set before StreamRequested is released, and
  a stream that ends/faults on its first MoveNext is marked ended first.
- SubscriptionRegistry fast path: a miss now falls through to the locked
  path (lookup + session + window check atomically) instead of a lock-free
  re-lookup.
- Tests: replay over an immediately-ending stream fails; AttemptInProgress
  across attempt/backoff; ReportTransportFailure after stop; the
  reopen-window test uses a generous wait bound.

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:07:26 -05:00
Joseph Doherty c9e5c8dcf7 Merge #855 folder event items: per-item delivery under the node manager, ConditionRefresh cut matches the Server object
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:07:21 -05:00
Joseph Doherty ec17a8797b fix(opcua): #855 review 4 — per-item "known" in the folder cut; pin the post-stall liveness check
- RefreshDirectItemsWithCut keys its "decided off Lock" set per (item,
  condition), so a condition one item's walk saw is still decided for another.
- The deferral warning no longer blames slow folder subscribers (no folder of
  ours defers since #855).
- FolderEventFloodWireTests' removal test now asserts the folder item got
  nothing and the Server object exactly the pre-stall event (it passed with
  the post-stall liveness re-check deleted).

Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
2026-11-11 13:05:06 -05:00