EffectiveCipPath now references ParsedAddress/Profile properties instead
of the captured primary-constructor parameters to avoid CS9124 (param
captured into enclosing type AND used to init a member).
NonZero_libplctag_status_maps_via_AbLegacyStatusMapper updated to pass
(int)Status.ErrorNotFound rather than the stale magic integer -14 that
the old mapper happened to handle but the new enum-based mapper does not.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 Medium findings resolved (-002 through -012); 3 Low findings remain
open (-005, -011, -013).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consume previously-dead AbLegacyPlcFamilyProfile fields:
- DeviceState.EffectiveCipPath applies DefaultCipPath when the parsed host
address has an empty CIP path (SLC 500 / PLC-5 misconfigured without /1,0
now gets the profile-supplied default route). All three tag/parent/probe
Create() callers updated.
- InitializeAsync validates each tag's DataType against SupportsLongFile /
SupportsStringFile and throws InvalidOperationException at init time so a
MicroLogix Long tag or similar fails early rather than at runtime with an
opaque comms error.
- MaxTagBytes tracked as a follow-up (string/array chunking requires broader
design work).
Tests added for CipPath fallback and Long/String type validation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MapLibplctagStatus now casts the int to libplctag.Status and switches on
named enum members (mirroring AbCipStatusMapper) instead of unverified
magic integers. A strongly-typed Status overload is the canonical path;
the int overload delegates to it. MapPcccStatus is retained with a comment
marking it as the reference mapping for future PCCC-STS inspection.
Tests updated to use Status enum members rather than raw integers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
InitializeAsync catch block now mirrors ShutdownAsync teardown: cancels
and disposes probe CancellationTokenSources, calls DisposeRuntimes, and
clears _devices/_tagsByName before rethrowing. A caller that catches and
abandons (rather than retrying via ReinitializeAsync) no longer leaves
orphaned probe tasks or libplctag handles alive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark _health volatile. The record-reference assignment is atomic, but
without an acquire/release memory barrier GetHealth() on another thread
can observe a stale snapshot indefinitely. volatile enforces the barrier
at read and write sites without a lock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runtimes and ParentRuntimes changed from Dictionary to ConcurrentDictionary.
EnsureTagRuntimeAsync and EnsureParentRuntimeAsync now use a per-key
GetCreationLock semaphore with a double-checked pattern: fast-path read
requires no lock; slow-path create+initialize+store is serialised per key
so a concurrent caller waits rather than creating a duplicate runtime that
would be leaked when DisposeRuntimes runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DecodeValue for Bit with no bitIndex now reads the full 16-bit word via
GetInt16(0) and tests bit 0 instead of GetInt8(0), which only covered the
low byte and silently misread any bit in positions 8..15. The comment
explains the two decode paths (suffix-present vs suffix-absent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TryParse now rejects three classes of malformed PCCC address:
- Sub-element + bit-index together (e.g. T4:0.ACC/2) — never valid in PCCC
- File number on I/O/S system files (e.g. I3:0, S2:1) — single-letter only
- Sub-element on non-T/C/R files (e.g. B3:0.DN, N7:0.FOO) — only Timer,
Counter, and Control files carry structured elements
New helper predicates IsNoFileNumberLetter / IsSubElementFileLetter
keep the parser's intent clear. Regression tests added in AbLegacyAddressTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`current & widthMask` was already applied in `WriteBitInWordAsync` by
the -001 High finding fix, making the 16-bit sign-extension hazard fully
neutralised. No further code change required; mark Resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WriteCommand.ParseValue wraps FormatException/OverflowException as
CliFx CommandException so a bad --value yields a clean one-line CLI error
naming the value and target type instead of a raw .NET stack trace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Driver.AbCip.Cli-001: WriteCommand.ParseValue wraps FormatException/
OverflowException as CommandException so bad --value input yields a clean
CLI error instead of a raw stack trace.
Driver.AbCip.Cli-002: probe/read/subscribe commands reject Structure types
up front (RejectStructure helper), matching the write guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added missing test coverage identified in the -005 finding:
- FormatTable_with_empty_input_returns_header_only: verifies the -004 fix
(empty batch read returns header+separator rather than throwing).
- FormatStatus_with_sub_code_bits_resolves_to_named_class: Theory exercising
the -002 high-word mask path (e.g. 0x80050001 → "BadCommunicationError").
- FormatStatus_unknown_sub_code_falls_back_to_severity_class: Theory for the
-002 severity-class fallback (unknown sub-codes still emit Good/Uncertain/Bad).
- New DriverCommandBaseTests class: four tests covering verbose/non-verbose
Serilog level selection, ConfigureLogging idempotency, and FlushLogging.
Also corrected the stale FormatStatus_unknown_codes_fall_back_to_hex_only
expectation (0xDEADBEEF now resolves to "Bad" via the severity-class fallback
introduced by -002, not bare hex) and fixed the FormatTable empty-input crash
(guard rows.Length == 0 before calling Enumerable.Max).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ConfigureLogging is now idempotent via a _loggingConfigured guard field so
repeated calls from subclasses do not abandon and leak the previous logger.
The previous Log.Logger is disposed before overwriting to release its
console-sink resources cleanly.
A new protected static FlushLogging() helper calls Log.CloseAndFlush() so
commands can guarantee buffered output is flushed in their finally blocks
before the process exits — important for the long-running subscribe verb.
XML doc updated to reflect call-once semantics and document FlushLogging().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FormatStatus now matches named codes against code & 0xFFFF0000 (high-word
mask) rather than exact equality, so status codes carrying sub-code or flag
bits in the low 16 bits (e.g. 0x80050001) still resolve to their named class.
For codes not in the named shortlist a severity-class fallback using the top
2 bits always emits Good / Uncertain / Bad rather than bare hex.
Updated the stale FormatStatus_unknown_codes_fall_back_to_hex_only test (its
expectation became invalid once the severity-class fallback was added) and
added new Theory cases exercising both the high-word matching and the
severity-class fallback paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core.VirtualTags-002: cold-start guard publishes BadWaitingForInitialData
instead of silently returning a stale value.
Core.VirtualTags-003: Load detects duplicate Path values and keys the
upstream-subscription loop off the registered tag set.
Core.VirtualTags-005: VirtualTagSource fires the initial-data callback per
path before registering the change observer, fixing an ordering race.
Core.VirtualTags-008: DependencyGraph caches topological rank, lowering
per-change-event cost from O(V+E) to O(closure).
Core.VirtualTags-012: added 9 engine tests; CoerceResult null-return now
maps to BadInternalError as the code comment intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Core.Abstractions-001: PollGroupEngine compares array values with structural
equality so a driver returning a fresh T[] each poll no longer fires spuriously.
Core.Abstractions-002: PollOnceAsync guards reader result cardinality and
throws a descriptive InvalidOperationException on mismatch instead of a
swallowed ArgumentOutOfRangeException that stalled the subscription.
Core.Abstractions-003: the poll loop Task is tracked; Unsubscribe/DisposeAsync
await loop completion before disposing the CTS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mark Core.ScriptedAlarms-002, -004, -005, -007, -012 as Resolved with
one-line descriptions. Update open-findings count from 11 to 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add engine-level tests covering the six gaps identified in the finding:
(1) timed-shelve auto-expiry driven via injectable clock + RunShelvingCheckForTest
hook so timer tests are deterministic;
(2) ConfirmAsync, TimedShelveAsync/UnshelveAsync round-trip, EnableAsync engine
methods exercised end-to-end;
(3) OnEvent subscriber-throws isolation — engine state advances and stays
operational after a subscriber throws;
(4) IAlarmStateStore.SaveAsync failure leaves in-memory state unchanged (locks in
the persist-before-update invariant from finding-007);
(5) second LoadAsync does not leak the old timer (regression for finding-002);
(6) AreInputsReady cold-start guard correctly blocks on Bad/missing inputs and
allows Uncertain-quality inputs through.
Expose RunShelvingCheckForTest() internal method on ScriptedAlarmEngine to
support deterministic timer tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SubscribeAsync now wraps each driver handle in a private HostBoundHandle
that carries the resolved host name. UnsubscribeAsync unwraps it and
routes through the recorded host's resilience pipeline, correctly
charging the subscription's originating host's circuit breaker/bulkhead
instead of always using the default host. Falls back to the default
host for handles not created by this invoker. Two regression tests
added; update findings.md Open count from 10 to 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BuildAddressSpaceAsync now checks _disposed (throws ObjectDisposedException)
and tears down the previous alarm forwarder + clears the sink registry
before re-walking, so a Galaxy-redeploy rebuild does not leak the old
forwarder and double-deliver alarm transitions. Three regression tests
added: double-build does not double-fire, sink count is correct after
rebuild, and post-dispose call throws.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Change ClusterEntry from sealed record to sealed class so TryUpdate
uses reference equality for the CAS comparison. Prune now uses a
read-compute-TryUpdate retry loop that restarts when a concurrent
Install updates the entry between the read and the write, preventing
a race that could silently drop the just-installed newest generation.
Two regression tests added to PermissionTrieCacheTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add FolderSegment member to NodeAclScopeKind; update WalkSystemPlatform
to report NodeAclScopeKind.FolderSegment (not Equipment) for each
visited Galaxy folder level, so MatchedGrant.Scope in
AuthorizationDecision.Provenance correctly distinguishes Galaxy folder
grants from UNS Equipment grants in the audit trail and Admin UI
diagnostics. Three regression tests added to PermissionTrieTests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorder persist/update in ApplyAsync, ReevaluateAsync, and ShelvingCheckAsync:
SaveAsync is now called before the in-memory _alarms entry is advanced. A store
failure therefore leaves both the persisted and in-memory views at the prior state
rather than diverging, maintaining the invariant that startup recovery reflects
actual persisted state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add _disposed re-checks inside ReevaluateAsync and ShelvingCheckAsync after
acquiring _evalGate so callbacks in flight when Dispose() runs bail out cleanly
instead of mutating _alarms or writing to a disposed store. Drop the
_alarms.Clear() from Dispose() — clearing outside the gate races concurrent
reads and is unnecessary since the object is being discarded.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split the LoadAsync seed-read + subscribe loop: ReadTag seed fills _valueCache
first, then persisted-state restore runs, then _loaded = true, then SubscribeTag
is called. Any synchronous initial push from the upstream now arrives after
_alarms is fully initialised and _loaded = true, so ReevaluateAsync will queue
correctly behind the gate rather than racing the half-built state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dispose any existing _shelvingTimer before reassigning it inside LoadAsync so
that a second LoadAsync call does not leak the old timer and leave two timers
running concurrently against the same engine state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration-002: sp_PublishGeneration is transaction-nesting aware
(BEGIN TRANSACTION vs SAVE TRANSACTION on @@TRANCOUNT) so a caller's outer
transaction survives a publish failure; sp_ValidateDraft wrapped in TRY/CATCH.
Configuration-003: ValidatePathLength uses the cluster's actual Enterprise/Site
lengths when available, falling back to the conservative approximation.
Configuration-006: ResilientConfigReader treats a command-timeout
TaskCanceledException as a fault (not caller cancellation) and falls back.
Configuration-009: removed the checked-in plaintext sa connection string;
CreateDbContext now requires OTOPCUA_CONFIG_CONNECTION.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client.Shared-001: lowered the OnAlarmEventNotification early-return guard
from <6 to <1; per-index field guards already default missing fields safely.
Client.Shared-002: GetRedundancyInfoAsync replaces unguarded unboxing casts
with StatusCode.IsGood + Convert.ToInt32/ToByte, defaulting on bad reads.
Client.Shared-007: alarm fallback Task.Run guards on ReferenceEquals(session,
_session) and drops stale alarms on ObjectDisposedException after failover.
Client.Shared-008: WriteValueAsync rejects type inference from bad/null reads;
ValueConverter wraps parse failures in a descriptive FormatException.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Client.CLI-001: parse --start/--end with CultureInfo.InvariantCulture and
DateTimeStyles.AssumeUniversal|AdjustToUniversal so dates are culture-stable.
Client.CLI-005: SDK notification callbacks now hand off to an unbounded
channel drained on the main thread; handlers are unsubscribed before the
summary phase so no notification interleaves with console output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Analyzers-001: IsInsideWrapperLambda now matches the wrapper method name
(ExecuteAsync/ExecuteWriteAsync) in addition to the containing type, so a
future non-callSite lambda overload cannot suppress the diagnostic.
Analyzers-006: extended StubSources and added coverage for the remaining
guarded interfaces, synchronous members, concrete-driver receivers,
ExecuteWriteAsync wrapping, and nested lambdas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Admin-006 commit added <AntiforgeryToken /> to the logout form
and updated the comment on the endpoint, but did not update LogoutAsync to
actually call IAntiforgery.ValidateRequestAsync. Blazor's UseAntiforgery()
middleware does not automatically validate minimal-API endpoints, so a
tokenless POST still succeeded. This commit injects IAntiforgery into the
handler, wraps ValidateRequestAsync in a try/catch, and returns 400 on
AntiforgeryValidationException. The endpoint keeps .DisableAntiforgery() to
prevent the middleware from also trying to read the body (which would cause
a double-read). The regression test is updated to log in first (to get an
authenticated session) before asserting 400 on a tokenless logout POST.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add AdminAuthPipelineTests (WebApplicationFactory + RoleInjectingHandler) to
enforce that ConfigViewer is denied CanPublish-gated pages while FleetAdmin is
permitted, and that an authenticated FleetAdmin session can reach the homepage.
Existing PageAuthorizationTests (anon page rejection) and AuthEndpointsTests
(login cookie + hub auth) cover cases (a)-(c); this file adds case (d).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add @ReleasedBy parameter to sp_ReleaseExternalIdReservation via a new EF
migration so the operator principal (not the shared SQL account) is recorded
in ExternalIdReservation.ReleasedBy and ConfigAuditLog.Principal.
ReservationService.ReleaseAsync gains a releasedBy parameter; Reservations.razor
resolves the signed-in user from AuthenticationState and passes it through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update status and resolution text for the five Medium findings resolved
in this batch; lower the Open findings count from 11 to 6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement IDisposable on MainWindowViewModel to detach ConnectionStateChanged,
call Teardown() on the subscription/alarm VMs, and dispose _service so the OPC UA
session and SDK resources are released. Call Dispose() from MainWindow.OnClosing
alongside the existing SaveSettings() call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove Password from UserSettings and stop writing it to settings.json;
the operator is re-prompted on each launch. Update LoadSettings/SaveSettings
comments and adjust the affected test assertion to verify the password is
not restored from the persisted model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NewCluster.razor and ClusterDetail.razor now resolve ClaimTypes.Name /
NameIdentifier from the cascaded AuthenticationState instead of hardcoding
"admin-ui" as the createdBy audit field. The operator principal is now
attributed correctly on every cluster-create and draft-create write path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Call Subscriptions?.Teardown() and Alarms?.Teardown() in the Disconnected
branch of OnConnectionStateChanged so server-side session drops also
quiesce the DataChanged and AlarmEvent handlers. Add Reattach() methods
that idempotently re-hook the handlers; call them from the Connected
branch so reconnects after a server-side drop restore live updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emit <AntiforgeryToken /> in the MainLayout sign-out form and remove
.DisableAntiforgery() from the /auth/logout endpoint so UseAntiforgery()
validates the token. A tokenless POST now returns 400, preventing CSRF-logout.
Regression-guarded by AuthEndpointsTests.Logout_without_antiforgery_token_is_rejected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Guard the two nullable child VM dereferences (BrowseTree at ConnectAsync
and History at ViewHistoryForSelectedNode) with != null checks, matching
the guarding style already used for Subscriptions and Alarms nearby.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route the synchronous IsLoading = true write through _dispatcher.Post so
both IsLoading assignments use the same dispatch path as Results.Clear()
and the final IsLoading = false, eliminating the ordering hazard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Timeout_maps_to_BadInternalError_without_killing_the_engine test's
"Hang" script busy-looped on Environment.TickCount64. Commit cfb9ff1
(Core.Scripting-001) added System.Environment to the script-sandbox
deny-list, so the script now fails sandbox validation instead of
reaching the timeout path. Switch the busy-loop to DateTime.UtcNow
(an allowed type) to preserve the test's intent — a self-terminating
~5s hang that overruns the 30ms script timeout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WonderwareHistorianClient.ReadAtTimeAsync passed the sidecar's reply.Samples
straight through ToSnapshots, which violated the IHistorianDataSource
contract: the result MUST be the same length and order as the requested
timestampsUtc, with gaps returned as Bad-quality snapshots. If the sidecar
dropped or reordered samples, OPC UA HistoryReadAtTime would silently
misalign values with timestamps.
Add an AlignAtTimeSnapshots helper that indexes the returned samples by
timestamp ticks, builds the result array at timestampsUtc.Count in request
order, and emits a Bad-quality (0x80000000) snapshot for any requested
timestamp the sidecar did not return.
Add the ReadAtTimeAsync_PartialAndReorderedReply_AlignsByTimestamp_AndFillsGapsAsBad
regression test where the fake returns a partial, reordered sample set.
Update code-reviews/Driver.Historian.Wonderware.Client/findings.md: -001
Resolved, open-finding count 10 -> 9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WriteToReadOnlyFile was listed in MalformedErrors, so ClassifyOutcome/
MapOutcome routed it to PermanentFail and the store-and-forward sink
dead-lettered every alarm event in the batch. But WriteToReadOnlyFile is
a connection-configuration fault (the write session was opened without
ReadOnly = false), not an event-payload fault — treating it as permanent
silently and permanently discards alarm events on a misconfigured or
regressed connection, which is data loss.
Move WriteToReadOnlyFile from MalformedErrors into ConnectionErrors. The
batch loop now aborts the batch, resets the connection (so the reconnect
path re-opens a writable ReadOnly = false session), and defers the
events as RetryPlease for the next drain tick.
Updated the ClassifyOutcome theory data and added a dedicated regression
test pinning WriteToReadOnlyFile -> RetryPlease.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DL205 family-native branch routed every V-prefixed address through
DirectLogicAddress.UserVMemoryToPdu, a plain octal-to-decimal decode.
DL205/DL260 system V-memory (V40400 and up) is not a simple octal decode:
the CPU relocates the system bank to Modbus PDU 0x2100. Octal-decoding
V40400 produced 16640 (0x4100), the wrong register, so any tag addressing
a system register through the grammar string silently read/wrote the
wrong PLC memory.
- Add DirectLogicAddress.VMemoryToPdu, which decodes the octal V-address,
detects the system bank (octal >= V40400 == SystemVMemoryOctalBase) and
relocates it through SystemVMemoryToPdu to PDU 0x2100; user-bank
addresses keep the plain octal decode.
- ModbusAddressParser's DL205 V branch now calls VMemoryToPdu instead of
UserVMemoryToPdu. UserVMemoryToPdu is retained for user-bank-only callers.
- Correct the ModbusFamilyParserTests V40400 assertion (16640 -> 0x2100)
and add system-bank regression cases plus direct helper coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_lastPublishedByRef was a plain Dictionary<string, object> mutated inside
ShouldPublish, which runs on the PollGroupEngine onChange callback. The engine
runs one background Task per subscription, so a driver with two or more
subscriptions invokes ShouldPublish concurrently on separate threads. Concurrent
TryGetValue/indexer writes on a non-thread-safe Dictionary can corrupt internal
state, drop entries, or throw, crashing the poll loop.
Switch _lastPublishedByRef to ConcurrentDictionary<string, object>; its
TryGetValue and indexer-set operations are individually thread-safe, so the
deadband cache is now correct under concurrent multi-subscription publishing,
consistent with the lock-guarded sibling cache _lastWrittenByRef.
Add an xUnit + Shouldly regression test that runs 24 deadband-configured
single-tag subscriptions concurrently and asserts the poll loop survives without
faulting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Driver.Galaxy-002 — DataTypeMap.Map had no Int64 arm though MxValueDecoder/
MxValueEncoder both fully support Int64. Galaxy attributes with the Int64
mx_data_type code fell through to the String default, creating a String
address-space node while runtime reads decoded a boxed long. Added
`6 => DriverDataType.Int64`, extending the contiguous 0..5 scheme so the type
map agrees with the decoder/encoder on all seven Galaxy data types.
Driver.Galaxy-008 — after a stream fault the EventPump's StreamEvents consumer
loop exited and its channel completed; EventPump.Start() is a no-op on a
completed-but-non-null loop, so a replayed subscription had no consumer and
ReplayAsync never re-registered the post-reconnect item handles. ReplayAsync
now recreates the EventPump (RestartEventPumpForReplay) and rebinds the
SubscriptionRegistry per subscription with the fresh item handles returned by
the post-reconnect SubscribeBulkAsync, via new SubscriptionRegistry.SnapshotEntries
and Rebind APIs.
Regression tests: DataTypeMapTests (every code incl. Int64), SubscriptionRegistry
Tests (Rebind/SnapshotEntries), EventPumpStreamFaultTests (faulted pump dead,
fresh pump resumes dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>