- Process-wide CipConnection statics (ForwardOpenCount, Forward Close counts, RequestsSent, exchange
stats, OpenAttempts, OpenAttemptBudget) move to an instance ConnectionDiagnostics owned by each
DeviceSessionPool (and by a stand-alone CipConnection). The open-attempt budget becomes a disposable
per-DeviceKey limit (LimitOpenAttempts); the differential harness scopes one to the press under test.
- LibplctagOrphanRelease.ForceRootingForTest is gone: ReleaseIfOrphaned(tag, canReadHandles) overload.
- A connected reply with general 0x01 / extended 0x0107 or 0x0203 (target no longer holds the class-3
connection, TCP still up) retires the session at once; the next send reopens through the throttle.
- New tests: dispose during RegisterSession, abandoned-open observer (falsified), scheduler-dispose-throws
guard (DeviceSessionPool.CloseAsync), release-in-background with a shared open in flight, per-device
limit scoping, CIP-close recycle (request + keep-alive + negative control).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
#562: direct event (alarm) monitored items are now tracked in a second map
(_eventItems, fed by the SubscribeToEvents override) and re-authorized by
RevalidateMonitoredItems on both triggers (DataPlaneAuthorityState.Changed and
AclRevalidationTimer). A denied item is detached via the SDK's own protected
SubscribeToEvents(unsubscribe: true) - never MonitoringMode.Disabled - and
re-attached on restore; a client delete of a stood-down item is absorbed.
Over-the-wire tests: revoke -> stop (positive control on a still-granted
notifier) -> restore -> resume; clock-driven expiry via the real timer with a
control session; delete-while-stood-down is not resurrected; unexpired
negative control. Server-object (all-events) subscriptions are documented as
outside the ACL (observed over the wire).
#563: DataPlaneAuthorizer.AuthorizeKeyUsable runs the FindUsableKey probe on
the WriteOperate and AlarmAck role gates regardless of Security:Acl:Enabled,
so an expired or deployment-disabled key loses its activation-time roles on
its open session. Anonymous (no key id) is unaffected; reads/subscriptions
stay ungated with the flag off. No new flag.
Docs: docs/security.md and the CLAUDE.md API-key/ACL section.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
#570
- STRING: the built-in 88-byte Logix STRING holds 82 characters (LEN + DATA[82] + 2 pad), not 84; the
write still sends the whole 88-byte structure and a read never decodes the pad as characters.
- Mistype churn: a type refusal (CIP 0xFF/0x2107) on the existence read no longer throws from
InitializeAsync, so the runtime is not disposed/re-created (and re-Forward-Opened) every poll; the driver
also keeps a runtime that throws TypeMismatch from init and records the device as reachable.
- Malformed tag name: a TagPath that does not parse, or a name the CIP symbolic encoder refuses (now also
non-ASCII, empty member, >255-char member, unterminated subscript), is AbCipTagConfigurationException ->
per-tag BadConfigurationError (named const in AbCipStatusMapper), warned once, never feeding the device
backoff or health; siblings keep reading. A malformed probe tag no longer feeds the backoff either.
- Micro800 BYTE/WORD/LWORD (0xD1/0xD2/0xD4) are type-checked as integers of their width: REAL/LREAL or a
narrower integer over LWORD is TypeMismatch, not decoded garbage.
#579
- ConnectionLease.ReleaseInBackground: one non-blocking release shape for both native AB runtimes. The
AbCip runtime's Dispose no longer waits up to 2 s under the per-tag lock; the pool still registers the
close synchronously so a re-create cannot overlap it. Offline soak tests (25 cycles, AbCip + AbLegacy)
prove no leaked connection; FakeEipServer gains OpenConnections.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
#572: listing continues at max(instance in page)+1 (no ascending-order assumption, no-progress guard kept);
browsePageDelayMs (default 5 ms) paces pages; DiscoverAsync reads templates through one batch reader
(IAbCipTemplateReaderFactory.CreateBatch) holding one lease.
#575: differential harness reports a same-status template refusal as an equal refusal; offline test pins the
open-attempt budget without relying on the abort ending the phase.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
A native TypeMismatch (read status, write gate, or CIP 0xFF/0x2107) used
to evict the runtime and degrade health on every poll; for a sole lease
holder that meant Forward Close + Forward Open per poll, and an all-
mismatched driver tripped the comms-loss watchdog. It now reports
BadTypeMismatch, keeps the handle, leaves health alone and warns once per
tag per instance — reads, whole-UDT reads and writes, plus the libplctag
width check. Tests (red against the pre-fix driver): single create over N
polls, unchanged health, one warning, and one Forward Open / zero Forward
Close over the real native runtime against FakeCipTarget. AbLegacy has no
TypeMismatch path (its native runtime has no type check).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The old AdminUI cannot author the per-driver transport key (#576), so the
'pin before upgrading' recipe had no working path. A node-level appsetting
(env Drivers__AbCip__DefaultTransport) now supplies the default for AbCip
drivers with no key: explicit key > node default > compiled Native. Bound
and validated unconditionally (bad value fails host start); the driver
factory and the Test Connect probe both read it so they agree. AbCip.md
upgrade recipe rewritten around the key; Configuration.md documents it.
Also documents first-wins for AllowPacking/probe and host-alias throttles
(final review M6) and moves the status-bit note out of AbCip.md (M5).
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
On a wire the native PCCC path is proven only for N-file reads/writes and
one F8 read (ab_server). An absent transport key now resolves to Libplctag
for every family; explicit Native still wins and still warns once per
instance on PLC-5/LogixPccc. AdminUI dropdown, CLI help, evidence claims
(PcccFileType, AbLegacy.md wire-evidence table, release note) corrected:
S:0 (default probe) and B/T/C/R/S/L/ST/A/O/I are unverified natively.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Native is the default, so a locally refused write must not look like a comms fault.
- AbLegacy: an AbTransportException with AbStatus.Unsupported (status-bit sub-element writes, PLC-5
non-N reads, all PLC-5 writes; libplctag's ErrorUnsupported maps here too) now returns
BadNotSupported on both the read and the write path (incl. the bit-RMW path). The tag handle is
not evicted and driver health is not touched. A native runtime whose init read is refused locally
is CACHED rather than disposed, so a poll of a refused tag does not re-create (and re-lease the
pooled connection) every cycle.
- AbCip: the same mapping on the write path and both read paths (single + whole-UDT).
- Uses the existing named BadNotSupported constants (StatusCodeParityTests guard).
- Tests (falsified by disabling the mapping, and separately the keep-on-refusal): status-bit
writes and PLC-5 read/write through the driver over the native runtime — BadNotSupported, zero
wire requests, one create, health unchanged; AbCip refused read/write via a refusing fake plus a
control that a non-refusal failure still evicts and degrades.
- Docs/CLAUDE.md: BadNotSupported; #571 known-gap wording removed.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- I1: the upgrade restart itself moves every no-key AbCip/AbLegacy driver to Native (every process
start constructs drivers, incl. boot-from-cache). Docs + CLAUDE.md now say so and tell operators
to set "transport": "Libplctag" BEFORE upgrading (older builds ignore the unknown key).
- I2: AbLegacy release note leads with the status-bit write refusal (T4:0.DN, C5:0.CU, R6:0.EN, ...)
on defaulted SLC/MicroLogix drivers and the fallback. The real client status is
BadCommunicationError, not BadNotSupported (the driver's write catch-all maps the runtime's
AbTransportException like any transport failure, as #571 does for PLC-5), with handle eviction
and Degraded health; pinned by a driver-level test (no 0xAA write on the wire, with a positive
control). ST string byte order stated as unverified. Also corrects the docs' "T4:0/DN" examples:
the driver's address grammar is "T4:0.DN" ("/DN" is a malformed address).
- I3: DriverProbeHandshakeE2eTests' two AbCip probe tests are a [Theory] over Native and
Libplctag. Run against ab_server controllogix: all 4 rows pass with identical exact messages.
- M1: documented (docs + resolver XML doc): the AdminUI merges only the probed device, so a
mixed-family defaulted AbLegacy driver can probe Native while running Libplctag.
- M2: factory ParseEnum rejects undefined integer strings ("7") via Enum.IsDefined (both drivers).
- M3: AbCip driver-level bad value reads "AB CIP config in '<id>' has unknown Transport ..."; the
named constant is gone (tagName: null now means driver-level).
- M4: the explicit-Native limited-family Warning is once per instance, not per (re)init.
- Nit: EffectiveTransport docs note that injected factories bypass it.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Task 22 (spec section 8 P5), with the controller rulings that exclude PLC-5 and LogixPccc.
- Transport is nullable on both option records; null (key absent) means "use the default" and is
never serialized. Factory DTO parse: absent -> null; a bad value still fails via ParseEnum.
- One resolver per driver (AbCipTransportResolver / AbLegacyTransportResolver), used by the driver
constructor, the Test Connect probe and (through the driver) the CLI.
AbCip: null -> Native. AbLegacy: null -> Native unless any device is Plc5 or LogixPccc, then the
whole driver resolves to Libplctag. Explicit values always win; explicit Native with a
Plc5/LogixPccc device logs one Warning at init.
- Init log shows configured vs effective, e.g. transport=Native (default).
- AdminUI forms: "Default (...)" / "Native (managed)" / "libplctag (fallback)". Default omits the
key on save (reverses Task 19's explicit re-emit, which would have pinned every edited driver).
- CLIs: --transport default|native|libplctag, default "default" via the resolver, so
--plc-type Plc5 with no --transport runs libplctag.
- Tests pinned to Libplctag where a default-path runtime was created: found by instrumenting the
native factories with the current xUnit test name (one unit test, AbLegacyReinitConfigAdoption;
plus the two fixture-gated AbCip probe E2E tests whose exact messages were proven on libplctag).
TransportMatrix and the differential harness already pass explicit transports.
- Switch/resolver/CLI/form tests are falsified by mutating the resolver (default -> Libplctag,
exclusion removed, AbLegacy default -> Libplctag): each mutation turns them red.
- Replaces the inline role-name literal in AbCipDriverFactoryExtensions' ParseEnum call with a named
constant; RoleAuthorityGuardTests was already red on the base commit for that line.
- Docs: AbCip.md / AbLegacy.md default + release note; CLAUDE.md AB section.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
Pass 2 (2026-10-08 00:21Z, df776170): 17/17. Browse 3162=3162 tags, 20 UDT
templates identical, 200 tags read with 0 unexpected divergences (6 live
values compared structurally + every still element), 1 Forward Open and 1
acked Forward Close per native phase. Latency native p95 105.8 ms vs
libplctag 139.5 ms (limit 153.4) PASS. Pass 1 failures and their fixes are
recorded in the gate-results doc; pass 2 had no independent pcap.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
The 5069-L3x supports the Large open, as the press proved. The narrow/refusing case is a pre-v20 1769-L3x or a bridge. AbCipDeviceOptions.ConnectionSize: the native transport honours it on the wire; libplctag ignores it.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
- AbCip/AbLegacy driver forms gain a Transport InputSelect; FromOptions/ToOptions now carry
Transport, so saving any other field no longer silently reverts Native to libplctag.
- Both drivers log the constructed transport once per init.
- AbCip factory now takes the host ILoggerFactory (it ran on NullLogger in production, hiding
every AbCip log line); DriverFactoryBootstrap passes it.
- Gate results: AdminUI /run (T19) native, inverted (dead port) and libplctag legs.