RequestRebirthAsync now returns only once the requested births have arrived
and gone quiet, but the modal still told the operator to re-expand the tree in
a few seconds. ConfirmRebirthAsync re-keys the tree against the same session
(as the Refresh button does) when the session and modal are still current,
drops the now-stale armed scope, and keeps the outcome line.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Since R06:M-5 the Sparkplug browser states no address for a metric the
ingestor will not read and labels its descriptor 'not bindable', but the commit
refusal still said the attribute lookup returned nothing. RawBrowseModal keeps
the attribute's descriptor on the selection, and DescribeUncommittableLeaf
reports a 'cannot be bound in this version' message echoing it. A lookup that
really returned nothing keeps the old text. A test pins the marker to the
browser's own string.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
/fleet/diagnostics was reachable only by URL: each driver-role row on /fleet now
links to it (FleetDiagnosticsLinks.For escapes the host:port node id). The
diagnostics table showed the string instance id because the wire carries no
display name; the page now joins the authored DriverInstances.Name from the
ConfigDb (enrichment only - a failed lookup falls back to the id) and keeps the
id beneath the name.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The dev-only demo page shipped routable in the production assembly. Its route
moves from @page to a code-behind [Route] under #if DEBUG, so a Release build
keeps the component type (nothing referencing it breaks) but has no /dev/*
route. The page-authorization guard classifies it under #if DEBUG, and
DevRouteGatingTests asserts each configuration's half (Debug: exactly the demo
route; Release: none) with a positive control. Both configurations pass.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
ImportEquipmentModal split each line on commas, so a quoted Manufacturer with
a comma silently shifted every later column. EquipmentCsvParser reads through
the shared Commons CsvParser (the raw-tag importer's authority): quoted
fields, doubled quotes, CRLF and blank lines parse; malformed quoting is an
error, not a guess. Header/shape messages are unchanged.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
v3 retired the equipment-Tag binding; IUnsTreeService.LoadTagsForEquipmentAsync
always returned empty, had no production caller, and EquipmentTagRow plus the
always-zero tagCounts / EquipmentRow.TagCount plumbing existed only to feed it.
All removed; the equipment badge count (virtual tags) is unchanged.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The equipment page's Remove reference / Delete virtual tag / Delete alarm row
buttons and the ScriptEdit Delete ran on the first click. They now go through
RawConfirmDialog, the same confirm /raw uses. The ScriptEdit change is limited
to the delete button and its dialog.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
I-1 - a non-C# paste produced thousands of Roslyn markers. The Problems-list report sent
them all as one JS->.NET message, which could exceed the 1 MB hub limit and kill the
circuit. The JS now sends at most MaxReportedMarkers (200) markers, errors first, each
message cut to 300 chars, plus the total. ScriptProblemReport.FromEditor clamps again
on arrival, and the Problems list shows "showing N of Total" / "+N more". The diagnostics
endpoint also bounds its response: Distinct, errors first, MaxDiagnosticMarkers (1000),
messages cut to 1000 chars. An error is always kept, so editor-accepts == publish-accepts.
M-1 - drop the per-instance compilation lineage; analyses derive from BaseCompilation.
M-2 - the VirtualTag modal clears its problems (and editor ref) on close and on a script
reload.
M-5 - an over-limit text is announced at blur (OnSyncRefused), not only at Save.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Every picker and RawBrowseModal stored the OpenAsync token after the await; if
the host was disposed (or the modal closed) meanwhile, its teardown had already
run with no token, so the plant-side session lived until the reaper's idle TTL.
BrowseOpenGuard.CloseIfOrphaned closes it at once; each host sets _disposed
first thing in DisposeAsync (RawBrowseModal also treats !Visible as gone).
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
A bare catch {} turned every failure - including an admin-only node with no
historian actor - into 'Loading...' forever, and a tick queued after a sync
timer dispose still issued an Ask. HistorianSinkStatusReader classifies a poll
as Available / NotOnThisRole (permanent: shown once, polling stops) /
Unavailable (shown with the last good status and its read time). The page
follows Fleet.razor: IAsyncDisposable, _disposed, overlap guard, page CTS.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
DriverTestConnectButton always sent a fixed 10 s, and the Galaxy/OpcUaClient
probes run under min(caller, configured), so ProbeTimeoutSeconds - documented
on every driver as the Test Connect timeout - could shorten a probe but never
lengthen it (Galaxy's 30 s default was silently 10 s). TestConnectBudget reads
the root ProbeTimeoutSeconds from the merged config at click time, clamped to
the actor's [1, 60] s, falling back to TimeoutSeconds. DeviceModal (the only
host) needs no change. The Galaxy/OpcUaClient form hints are reworded.
The 30 s result-clear timer moves from System.Timers.Timer + an async void
Elapsed lambda to System.Threading.Timer, drained in DisposeAsync, with a
generation so a superseded fire cannot clear a newer result.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
DriverConfigModal, DeviceModal and RawTagModal built a new parameter dictionary
and new EventCallbacks every render. ParameterDictionaryCache reuses the
dictionary while the values it carries are unchanged and rebuilds it with the
current value when one changes (a build-once cache would strand edits); the
callbacks are created once. Blazor still re-sets an EventCallback-bearing
child's parameters per parent render, so the forms' _lastParsed guards stay.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The import review grid rendered every parsed row and re-scanned all rows three
times per render for its counts; RawCsvReviewView recomputes rows + counts once
per change and the grid renders through Virtualize. The export modal embedded the
whole CSV twice (textarea + base64 data: URI); it now shows a 50-line preview
and links to GET /api/raw/tags.csv (ConfigEditor, like /raw), which runs the same
RawTagCsvExport builder and sends it as an attachment.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
R04:P1 - the editor no longer ships its full text JS->.NET on every keystroke. It reports
at blur, and ScriptEdit / the VirtualTag modal read the authoritative value with
MonacoEditor.GetValueAsync at Save (validation runs after the pull). The Blazor hub's
MaximumReceiveMessageSize is raised from 32 KB to 1 MB. The JS refuses a value that
would not fit instead of sending an oversize frame, and Save surfaces that refusal
rather than persisting stale source.
R04:P9 - diagnostics carry a generation counter and an AbortController. Every edit aborts
the in-flight request, and only the response for the current text is applied.
R04:S2 - the always-empty inlay-hints provider, endpoint, service method and contracts
are removed.
R04:S3 - MarkersChanged is typed (DiagnosticMarker) and feeds a new ScriptProblemsList.
Clicking a problem calls RevealLineAsync. Markers cross to .NET only when a list
subscribes.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The deploy API's 202 pointed at a route that did not exist. The GET reads the
deployment (projected, no artifact blob) and its per-node apply states, and
carries the POST's own gate: API key in X-Api-Key, AllowAnonymous against the
cookie fallback, 503 while Security:DeployApiKey is unset.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The FC15, FC16, BitInRegister[] RMW FC16 and FC23 builders allocated one byte
more than they filled (6 + 1 + n / 10 + 1 + n, data copied at offset 6 / 10), so
every multi-write request ended in a stray zero byte beyond the length its
byte-count field declares. A strict device or gateway that checks the frame
length against the byte count refuses it. Each PDU is now exactly 6 + n
(FC15/FC16) or 10 + n (FC23).
Tests: ModbusWritePduFramingTests asserts the exact length and byte-count field
for FC15 (1/8/9 coils), FC16 (array, Float64 array, scalar multi-register), the
BitInRegister RMW FC16 and FC23 (all red before the fix); the parity suite's
conformant device now refuses a frame-length mismatch on FC15/FC16/FC23.
Live: Driver.Modbus.IntegrationTests against 10.100.0.35:5020 (standard
profile) passes, including the FC16 and FC23 multi-register round trips.
Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The recursive RenderNode rendered every row of an expanded device, and built a
fresh context-menu list + closures per node per render. RawTreeRows flattens the
expanded tree (explicit stack) into rows rendered by one Virtualize; each level
shows 500 children, then a 'Show more' row. RawNodeMenuCache keys menus weakly
on the node instance and rebuilds on a picker-mode or Enabled change.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Review r0 of Integration-W3:
- I1: the effective FC16 cap is min(configured maxRegistersPerWrite, 123). The
driver trusts a larger configured cap and sends an over-ceiling FC16 (past 127
registers its byte-count byte wraps), which every conformant device refuses, so
such an array is never advertised writable.
- I2: a BitInRegister[] write reads its span first in one unchunked FC03, so it is
also bounded by min(configured maxRegistersPerRead, 125) (absent/0 = 125).
- m2: every span (registers and coils) must end at or before address 65535.
- m1: the String-node wording says it converts only when the text parses, which
the server cannot promise (DateTime never converts).
- m4: the parity device now enforces FC01/02 2000, FC03/04 125 (or a smaller
device read limit), FC15 1968, FC16 123, FC23 121/125, the FC15/FC16 byte-count
field (exception 03) and the address-space end (exception 02).
Tests: parity rows for each ceiling/cap/span boundary against the real write
path, and Core.Abstractions row tests. The configured-cap test that asserted a
cap of 200 lifts the ceiling now asserts the opposite.
Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
R04:P2 - every analysis derives from one process-wide bound base compilation via
ReplaceSyntaxTree (the scoped service keeps its current compilation); every entry point
takes the request token, the endpoints bind RequestAborted and answer 499 on abort, and
the Monaco providers abort their fetch when Monaco cancels the request.
R04:P3 - the catalog snapshot carries a sorted, de-duplicated string[] (binary-search
prefix completion) and an Ordinal path->info dictionary, built once per snapshot.
R04:S1 - Diagnose emits an OTSCRIPT_SETVTAG_INERT warning for each ctx.SetVirtualTag
call. It is a warning, so editor-accepts == publish-accepts still holds.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The list loaded every script entity including SourceCode to render names and
hashes. ScriptListQueries projects {ScriptId, Name, Language, SourceHash} for
the list and fetches one script's source when its Source dialog opens, with a
generation guard against a superseded open.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
R07:L5 (Modbus half, former F2): ModbusTcpTransport builds the MBAP ADU in a
pooled buffer and reads the reply header into one per-transport buffer (sends are
single-flight under the gate). ModbusRtuOverTcpTransport writes its ADU into a
pooled buffer (ModbusRtuFraming.WriteAdu, byte-identical to BuildAdu), and
ReadResponsePduAsync reads and validates the whole frame in one pooled buffer
instead of five arrays. Only the returned PDU is allocated. The exchange stays one
async method: splitting it cost a state-machine box that ate the saving. Scratch
A/B per op: RTU deframe 352 -> 184 B, RTU send 640 -> 408 B, TCP send 864 -> 800 B;
timings within noise; Driver.Modbus.Tests 2 s before and after. NormalizeWordOrder
lives in ModbusDriver (not a transport) and is left alone.
Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Each received event used to Insert(0) into a List and render the full page.
LiveTailBuffer queues events from the bridge thread into a bounded newest-first
ring; CoalescingRenderScheduler (TimeProvider-driven, drained on dispose)
flushes and renders at most once per 200 ms window. ScriptLog's filtered rows
are recomputed on flush/filter change instead of three times per render.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
OnSnapshotChanged used to re-project every cluster group and re-render the
whole page for each driver publish, per open tab. It now only marks a
CoalescingDirtyFlag; the 5 s tick rebuilds when the flag is set or the config
load changed (or evicted) anything.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The driver-status and resilience stores had no removal path, so a deleted
driver stayed on /hosts as a frozen row until restart. Both stores gain
RetainOnly, which /hosts calls after each successful config load (never on a
failed one).
The stores and InProcessBroadcaster now raise their events one subscriber at a
time, so a throwing handler cannot starve later subscribers or escape onto the
bridge actor. The alert/script-log bridges publish inside their try, and the
resilience bridge catches a store failure instead of restarting.
Refs #745
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Review I2: Driver.Ab.Transport raises statuses that came off the wire in an
Execute PCCC reply (envelope CIP status, PCCC STS) as AbDeviceReplyException.
AbLegacy's IsLocalRefusal now excludes it, and the new IsAnsweredRefusal (used
by the read, write, probe and indirect-index paths) counts a wire Unsupported -
CIP 0x08/0x1C/0x20 on the envelope, STS 0x10 - as answered: per-tag
BadNotSupported, Healthy, LastSuccessfulRead advanced. Before, they landed in
the 'refused before the wire' neutral class. Pre-wire refusals are unchanged
(still neutral). A socket test covers all four codes (red before); transport
tests pin the marker and a pre-wire builder refusal as the positive control.
The AbCip Forward Open BadConnection path is untouched.
Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Review I1: ConnectAsync, DisconnectAsync and Dispose each start a new connection
epoch under _subscriptionLock. A keep-alive failover runs under the epoch it
started in and abandons itself the moment it moves: it installs its session only
if the epoch still matches (checked and assigned under the lock, otherwise the
session is disposed), its state transitions are epoch-guarded, it stops trying
endpoints, and it never flips a user's new connection to Disconnected. A late
keep-alive from a previous connection's session starts no failover. Gate-driven
tests cover the success and all-fail arms, the stale keep-alive, with a genuine
failover as the positive control (4 of them red before).
Minors: m8 only the CompareExchange winner sets LastFailover; m5 the endpoint
loop stops early, and a failed Unsubscribe still drops its bookkeeping; m1 a read
cancelled mid-paging releases its cursor (bounded token) and rethrows, and the cap
release no longer rides the caller's token; m2 results are trimmed to the cap;
m3 a non-positive cap is refused (CLI and UI already bound it to >= 1); m6 the
subscription adapter's Dispose uses one bounded bridge for delete + detach.
Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
The Modbus lane (#741 U3) gave the driver String[] and BitInRegister[] holding-register array encoders, and
moved address resolution to ModbusTagAddressResolution (keys first, a bit region coerced to Bool, strict key
reads), but DriverArrayWriteSupport still listed both array shapes read-only (follow-up F1), so a client
write was BadNotWritable before it reached the driver.
Modbus row now mirrors the driver end to end:
- Keys shape only (region + address both present). A string-only addressString tag is resolved by the
driver with its own family through a parser Core.Abstractions cannot reference, so it stays read-only
(pinned: the conservative direction).
- Strict reads as the resolver: address an integer 0-65535; a present region / dataType / byteOrder /
stringByteOrder string must name a member.
- Coils: any dataType (coerced to Bool), <= 1968 coils (FC15).
- HoldingRegisters (FC16, never Bool): numeric, String ((stringLength+1)/2 registers, stringLength >= 1),
BitInRegister (1 register, bitIndex 0-15, span ending <= 65535), all within maxRegistersPerWrite.
FC23 is opt-in and never changes the answer.
- Elements convert (ScadaBridge int[] -> Int16 stays writable); only a String / DateTime node over a
numeric or bit element is left out, since such an element never converts.
Tests: the parity test's PendingF1 exemption and its guard are gone; the parity matrix is now region x
element type x node type against the real WriteAsync with a spec-conformant device (FC15 > 1968 refused),
plus shape edges at every cap boundary (numeric, String, BitInRegister, coil), strict key reads, FC23
on/off, string-only, the ScadaBridge int[] shape and U3 wire bytes. Core.Abstractions row tests and a
composer test (String[] and BitInRegister[] writable; over-cap, no-stringLength and string-only read-only).
Docs: table XML doc, Uns.md, modbus-addressing.md, drivers/Modbus.md, data-plane rules.
Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
- R2-1: the ConditionRefresh sweeps (RefreshRequired, post-outage recovery) run
under the session's work token, and ShutdownAsync cancels session work first
thing - so a sweep holding the subscription gate against a stalling upstream no
longer eats the whole teardown budget the deletes and close need.
- R2-2: DisposeAsync skips its nested ShutdownAsync when a shutdown already started
for the current lifecycle generation (nothing initialised since), so PostStop's
dispose does not re-wait the gate past its budget. Test: with the gate held, the
shutdown waits (positive control) and the dispose does not wait again; red with
the skip removed.
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
Coordinator ruling: a delivered chunk proves the Agent is alive (the analogue of
OpcUaClient's good keep-alive), so TryMarkStreamDelivered - generation-checked
under _subscriptionLock - also clears _writeDegraded. The degradation no longer
sticks forever on a subscription-only driver. Test: unanswered write -> Degraded,
stays Degraded with nothing else happening (positive control), the next chunk ->
Healthy; red with the clear removed. Round-2 doc text corrected.
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
I-1: a node whose in-memory trail is one short of the stored trail (it missed
a command the peer applied, whose entry then replicated in) silently dropped
its own next entry — in failover, the survivor's newest ack — because the
positional "past MAX(seq)" rule treated it as already stored.
SaveAsync now reconciles the last 16 in-memory entries against the stored
rows from that position on, by content (Kind/User/Text, timestamps within a
5-minute skew window; matched rows are consumed so repeats count). The peer's
copy of the same command is recognised and not re-appended; an unmatched
entry is inserted at its own ordinal when free, otherwise appended at
MAX(seq)+1 with a Warning — kept, never silent. Bounded read, constant write.
m1: MigrateInlineComments copies only — it no longer UPDATEs the condition
row (a fresh-HLC re-stamp could beat a newer peer ack under LWW); the next
save clears comments_json. m2: it returns unparseable alarm ids and OnReady
logs a Warning per id (+ an Information tally). m3/m4: documented in the
store remarks (Remove vs lagging peer; in-memory index == durable ordinal).
Tests: store-level failover sequence (missed command -> survivor's ack and
next entry persisted once, re-save writes 0 rows, one Warning) with the
dual-write positive control (peer copy present -> 0 rows, no Warning) and a
repeated-identical-comment case; a loopback pair failover test (link down,
survivor acks, rejoin -> both entries on both peers, held); migration
no-row-rewrite, partial fill and unparseable reporting; OnReady Warning.
Both failover tests fail against the round-0 store.
Refs #748
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
- N-4: the /sample Unsupported latch is set inside TryDegradeStream's
generation-checked critical section (latchUnsupported), so a superseded pump can
no longer latch it on the session that replaced it.
- N-5: a read served from the live pump index clears only the read degradation -
it is stream evidence, like a chunk, which by the I-3 ruling does not clear an
unanswered write. The 'cannot stick' wording in the XML doc and
docs/drivers/MTConnect.md now states the latch-until-next-answered-/current-read-
or-write behaviour, incl. the subscription-only consequence. Tested (chunk +
live-index read keep Degraded; an answered write clears it).
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
- N-1 (Important): m3's BadNodeIdUnknown throw for an ack with no resolvable
condition counted toward the host's AlarmAcknowledge breaker (threshold 5), so
re-acks of one unresolvable condition blocked every genuine ack. It now logs a
Warning and returns; only a no-reply transport failure throws. Test through the
real CapabilityInvoker: six unresolvable acks, then a genuine ack that is still
delivered (red with the throw restored).
- N-2: ShutdownAsync holds _subscriptionGate for the whole teardown (in-flight
subscribes finish first, bounded by the budget), so a subscribe arriving during
the delete phase finds the session released instead of creating a subscription
on the closing session. Red-tested.
- N-3: the shutdown-side cert-validation detach is generation-checked under
_probeLock with the map / endpoint clear.
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
R07:L5 (AB half): CipWriter rents its working buffer from ArrayPool and returns
it in ToArray (which now completes the writer; later writes throw). A single-reply
Logix read is one copy of the data (no List<byte> + ToArray); a fragmented read
reassembles in a pooled buffer. EncapsulationClient reads the frame header into a
per-session buffer (reads only happen under the session gate) instead of a new
array per reply. Measured per op (scratch A/B, Release): frame encode 784 -> 200 B,
Logix read 1994 -> 1655 B, 3-fragment read 11.9 -> 8.2 KB, EIP round trip
2.75 -> 2.15 KB; encode time 41-46 -> 28-39 ms per 500k, other timings within
noise; Ab.Transport.Tests 3 s before and after. Modbus transports are left to a
follow-up (its lane is still in review).
Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
N3: a keyless blob carrying the addressResolved marker was served from its
string by the driver but saved as HR[0] by the editor model / CSV path. The
precedence rule now lives in ONE pure resolver in Driver.Modbus.Contracts
(ModbusTagAddressResolution.Classify/Resolve): keys when region+address are
both present, else a string-only addressString resolved with the driver's
family (refused when it cannot parse), else refused. The marker never decides.
Consumers that now ask it instead of restating it: the driver's tag reader
(ModbusTagDefinitionFactory.FromTagConfig), the deploy gate (Inspect /
DescribeRejection), the editor model (AddressShape / IsStringOnly /
HasKeyAddress / ResolveWith, ToJson stamping, Validate), the CSV export cells
and the editor hint. The model writes keys only when it loaded them or a field
was authored - never defaults it did not resolve (also closes two divergences
the new matrix found: malformed-string+marker and non-string addressString were
saved as HR[0]).
One table-driven test (ModbusAddressAuthorityAgreementTests) runs 15 persisted
shapes through driver, deploy gate, model load/save/Validate and the real CSV
export->import under all four families; breaking the CSV export or reverting
the model to the marker rule fails it.
Minors: m1 CSV address cells keep the string+marker while it still describes
them (lossless unchanged round trip), drop it when contradicted, and the review
grid reports the drop; m2 the pre-lane test uses the real export residual; m3
unmarked string-only + typed-cell case added; m4 stale docs/summary and the
unused factory parameter removed.
Refs #741
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
K-F1 (AbCip #737 F1): FromCip maps general 0x1C and 0x20 to Unsupported and
0xFF/0x2104 to OutOfBounds, so a controller refusal is per-tag Bad instead of a
BadReply comms fault. A Forward Open refused with one of them is still
BadConnection. Every CIP code is now listed with its meaning; 0x01/0x07 stay
opaque on purpose.
K-F3 (AbLegacy F8, review 07 r0 I4): FromPccc follows the DF1 STS table:
0x20/0x30/0x90 -> BadConnection (a bridge reporting an unreachable END node),
0x50 and EXT 0x06 -> NotFound, EXT 0x07 -> OutOfBounds, 0x60 -> NotAllowed,
0x70/0xB0 -> DeviceStateConflict. Golden ParseReply tests per STS, and an
end-to-end AbLegacy test: STS 0x30 degrades with LastSuccessfulRead preserved;
the 0x60 positive control stays Healthy and advances it. Both were red before.
Driver edits are doc remarks and the two runtime tests the old table pinned.
Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
- m1: the form hint now says ProbeTimeoutSeconds can only shorten Test Connect's
10 s budget (passing the configured value as the button's budget is shared code -
follow-up).
- m2: a superseded init no longer overwrites _options (generation-checked under
_probeLock) or the cert-validation handler (handed back and published with the
session; detached when not installed; a previous one detached on replace).
- m3: an ack none of whose condition ids resolves throws BadNodeIdUnknown + Warning.
- m4: an ack with no reply (transport failure / non-caller timeout) degrades.
- m5: no session before the gate is the same ServiceResultException(BadNotConnected).
- m6: the caller-cancelled-write deviation is documented.
- m7: a reply during a published outage advances the stamp but does not write
Healthy; the keep-alive recovery ends the outage.
- m8: test for a non-caller OperationCanceledException write -> Degraded.
- m9: the browse session owns and disposes the user certificate it loaded
(OpcUaUserIdentityFactory.LoadCertificate; a key-less cert is disposed before the
throw); AdminUI-host certificate path documented.
- m10: a subscribe overtaken by a shutdown / re-init removes its half-built
subscription instead of registering it (data and alarm).
- Sync Dispose drains each gate for 500 ms (was 2 s).
Refs #743
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
2: only a PUBACKed (QoS >= 1) publish advances LastSuccessfulRead; a QoS 0 Sparkplug
command completing means only that the bytes left the local socket.
3: a no-verdict SUBSCRIBE caused by the caller's own cancellation now throws
OperationCanceledException (refs still Bad + pending), so the breaker no longer
counts a pipeline timeout as a failure.
4: the rebirth wait is the window capped at 15 s, below the AdminUI 20 s per-call
timeout.
6: a 10-topic test pins the 8-in-flight publish bound. 7: partial-level wildcards and
$share filters are pinned never to confirm. 8: the intended init-vs-shutdown
supersede is commented at the re-check.
Refs #744
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT