Every round-2 finding re-verified against code (73 closed / 31 partial / 97 carried); matrix 19/28 cells up, one down. New Critical 03/S14 (AdminUI FallbackPolicy 401s every mesh gRPC map on a login-enabled central; masked on every rig gate by DisableLogin=true). New Highs 03/S15, 05/STAB-19, 05/STAB-18, 03/S16 — the stale-Good family at four seams.
37 KiB
Architecture Review 06 — Gateway Integrations (Galaxy Driver + Historian Gateway Driver)
- Date: 2026-08-19
- Commit:
12b0035a(master, clean tree) - Updates: this is the second update — round 1 ran 2026-07-08 at
9cad9ed0, round 2 (re-review) 2026-07-12 atf6eaa267. Sincef6eaa267, 25 commits landed in this domain's source (plus the Runtime-side historian recorder work), including: the R2-04/06/S-1 Galaxy write-confirmation arc (a537d29f,61030673,084c6e3c,20a816db,205cc4a4); the continuous-historization gate arc #491/#537/#538 (ad744734,e7fbe977,a76b4e07,384568b4,6ff64ca0,73205865,451273c8,7870c91f); Boolean→Int2 provisioning #441 (9f62310b) + StorageRateMs (4da2d62b); v3 RawPath re-keying (636c755b); replay-gap + sparse-array handling (4769044f); BadTypeMismatch for unrepresentable values (c51c5998); numeric alarm severity + provisioner channel dispose (55b12dc2); skipped-tags reporting #541 (39db7508,dd900f97); status-constant parity #497 (b95b99ba);GetMemoryFootprint/FlushOptionalCachesAsyncdeletion #525 (b8154469); secretssecret:arm (1424a214,772d3a5f). - Scope:
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy(+.Contracts,.Browser)src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway- Test coverage:
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests,.Galaxy.Browser.Tests,.Historian.Gateway.Tests - Peripheral (read for context):
Runtime/Historian/*(ContinuousHistorizationRecorder, options,ServerHistorianOptions), HostProgram.cs+Host/Configuration/ServerHistorianOptionsValidator.cs,Host/Drivers/DriverFactoryBootstrap.cs.
- Dimensions: Stability, Performance, Conventions, Underdeveloped Areas.
Prior-finding status (f6eaa267 → 12b0035a)
Every verdict below was re-derived from current code, not from remediation claims.
| ID | One-liner | Status @ 12b0035a |
|---|---|---|
| S-1 (High) | Galaxy write success optimistic; committed-write failure could never surface | CLOSED (residual metered, documented). Three legs all verified in code: (1) fail-closed advise — a failed AdviseSupervisory short-circuits to Bad without issuing the raw Write (GatewayGalaxyDataWriter.cs:174-178, meter galaxy.writes.advise_failed :48-50), feeding the #5 node revert; (2) correlated write statuses — TranslateReply consumes the gateway's OnWriteComplete outcome rolled into statuses[0] (:308-332); an EMPTY statuses array is provisional Good and increments galaxy.writes.unconfirmed (:330-331); (3) no truncation on the mapping seam — StatusCodeMap.FromMxStatus routes MxStatus detail codes > 0xFF per-code instead of truncating into the quality-byte table (the 1007→239→Good defect: StatusCodeMap.cs:120-133), with an explicit invariant that a failure row can never surface Good (:129-131) and a per-code MxStatusDetail table (1004-1017, :152-167). The borrow path is deleted by design — the writer never borrows a subscription's plain-advised handle (live-captured rationale in the class remarks, :21-27). Pinned offline via the IMxWriteOps seam (GatewayGalaxyDataWriterFailClosedTests). Residual: empty-statuses ⇒ provisional Good against a pre-correlation gateway — metered, cross-repo (mxaccessgw ≥ 53f69cd). But see NEW S-12: the advise cache has an exception-path poisoning bug. |
| S-2 (Med) | EventPump saturation drops the newest events | STILL OPEN — TryWrite drop-newest unchanged (EventPump.cs:164-170). Metered; bias still inverted for last-value-wins telemetry. |
| S-3 (Med) | ReadViaSubscribeOnceAsync can wait forever on a non-cancellable token |
STILL OPEN — Bad-fill still only via cancellationToken.Register (GalaxyDriver.cs:899-909); the awaits at :914 hang on CancellationToken.None + a never-publishing subscribed tag. No internal deadline added; no test covers the case (GalaxyDriverReadTests always emits or cancels). |
| S-4 (Med) | Transport-failure detection is EventPump-only | OPEN, NARROWED — ReportTransportFailure still has exactly one caller (GalaxyDriver.cs:1196, the pump fault callback). Improvement: the pump now treats a server-completed stream as a fault too (EventPump.cs:180-188 — closes the "clean termination evades watchdogs" shape for the streaming path). Residual: a subscription-less driver (writes only, or idle) never starts the pump, so a gateway restart still leaves GetHealth()==Healthy with per-write BadCommunicationError and no reopen — and the #527 comms-loss watchdog needs a non-Healthy state to trip, so it doesn't cover this either. |
| S-5 (Med) | Outbox RemoveAsync truncates the FIFO prefix; out-of-order acks silently drop |
OPEN → downgraded Low / accepted-by-design. The truncate-through semantics are unchanged (FasterLogHistorizationOutbox.cs:203-227) but are now (a) pinned by test (FasterLogHistorizationOutboxTests.cs:99 — "acks A and B, leaves C"), (b) explicitly relied on by the sole caller (ContinuousHistorizationRecorder.cs:677-680 documents why the ack stays all-or-nothing), and (c) documented in CLAUDE.md (#538 narrative). The prefix drop still doesn't increment DroppedCount — residual observability nit only. |
| S-6 (Med) | Historian health snapshot's connection flags are dormant | STILL OPEN, and SHARPENED — RefreshConnectionStateAsync (GatewayHistorianDataSource.cs:219-244) still has only test callers (GatewayHealthSnapshotTests.cs:33,59,74). Worse: grep shows GetHealthSnapshot() itself has no production consumer anywhere — the whole HistorianHealthSnapshot surface (counters + flags) is currently a write-only ledger. Wire a reader (the R2-10 resilience-chip pattern is the template) or delete the flags. |
| S-7 (Low) | Alarm feed reconnect has no backoff | STILL OPEN — fixed 5 s (GatewayGalaxyAlarmFeed.cs:46,141); DeployWatcher and ReconnectSupervisor still do capped exponential. |
| S-8 (Low) | GalaxyMxSession state unsynchronized across reopen |
STILL OPEN / accepted — plain fields (GalaxyMxSession.cs:29-31); safety remains emergent (single-flight supervisor + per-call catch). The AttachForTest path at least documents the _connected invariant now (:129-131). |
| S-9 / S-10 (Positive) | S&F/outbox crash-safety; TLS/API-key posture | Stand, strengthened — batch append is incremental-indexed so a mid-loop fault leaves no untracked prefix (FasterLogHistorizationOutbox.cs:119-137); provisioner now disposes its channel (GatewayTagProvisioner.cs:108-115); ApiKey supports ${secret:} (ServerHistorianOptions.cs:43-46). |
| S-11 (Med) | Misconfigured ServerHistorian crash-loops Host with raw UriFormatException |
CLOSED — verified all three layers: fail-fast ServerHistorianOptionsValidator wired via AddValidatedOptions (Host/Program.cs:188-196), consumer-gated so the alarm-only corner is covered, with the alarm-only startup warning (Program.cs:205-212); adapter defense-in-depth Uri.TryCreate → named InvalidOperationException (HistorianGatewayClientAdapter.cs:53-58); warning parity for malformed endpoints in Validate() (ServerHistorianOptions.cs:117-123). |
| P-1 (Med) | Galaxy Read = 3 round-trips + publish wait | STILL OPEN — gateway-constrained, unchanged (GalaxyDriver.cs:805-941). Accepted; "subscribe, don't poll" remains the pattern. |
| P-2 (Med) | Alarm drain is one unary SendEvent per event |
STILL OPEN — serial per-event loop unchanged (GatewayAlarmHistorianWriter.cs:66-78); still no batched SendEvents on the client surface. Deliberate poison isolation; accepted ceiling. |
| P-3 (Low) | Four channels to the historian sidecar (deliberate) | Unchanged — and the dispose-ownership rationale is now stronger (each path, provisioner included, owns and disposes its channel). |
| P-4 (Positive) | Fan-in/fan-out indexed, bounded, metered | Stands — plus new counters (galaxy.events.replay_gaps, galaxy.events.undecodable, EventPump.cs:50-55). |
| P-5 (Low) | HistoryRead buffers whole replies; maxEvents <= 0 unbounded |
OPEN, NARROWED — events reads now stop at the cap client-side with a truncation continuation token (GatewayHistorianDataSource.cs:142-175); only maxEvents <= 0 still collects unbounded (:142-143), and the adapter still cannot forward the cap on the wire (HistorianGatewayClientAdapter.cs:100-113 — client signature unchanged). The 03/S3 knobs (HistoryReadBatchParallelism/MaxConcurrentHistoryReadBatches/HistoryReadDeadline, ServerHistorianOptions.cs:80-97) bound the server-side fan-out/flood — a real mitigation for the same blast radius. |
| P-6 (Low) | Outbox peek holds the state lock across disk I/O | STILL OPEN — PeekBatchAsync scans under _state (FasterLogHistorizationOutbox.cs:190-197); note the exposure grew 16× with DrainBatchSize 64→1024 (ContinuousHistorizationOptions.cs:78). Still an in-memory-page scan; still acceptable; revisit if drain batches grow again. |
| C-1 / C-2 (Positive) | Secret handling; seam quality | Stand, strengthened — GalaxySecretRef gained the fail-closed secret: arm through the shared ISecretResolver (Galaxy.Contracts/GalaxySecretRef.cs:19-21,93-98); production wires the real resolver (DriverFactoryBootstrap.cs:175) while the NullSecretResolver default is confined to the test/standalone convenience overload (GalaxyDriverFactoryExtensions.cs:45-54 — the ForTest-seam trap checked and clear). New IMxWriteOps internal seam (GatewayGalaxyDataWriter.cs:342-405) made the sealed-SDK write pipeline unit-testable without widening the public contract. |
| C-3 (Med) | Historian seam leaks wire types by design | STILL OPEN (Low now) — IHistorianGatewayClient.cs:5-11 unchanged, no guard note added. Mitigation exists in practice: the U-7 presence-pinning tests + the 6/6 live gate are the bump-gate the finding asked for. Keep any future consumer above GatewayHistorianDataSource. |
| C-4 (Low) | Options-validation styles diverge; warnings-only style crashes | CLOSED — the asked-for fail-fast tier landed (S-11's ServerHistorianOptionsValidator + Validate() parity). Two idioms still coexist (Galaxy throw-on-construct vs. historian warnings+Host validator) but the sharp edge — correct warning followed by opaque crash — is gone. |
| C-5 (Low) | Retired Wonderware project dirs on disk | CLOSED — glob src/Drivers/*Wonderware* matches nothing (R2-12 established they were untracked husks; they are gone from this checkout). |
| C-6 (Low) | EventPump silently drops unknown event families with no metric | STILL OPEN (narrowed) — the default family arm still returns uncounted (EventPump.cs:297-312); however undecodable values and replay gaps now have their own counters, and filtered volume is derivable as received − dispatched − dropped − gaps. A galaxy.events.filtered counter remains the clean fix. |
| C-7 (Low) | Live-value writer populates a documented-dead Quality field |
CLOSED — keep-populating decision verified with the full rationale in the class remarks (GatewayHistorianValueWriter.cs:27-37: proto3 unset transmits 0 = Bad, so populating the correct 192 is forward-safe) + inline note at the mapping (:74-76) citing 0.2.0 Contracts C-002, + the explicit "do NOT extend the recorder to capture real node quality" warning. |
| U-1 (High, doc-drift) | CLAUDE.md KNOWN LIMITATION 2 stale | CLOSED (round 2) — and now doubly moot: the live gate ran and #491 is closed; CLAUDE.md reflects it. |
| U-2 (Med) | Live-validation gate built but not fully run | CLOSED — two full documented live runs since: (1) R2-06 T12, 2026-07-13, against a real 0.2.0 gateway + real AVEVA Historian over VPN — 6/6 after finding + fixing the StorageRateMs provisioning bug (4da2d62b) and driving the Boolean→Int2 remap (STATUS.md §"R2-06 live gate"); (2) the continuous-historization gate 2026-08-01→04 (#491) — outbox/drain/WriteLiveValues/EnsureTags/read-back all green with distinct non-zero constants (docs/plans/2026-08-01-continuous-historization-live-gate.md). The retype-policy question is answered (upsert; supported-type retype accepted). |
| U-3 (Med) | WriteSecured / VerifiedWrite user identity stubbed at zero | STILL OPEN — CurrentUserId = 0, VerifierUserId = 0 (GatewayGalaxyDataWriter.cs:284-285). Unchanged since round 1: writes to secured/verified-classification tags remain effectively unsupported, with no session-identity→ArchestrA-user mapping and no fail-fast. The correlated statuses (S-1) at least make a rejection visible now (Bad instead of phantom Good). |
| U-4 (Med) | Dormant-paths inventory | PARTIALLY CLOSED — GetMemoryFootprint/FlushOptionalCachesAsync deleted fleet-wide (#525, b8154469 — grep-zero in GalaxyDriver.cs). Still open: per-subscription replay vs. the gateway's batched ReplaySubscriptionsCommand (note survives at GalaxyDriver.cs:446-447); maxEvents never forwarded on the wire + defensive client-side source filter (GatewayHistorianDataSource.cs:152-157) — both still gateway gaps to track against the sidecar repo; RefreshConnectionStateAsync (S-6); IHostConnectivityProbe.GetHostStatuses still unconsumed (Gitea #521, GalaxyDriver.cs:724). |
| U-5 (Low) | Outbox serializer has no version/format byte | STILL OPEN — fixed positional layout, no version prefix (HistorizationOutboxEntrySerializer.cs:14-16,23-40). The window to add one byte cheaply is closing as deployments accumulate on-disk logs. |
| U-6 | Test-coverage thin spots | LARGELY CLOSED — +2,304 test lines across 30 files this period. The advise-failure status path is now covered offline (GatewayGalaxyDataWriterFailClosedTests via IMxWriteOps); outbox prefix-removal semantics pinned (FasterLogHistorizationOutboxTests.cs:55-99,288); pump fault/replay-gap/bounded-channel suites exist; recorder gates all pinned (capture-gate re-read, quarantine falsification control, batch-append prefix accounting). Remaining thin spots: the S-3 read-hang case; a C-6 unknown-family filter assertion. |
| U-7 (Med) | 0.2.0 DataType presence change silently altered Boolean provisioning |
CLOSED — and resolved differently than the round-2 remediation pinned: the live gate exposed Int1 as server-degenerate, so Boolean → Int2 (HistorianTypeMapper.cs:31, issue #441), retyping cleanly from pre-existing Float tags. StorageRateMs is stamped (GatewayTagProvisioner.cs:72 — its absence had silently failed every provision, found live). Live gate 6/6; retype policy answered (accepted-retyped). |
New findings this pass: S-12 (Medium), S-13 (Low), C-8 (Low) — see body.
What changed since 2026-07-12 (delta assessment)
- The Galaxy write path went from optimistic to correlated (06/S-1 arc, closed). The shape that shipped is stronger than the round-2 partial: the gateway now correlates the worker's
OnWriteCompleteinto every unary write reply, so the driver's reply translation carries the real COM-side commit verdict, including refusals — with an explicit per-codeMxStatusDetailtable and an anti-truncation invariant on the mapping seam (the live gate caught1007 → low-byte 239 → Good, i.e. a REFUSED write mapping to Good;StatusCodeMap.cs:120-133is the fix). The subscription-handle borrow path was deleted outright after live captures showed a plain-advised handle can never carry a no-login write (SECURITY_ERROR 1008) and cannot be re-advised (E_INVALIDARG). - The continuous-historization pipeline was live-gated and hardened end-to-end (#491/#537/#538, all verified in code):
Selfcaptured before theConfigureAwait(false)await (ContinuousHistorizationRecorder.cs:369-382); capture gated on the per-cluster Primary viaIHistorizationRoleView(registered unconditionally on driver nodes,Runtime/ServiceCollectionExtensions.cs:99— the #511 registration accident avoided); poison-tag quarantine keyed on "did another tag write this pass" with the falsification control (:767-806); shutdown cancellation mid-drain rethrown so it aborts the pass instead of counting as a per-tag failure (:707-716,7870c91f). Perf landed with correctness intact: batch ingest with one group commit per burst (FasterLogHistorizationOutbox.AppendBatchAsync, incremental bookkeeping:119-137), bounded-parallel per-tag drain (DrainConcurrency8, fold on the single drain flow:741-758),DrainBatchSize64→1024. - v3 re-keyed the Galaxy driver to RawPath identity (
636c755b): forward/reverse RawPath⇄attributeRef tables built once from the artifact (GalaxyDriver.cs:247-277), identity on a miss, with the skipped-tag ledger (#541) accounting for undialable entries. Alarm ConditionId, ack routing, discovery and write security resolution all translate consistently (:964-972,:1298-1310,:1354). - Wire robustness grew: the replay-gap sentinel is handled ahead of channel pressure with a coalesced, non-blocking re-snapshot (
EventPump.cs:153-162,GalaxyDriver.cs:1125-1173— deliberate contract defence for a path the current configuration cannot hit); sparse arrays decode with gateway-mirrored fill semantics (MxValueDecoder.cs:96-166); an unrepresentable value now publishesBadTypeMismatchinstead of a Good null (EventPump.cs:336-353— the "good null is indistinguishable from a real null" fix).
Maturity Ratings
| Dimension | Rating (1-5) | Prior | Justification |
|---|---|---|---|
| Stability | 4.5 | (4) | The two headline stability debts both closed and were live-gated: Galaxy writes are fail-closed + correlated (S-1), and the historization pipeline survived a real gate that found and fixed three production-fatal defects (#491/#537/#538). The pump now detects server-completed streams. Docked half a point for the surviving cluster: the S-3 read hang, EventPump-only fault detection for subscription-less drivers (S-4), the dormant health snapshot (S-6), and the new advise-cache poisoning edge (S-12). |
| Performance | 4.5 | (4) | Group-commit batch append (one fsync per burst), bounded-parallel per-tag drain, a 16×-wider drain batch, and the 03/S3 HistoryRead parallelism/limiter/deadline knobs — all with the correctness discipline intact (all-or-nothing ack, outage-vs-poison discrimination). Remaining docks are gateway-constrained and documented: the 3-round-trip read synthesis (P-1) and serial alarm sends (P-2). |
| Conventions | 4.5 | (4) | C-4, C-5, C-7 closed; the IMxWriteOps seam and the secret: arm follow the house patterns exactly; status-code constants are named + reflection-guarded (#497); in-source rationale quality (the anti-truncation invariant, the keep-populating Quality decision, the replay-gap unreachability note) is exemplary. Docked for the surviving Low trio: seam wire-type leakage note (C-3), the unmetered pump family filter (C-6), and the unlatched quarantine warning (C-8). |
| Underdeveloped areas | 4 | (3) | The live gates that defined this dimension's debt have now run — twice — and each found real defects offline tests missed (StorageRateMs, Int1 degeneracy, the ConfigureAwait/Self restart, dual-capture, poison-tag pinning); U-2/U-7 closed, dead surface pruned (#525). Remaining: WriteSecured identity still stubbed (U-3), the dormant health-snapshot reader (S-6), the outbox version byte (U-5), and the gateway-gap workarounds tracked cross-repo. |
Findings
1. Stability
S-12 (Medium, NEW) — A transport failure during AdviseSupervisory poisons the supervised-handle cache; subsequent writes skip the advise until the next reconnect
EnsureSupervisoryAdvisedAsync marks the handle supervised before the round-trip (_supervisedHandles.TryAdd, GatewayGalaxyDataWriter.cs:234) and removes the mark only on the non-Ok reply path (:251-261). If ops.InvokeAsync throws instead — transport blip, gateway restart, cancellation — the exception propagates to WriteOneAsync's catch (:192-197, this write correctly returns BadCommunicationError) but the handle stays marked supervised. Every subsequent write to that tag then takes the "already advised" fast path (:234) and issues a raw Write against an item that was never supervisory-advised:
- Against a correlated gateway (the production posture), each write is refused with SECURITY_ERROR 1008 →
BadUserAccessDenied— visible, but never self-healing: nothing clears the stale cache entry on a 1008 row, so the tag's writes fail until a session reconnect happens to runInvalidateHandleCaches(:67-71). - Against a pre-correlation gateway, the reply's statuses are empty → provisional Good (
:330-331) — the exact S-1 phantom-write shape, resurrected for this one edge.
This is the classic one-fault-variant gap: the fail-closed fix was verified against the reply-failure variant (which the fail-closed tests cover) but not the throw variant. Fix: wrap the invoke in try/catch and TryRemove the handle on any failure to complete the advise; optionally also clear the entry when a write's correlated status comes back 1008 (self-heal). One unit test on the throw path via the existing IMxWriteOps fake closes it.
S-2 (Medium) — EventPump saturation still drops the newest events
Unchanged (EventPump.cs:164-170): TryWrite against the bounded channel preserves 50 000 stale queued events and discards the just-arrived one. For last-value-wins telemetry the bias is inverted. Metered (galaxy.events.dropped). Recommendation stands: per-item-handle conflation (keep newest per handle) or drop-oldest ring semantics.
S-3 (Medium) — ReadViaSubscribeOnceAsync can still wait forever on a non-cancellable token
Unchanged (GalaxyDriver.cs:899-915): the Bad-fill for still-pending snapshots runs only from cancellationToken.Register. A caller passing CancellationToken.None (or a never-firing token) with a successfully-subscribed tag that never publishes its initial event hangs the read and holds the subscription open. StatusCodeMap.BadTimeout is now the correct constant (#497) — but nothing arms it without a caller token. Recommendation stands: race each pending TCS against an internal deadline derived from PublishingIntervalMs/call-timeout regardless of the caller's token. Still untested (the read suite always emits or cancels).
S-4 (Medium) — Transport-failure detection is still EventPump-only; a subscription-less driver never degrades
ReportTransportFailure retains its single production caller (GalaxyDriver.cs:1196). The pump-side coverage improved: a server-completed stream now notifies the supervisor instead of dying silently (EventPump.cs:180-188 — explicitly written against the #527 watchdog's non-Healthy requirement). But the pump only exists once something subscribes or reads; a write-only or idle-after-discovery driver whose gateway restarts stays Healthy with per-request BadCommunicationError, and neither the supervisor nor the #527 watchdog (which requires a non-Healthy state) engages. Recommendation stands: report classified transport exceptions from the write/subscribe unary paths into the supervisor (idempotent, cheap).
S-5 (Low, was Medium) — Outbox RemoveAsync prefix-truncation: now pinned and relied upon; the silent-drop residual is observability only
The truncate-through-the-id semantics (FasterLogHistorizationOutbox.cs:203-227) are now an explicit, tested contract (FasterLogHistorizationOutboxTests.cs:99) that the recorder's all-or-nothing ack deliberately builds on (ContinuousHistorizationRecorder.cs:677-680) — the round-1 worry ("a future recorder change acks mid-batch") is fenced by documentation at both ends plus the #538 design. Residual: entries dropped by a prefix removal still don't count on DroppedCount. One counter increment in the prefix loop would make the ledger honest; low priority.
S-6 (Medium) — The historian health snapshot is a write-only ledger: no refresh caller, and now provably no snapshot reader either
RefreshConnectionStateAsync (GatewayHistorianDataSource.cs:219-244) still has zero production callers (only GatewayHealthSnapshotTests.cs:33,59,74), so ProcessConnectionOpen/EventConnectionOpen are permanently false in a deployed host. Sharpened this pass: GetHealthSnapshot() itself has no production consumer — the interface member (IHistorianDataSource.cs:117) and the Null impl are the only other references in src/. Third review in a row for this built-but-never-wired instance. Either wire a periodic refresh + a reader (the R2-10 resilience-status flow is the exact template: publisher service → DPS/gRPC → AdminUI chip), or delete the snapshot surface. If kept dormant, at least a registration-time guard test (the R2-10 ResilienceStatusReaderRegistrationTests pattern) should pin whichever decision is made.
S-7 (Low) — Alarm feed reconnect still has no backoff
Fixed 5 s forever (GatewayGalaxyAlarmFeed.cs:46,141), still inconsistent with DeployWatcher/ReconnectSupervisor's capped exponential. Cosmetic at fleet scale; align when next touched.
S-8 (Low) — GalaxyMxSession state unsynchronized across reopen (accepted)
_session/_connected remain plain fields (GalaxyMxSession.cs:29-31); the emergent safety (single-flight recovery, per-call catch → BadCommunicationError, cache invalidation post-reopen) held through this period's changes, and the batch-scoped SessionMxWriteOps binding (GatewayGalaxyDataWriter.cs:115-122) keeps a mid-batch reconnect contained to Bad results. Accepted; the invariant is now partially documented (:46-48).
S-13 (Low, NEW) — Two RawPaths authored to the same Galaxy attributeRef: the second node silently never receives a value
BuildRawTagTables is first-wins on the reverse map (GalaxyDriver.cs:272-273 — comment-only), and OnPumpDataChange reverse-translates every event through ToRawPath (:316-317, :1394), so all data-change fan-out for a shared attributeRef surfaces under the first RawPath. The second raw tag dials fine (forward map, writes work) but its node never publishes — stuck on BadWaitingForInitialData with nothing logged and no skip-ledger entry (#541's exact silence, one layer up: the tag is "bound" yet dead). Nothing upstream prevents authoring the duplicate (two folders can each hold a tag dialing the same tag_name.Attribute). Minimum fix: log a warning and record the loser in the SkippedTagLedger ("duplicate attributeRef — value fan-out surfaces under {winner}"); better: fan the reverse map out to all RawPaths sharing the ref (the historian recorder's _refMap multi-name pattern, ContinuousHistorizationRecorder.cs:540-554, is the in-repo precedent).
S-9 (Positive) — Store-and-forward + outbox crash-safety discipline extended under the perf work
The batching arc kept every crash-safety property while removing the per-value fsync: incremental per-entry indexing means a mid-batch fault strands nothing untracked (FasterLogHistorizationOutbox.cs:119-137 — the follow-up 73205865 fixed exactly the deferred-indexing variant); the commit-residual is documented at the seam (:145-149); drop-oldest runs once per batch (:153-181). The alarm writer's shutdown short-circuit and never-throw classification stand (GatewayAlarmHistorianWriter.cs:66-78,125-156). The recorder's ingest is hardened per-item (a faulting capture-gate delegate cannot drop the burst tail or restart the actor — ContinuousHistorizationRecorder.cs:385-423) and the append failure path explicitly refuses to restart-loop under a persistent disk fault (:446-467).
S-10 (Positive) — TLS / API-key posture unchanged and extended
CA pinning, the documented inverted TLS mapping and the h2c TLS-only-options guard (HistorianGatewayClientAdapter.cs:60-79), keys never logged, ${secret:} support on ServerHistorian:ApiKey, and the Galaxy secret: arm resolving fail-closed through the shared store with the real resolver wired in production (DriverFactoryBootstrap.cs:175).
2. Performance
P-1 (Medium) — Galaxy Read still costs three gateway round-trips plus a publish wait
Unchanged and gateway-constrained (GalaxyDriver.cs:805-941). Correctly batched per request; "subscribe, don't poll" remains the supported pattern.
P-2 (Medium) — Alarm-history drain is still one unary SendEvent per event
Unchanged (GatewayAlarmHistorianWriter.cs:66-78) — deliberate poison-event isolation; still no batched SendEvents with per-event status rows on the client surface. Accepted, documented ceiling; adopt a batched RPC when the gateway grows one.
P-3 (Low) — Independent channels per path (deliberate)
Unchanged; the trade-off is argued in-source and every owner now disposes its channel (the provisioner joined this period, GatewayTagProvisioner.cs:108-115). No action — still noted so nobody "optimizes" it into a shared-singleton dispose bug.
P-4 (Positive) — Fan-out paths indexed, bounded, metered — now including the drain
SubscriptionRegistry reverse maps, BuildResultIndex, and the bounded pump channel stand. New this period: the recorder's per-tag drain groups run under a bounded SemaphoreSlim window with the outcome fold deliberately kept on the single logical drain flow so the #538 counters can't be corrupted by concurrent mutation (ContinuousHistorizationRecorder.cs:696-758); ingest is one group commit per burst; Task.WhenAll's complete-before-fault semantics are used to make the using-scoped semaphore safe on the cancellation path (:736-739).
P-5 (Low) — Events reads: client-side cap + truncation landed; maxEvents <= 0 still unbounded; the cap still never reaches the wire
ReadEventsAsync now stops draining at the cap and signals truncation with an opaque token (GatewayHistorianDataSource.cs:142-175). Remaining: maxEvents <= 0 disables the cap entirely (:142-143 — the gateway's remote RuntimeDb:EventReadMaxRows is then the only bound), and the adapter still cannot forward the cap on the wire (HistorianGatewayClientAdapter.cs:100-113). Clamp <= 0 to a server-side default; keep tracking the wire-side cap against the sidecar repo. The 03/S3 parallelism/limiter/deadline knobs (ServerHistorianOptions.cs:80-97, validated at :133-138) meaningfully bound the surrounding fan-out.
P-6 (Low) — Outbox peek still holds the state lock across the log scan — and the batch is 16× wider now
PeekBatchAsync scans under _state (FasterLogHistorizationOutbox.cs:190-197); DrainBatchSize default rose 64→1024 (ContinuousHistorizationOptions.cs:78), so a concurrent AppendBatchAsync's per-entry index updates can now stall behind a 1024-entry scan. Mostly page-cached; fine today. If drain batches grow again, snapshot _nextScanAddress under the lock and scan outside it.
3. Conventions
C-1 (Positive) — Secret handling remains the model
GalaxySecretRef now resolves env: / file: / dev: / secret: / literal, fail-closed on the secret: arm through the shared ZB.MOM.WW.Secrets resolver (Galaxy.Contracts/GalaxySecretRef.cs), shared by driver and browser. ServerHistorian:ApiKey documents env-or-${secret:} delivery (ServerHistorianOptions.cs:40-48). No key value logged on any path.
C-2 (Positive) — Seam quality improved again
The IMxWriteOps internal seam (GatewayGalaxyDataWriter.cs:342-405) is the right-sized answer to a sealed SDK: production binds a zero-translation adapter, tests drive the real single-write pipeline (WriteOneForTestAsync, :142-144) — precisely the design that lets the fail-closed behaviour be pinned offline. IHistorianGatewayClient remains the single historian seam with every consumer behind it.
C-3 (Low, was Medium) — The historian seam still leaks wire types by design
IHistorianGatewayClient.cs:5-11 unchanged; no guard note added. Downgraded: the risk the round-2 bump demonstrated is now institutionally covered — presence-semantics pinned by unit tests (GatewayTagProvisionerTests.Boolean_definition_carries_explicit_Int2_presence) and the live suite has become a routine bump gate. Keep future consumers above GatewayHistorianDataSource; add the one-line guard note when next editing the file.
C-6 (Low) — EventPump unknown-family filter still unmetered
Dispatch's default arm returns uncounted (EventPump.cs:297-312). Narrowed by the new replay_gaps/undecodable counters (a sentinel or bad payload is no longer invisible), and filtered volume is derivable from the counter arithmetic — but a first-class galaxy.events.filtered counter (mirroring the alarm feed's decode-failure counter) is still the clean close, and no test asserts the filter.
C-8 (Low, NEW) — The poison-tag abandonment warning repeats every drain pass with no latch
Once a tag is over its MaxWriteAttempts budget, each subsequent pass (2 s default cadence) that captures new entries for it re-abandons and re-logs the full Warning (ContinuousHistorizationRecorder.cs:808-819 — _tagWriteFailures is never reset on abandonment, so stillBlocking stays false and the branch re-fires per pass): ~30 warnings/minute per poison tag until it is fixed or de-historized. The recorder already has the right idiom in-file — the ingest-fault Warning-then-Debug latch (:404-416). Apply it per tag.
4. Underdeveloped Areas
U-3 (Medium) — WriteSecured / VerifiedWrite user identity is still stubbed at zero
GatewayGalaxyDataWriter.cs:284-285 unchanged across all three reviews. Any galaxy that actually classifies tags SecuredWrite/VerifiedWrite will reject or mis-attribute user 0; there is still no mapping from the OPC UA session identity (now a rich, ACL-carrying principal since #520) to an ArchestrA user id, and no fail-fast. The S-1 correlation at least makes the rejection visible as Bad now. Decide: document as unsupported + fail fast with a distinct status, or build the identity mapping. This is the domain's oldest open Medium.
U-4 (Medium → Low) — Dormant-paths inventory, pruned
Closed this period: GetMemoryFootprint/FlushOptionalCachesAsync deleted (#525). Remaining, all tracked or gateway-side: batched ReplaySubscriptionsCommand swap (GalaxyDriver.cs:446-447), wire-side maxEvents + server-side source filter (sidecar backlog), RefreshConnectionStateAsync/GetHealthSnapshot (S-6), IHostConnectivityProbe (Gitea #521).
U-5 (Low) — Outbox serializer still has no version/format byte
HistorizationOutboxEntrySerializer.cs:14-16,23-40 unchanged. Now that Capacity defaults non-zero and production deployments accumulate on-disk logs, a layout change would break RecoverState mid-scan with an undiagnosable failure. One reserved byte remains free today; a migration later is not. Third listing.
U-6 — Test coverage assessment
Substantially improved: +2,304 lines / 30 files across the three gateway test projects since f6eaa267. Now covered: the fail-closed advise path and unconfirmed-write metering (offline via IMxWriteOps), RawPath keying, replay-gap handling, stream-completion fault, sparse-array/undecodable decode, outbox batch-append prefix accounting + prefix-removal pinning + serializer round-trips, value-writer quality/timestamp mapping, recorder gate/quarantine/falsification controls (Runtime.Tests). Live suites: Galaxy reopen-and-write + alarm feed + write meters (env-gated), historian LiveIntegration 6/6. Remaining thin spots: S-3's read-hang (no test passes CancellationToken.None with a never-publishing subscribed tag); C-6's family filter; and the new S-12 advise-throw path (one IMxWriteOps fake test).
Cross-Cutting Themes
- The live gate is now this domain's most productive tool — keep running it. Every closure this period that mattered was live-gate-driven, and each gate found defects that 100+ green unit tests coexisted with: the StorageRateMs=0 silent-total-provisioning-failure, Int1 server-degeneracy, the
ConfigureAwait(false)/Selfactor kill, dual-node double-capture, the 1007→Good truncation, and the plain-Write/borrow-path gaps in the original S-1 brief. The offline suites are strong, but the pattern is consistent: mapping seams and fire-and-forget boundaries only reveal themselves against the real peer. - "Fixed for the reply-failure variant, not the throw variant" is the residual failure mode. S-12 is exactly this: the fail-closed advise handles a non-Ok reply but not a thrown invoke, leaving a poisoned cache. When hardening any request/reply seam here, enumerate both failure shapes (and cancellation) explicitly — the house's one-fault-variant trap.
- Built-but-never-wired is down to one instance — but it's on its third review. The provisioner (PR #423), the sink forwarding (F10b), and the resilience tracker (R2-10) all got readers or guards;
GetHealthSnapshot/RefreshConnectionStateAsync(S-6) is the last holdout, and this pass established the entire snapshot surface is unread. Wire it or delete it; either way pin it with a registration guard. - Silent-partial-binding is the next #541. The skipped-tag ledger closed the "N authored, fewer bound, nothing says why" gap at the artifact boundary — but S-13 shows the same silence one layer deeper (bound-but-never-publishing on a duplicate attributeRef). When a first-wins/last-wins tiebreak decides which of two authored things works, the loser must be logged and ledgered.
- Gateway-gap workarounds still accrue client-side — wire-side
maxEvents, server-side event source filter, batched replay, batchedSendEvents, and the WriteComplete correlation's completion-wait timeout (thegalaxy.writes.unconfirmedresidual). All are known and metered/documented here; they belong on the sister repos' backlogs with links from the in-source comments.