Repository navigation
feat(canopen): CANopen Safety (CiA DSP 304 V1.0) — SRDO, GFC, peer configuration and boot-up step D - #291
Conversation
… 9 October Package placement, norm basis (DSP 304 V1.0, V1.1/EN 50325-5 reconciliation as an issue), both roles in one round, step D in the boot-up manager, SrdoEngine as its own class, one pull request, ICanOpenSafety via extension instead of widening ICanOpenNode. The design and the item table that the FR-CO rows will cite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… record Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Twelve tasks from the value types to the pull request, each with its failing test first, the code, the mutation that proves the test, and the commit. Argues from docs/reviews/2026-10-09-canopen-safety-scope.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…SrdoCount option Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… 304 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…3FFh Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ep 13FEh across a restore Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… re-arm mid-cycle Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ailsafe command Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rop dead runtime guards Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e SRDOs and the GFC on a node Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nge of state is off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gate 001h - Every SRDO frame and GFC goes out through one send chain. Two independent SendControlFrame calls could overtake each other, and a consumer took the inverted frame first as OutOfOrder (CiA DSP 304 §8.1, §9.5). - SrdoStateChanged and GlobalFailsafeCommandReceived enqueue on producer slots outside 1..127 (GFC 0, SRDO n 128 + n): a repeated GFC folds into the one waiting instead of growing the critical queue, and SRDO n no longer shares node n's slot in the timeout folding. - The Operational check of the configuration methods runs on the actor. - 001h is subscribed and handled only by a node that has SRDOs. - Tests: the GFC fold, pair order, both LeaveOperational hooks, a witness for the §8.3.1 D negative, and no GFC event on a node without SRDOs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An SRDO pair is one unit on the send chain (ISrdoEngineHost.SendPair), and at most one pair per SRDO is in flight: a pair due while the previous one is unconfirmed is skipped instead of queued. On a bus whose confirmations stall the chain no longer grows without bound and later drains stale pairs; the consumer's SCT reports the gap. - The GFC leaves the chain and goes out at once, as before the chain, so it never waits behind SRDO pairs (CiA DSP 304 §8.2). - Tests: skipped pairs under held confirmations, the GFC passing an unconfirmed pair, a real witness for the reset test's negative, and the SRDO chain awaited before every "no pair sent" assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pair that comes due while one of the same SRDO is in flight becomes that SRDO's single pending pair (latest data wins) and goes on the send chain as soon as the in-flight pair completes, so a triggered or change-of-state transmission (CiA DSP 304 §8.1, "fast reaction") no longer waits a whole refresh cycle. The bound stays one in flight plus one pending per SRDO. Leaving Operational drops the pending pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ild a peer safety configuration from one Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… mapping record without its communication record Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s over SDO Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er SRVT, delete every SRDO first A configuration naming an SRDO above the peer's 13FFh:00 count was silently skipped and reported as succeeded; both Configure and Verify now throw before any write, and an empty 13FFh:00 reply is an error instead of zero SRDOs. SRVT (sub3) is part of the checksum for every direction, so it is written and bounded 1..255 ms for a producer too. A first pass deletes every SRDO so that a configuration moving COB-IDs between SRDOs is not refused by the device. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FromDeviceDescription produced ValidationTime = 0 for a record whose file declares no sub-index 3 or declares 0, which the configure/verify bound (1..255 ms for every direction) refuses. Substitute the 20 ms default of section 8.4.2.2, which is what the device stores and recomputes its checksum from. The SrdoCommunicationParameter documentation now says the validation time is written and checksummed for every direction. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (CiA 304 step D) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…held reset, drop a stale step-D result A failed safety verification now disarms 1F89h and a halted boot ignores its timeout, so a mandatory slave that is still unseen does not trigger a second stop-all/reset-all. A verification ending while a forced Reset Communication is held starts, resets and signals nothing; the resumed boot uses the kept result. A result carries the boot generation it began under and is dropped once that boot was cancelled. Only the verification's own cancellation counts as cancelled. The step-D limits under a simultaneous start and for an already operational slave are documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a verification failing in the reset hold A slave that is already Operational when first seen (running before the takeover, keep-alive, or reached by a simultaneous-start broadcast before it announced) is not started by this master and so not verified; the earlier text named the wrong case. A verification failing while a forced reset is held is repeated by the resumed boot and does not start the slave. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… file Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ive mapping is not replaced by it Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ide the SRDO overload e66a6fa appended srdoCount and enableChangeOfStateSrdo to With(...) as optional parameters. That changes the method's CLR signature, so a caller compiled against 1.3.0 would fail with MissingMethodException: a binary break in what is meant to be a minor release. The 13-parameter signature of 1.3.0 is restored as an overload without default values (hidden from IntelliSense) that forwards to the 15-parameter one; source callers keep resolving to the latter, a call with all 13 arguments to the former, with the same result. The type's summary no longer cites the now ambiguous cref. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The master these tests open is already a CanOpenNode (CodeQL 563-565). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exact widths ObserveForeignSrdoAsync took any 1301h-1340h:05 response of four bytes or more by its low four, and any sub-index 01h response by its low byte. COB-ID 1 must now be exactly four bytes (UNSIGNED32) and the direction exactly one (UNSIGNED8); a value of another length counts as not read, so the file decides, as for the mapping read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…RDO id The consumer took any payload length, so a bitwise-inverse pair longer than 8 bytes on a CAN FD capable bus was actuated by its mapped prefix and made the SRDO valid. An SRDO is a classic CAN frame (section 8.1): a CAN FD frame, or a payload over 8 bytes, on COB-ID 1 or 2 of a consumer is now rejected as a wrong inverse is (Mismatch, the pending first frame dropped, nothing actuated, no SrdoReceived, the SCT not restarted, no EMCY). The reader carries the frame kind to the engine (HandleIncomingFrame); section 8.1.3.1's first-n-bytes rule applies within 8 bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GFC is a classic CAN frame with DLC 0 (section 8.2); a CAN FD frame on 001h, even with no data, raised GlobalFailsafeCommandReceived. It is now ignored, as the SRDO ids ignore a CAN FD frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 81842ffafe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ation PeerSafetyConfiguration stays mutable through Add (and its mappings through SrdoMapping.Add), and Configure read it again after awaiting the SRDO count, so an SRDO added meanwhile skipped the description pre-check and was written. Configure and Verify now take a copy of the configuration or expectation (PeerSafetyConfiguration.Snapshot, mappings copied) when the transaction owns the peer's channel, at the same moment as the description snapshot and before any frame; the pre-check, the count check, the write list and the comparison use that copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The observer's match took any direction but 0 as an existing record, so a reserved value 3..255, live or from the file, matched. A record now exists for the match only with direction 1 (tx) or 2 (rx), the values the device's validator accepts (section 8.4.2.2); IsDeletedSrdoAsync became IsExistingSrdoAsync to say what it decides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unreadable-direction case Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review of head [P2] Search SRDO communication records implied by mapping declarations —
Confirmed with a focused reproduction: remove Use the existing Validation: all 880 existing CANopen tests passed on .NET 10. The focused reproduction failed at the expected non-null-observation assertion. Its temporary source was removed; no production changes were made. No confirmed standards findings. |
…ion implies MatchSrdoAsync took its search limit from the communication records the description declares. The loader and the peer SDO gate also imply a communication record through the highest declared mapping record (FR-CO-029), so a peer described by 1381h/1382h alone could be configured (§9.2) and still its valid pair was not observed (FR-CO-046): the search count was zero. The limit is now DescribedSrdoCount, the same reading the loader and the gate use. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Fixed in aeb3bbc.
Regression test: Local gate on Not mergeable yet: the new head re-triggers the reviewers and the checks. I will say so once they have settled. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aeb3bbc033
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
ObserveForeignSrdoAsync read the peer's COB-ID, direction and mapping with one public upload each, every one taking the peer's SDO channel for that call only. A safety transaction starting in between waited for the channel and took it after the COB-ID read, so the direction and mapping uploads were refused as in flight and answered from the file: a live COB-ID of one configuration combined with the file's mapping of another, while the peer was being reconfigured. The observer now takes the peer's channel without waiting and holds it to the end of the snapshot, running its uploads as channel-held transfers. A transaction starting meanwhile waits, as it does for any call in flight; a channel busy at the start makes every live read of that observation the in-flight refusal, so the whole snapshot is the file's, as before. The PDO observer keeps main's per-upload behaviour (#295); the shared live mapping read takes the upload as a delegate. The SRS row FR-CO-046, the README and the interface say so, and also record the search over the records the description implies (aeb3bbc) and the tests that pin both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7da0944fed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…the peer channel The boot-up built a safety slave's expectation on the actor turn that queued its verification and handed it to VerifyPeerSafetyConfigurationAsync, whose own description snapshot is taken later, once the transaction owns the peer's SDO channel. A rebind or an in-place edit of the bound DCF in between — while the verification waited for a call in flight — was then compared against stale values and the slave treated as not verified. The verification core now takes the expectation from a factory it calls once it owns the channel, with the description bound at that moment, the same one its transfers are gated against; the public call passes its copy of the caller's expectation as before. No safety DCF bound at that moment means nothing to verify, and the slave is started as one without SRDOs. FR-CO-045, the README and the boot-up test class say so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ee6686d. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ee6686d6fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…d do not latch a skipped step D Two findings on ee6686d. Codex: the step-D expectation and the transaction's gate were built in two passes over the bound, publicly mutable description, so an edit between them could give the expectation one content and the gate another. A safety transaction now takes one reading of the description (DescribedObjects: every index with its entries, frozen) when it owns the peer's channel, and the expectation, the gate and the pre-check are all derived from it; the loader's safety steps run over that reading, and the public FromDeviceDescription takes its reading the same way. Cursor: a verification that found no safety DCF when it owned the channel was taken as verified and latched, so a DCF bound later in the same boot - during a held Reset Communication, say - was never verified. The outcome is now three-valued; nothing to verify records nothing, and ConsiderStart looks again: a DCF bound since is verified, none means a start as for a slave without SRDOs. FR-CO-045 and the README say so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47408bfa34
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

What does this change?
Adds CANopen Safety after CiA DSP 304 V1.0 to
CanKit.Pro.CANopen, opt-in: a node opened withCanOpenNodeOptions.SrdoCount > 0(or from a DCF that declares SRDO records) gets the objects1300h–13FFh, an SRDO engine (producer cycle with the plain/inverted frame pair, consumer with SCT/SRVT and the order and inversion checks, EMCY8210h), the GFC, the13FFhchecksum with the §9.5 check at the transition to Operational, EDS/DCF loading, and — for the master and tool role — the §9.2 peer configuration with readback and acknowledgement, the §8.3.1 step-D verification wired into the flying master's boot-up (while a safety slave is assigned the master sends no NMT Start to node 0: the 1F80h bit 1 start moment starts every seen slave individually, safety slaves after verification), andObserveForeignSrdoAsync. Everything is reached throughnode.Safety()(ICanOpenSafety);ICanOpenNodeis not widened. A node without SRDOs is unchanged.Why now and why in this package:
docs/reviews/2026-10-09-canopen-safety-scope.md(the eight decisions of 9 October, the design, the item table that FR-CO-035..046 cite). Norm basis is DSP 304 V1.0, the only full text available; the reconciliation against EN 50325-5:2010 / CiA 304 V1.1 is #289 (CRC initial value and byte order, the13FFhro/rw conflict, defaults).Type of change
feat— new behaviour (minor release)fix/perf— bug or performance fix (patch release)docs/test/refactor/chore/ci— no release!in the title, plus aBREAKING CHANGE:footer explaining the migration)Notes a reviewer should know:
CanOpenNodeOptions.With(...)gained two parameters; the 1.3.0 thirteen-parameter overload is kept beside it (binary-compatible,[EditorBrowsable(Never)], covered by a reflection test).With(...)now validates its result (the sameArgumentOutOfRangeExceptionOpenNodewould throw, only earlier) — documented in the package README under "Changes since 1.3.0".CanKit.Pro.CANopen.approved.txtwas regenerated from the.received.txt; the diff is additive apart from thatWithline pair.FlyingMasterSignal.SlaveSafetyConfigurationInvalid,ForeignPdoKind.Srdo,SrdoInvalidReason.ActuationRefusedandSrdoInvalidReason.MappedObjectUnavailableare additive enum members.§8.4.2.3does not exist in DSP 304 V1.0 (§8.4.2 ends at §8.4.2.2); the branch cited it in a loader finding, an exception message and comments until b5868dd. The "at most 16 mapping sub-indices" bound is this implementation's (byte-aligned entries in 8 bytes); the specification allows 2, 4–128, and bit-granular mapping is not built. An SRDO maps objects of fixed width only (no strings or domains); a PDO is unchanged. SRDO frames and the GFC are classic CAN frames: a CAN FD frame or a payload over 8 bytes on those ids is rejected (the consumer goes toMismatch; an FD frame on 001h is no GFC).ConfigurePeerSafetyAsync,VerifyPeerSafetyConfigurationAsync) holds the peer's SDO channel for its whole length; a concurrent SDO call of this node to that peer is refused with the "already in flight" exception a concurrent transfer already produced before, so callers see no new behaviour.mainaccepted any length ≥ 1 and ≥ 4 for the PDO observer, so that observer tightens with it.ObjectDictionary;PeerSafetyConfiguration.FromDeviceDescriptionbuilds its expectation by running them on a dictionary of its own (CanOpenNode.LoadDescribedSafetyObjects), so a DCF reads the same on both sides by construction. The node's findings and its 13FEh-last order are unchanged.Checklist
dotnet build CanKit.Pro.sln -c Releasesucceeds (with-p:CI=true, 0 warnings)dotnet test CanKit.Pro.sln -c Releasepasses (net10.0 leg locally, every test; the net48 leg runs in CI, the Safety classes were also run on net48 locally)docs/reviews/2026-10-09-canopen-safety-scope.md, CANopen Safety: reconcile the CiA 304 implementation against EN 50325-5:2010 / CiA 304 V1.1 #289Also run locally:
dotnet format CanKit.Pro.sln --verify-no-changes(clean) anddotnet packinto a fresh directory +eng/verify-packages.py(9 packages ok).Mutations run (CLAUDE.md § Before claiming something is true)
Each mutation was applied to the source, the named test watched red, the source restored.
SrdoEngine.Transmitsends the plain payload twice →A_Transmission_Is_A_Plain_Frame_And_Its_Inverse_On_Following_Idsred.ArmSctremoved fromOnSecondFrame→Valid_Pair_After_Expiry_Rearms_Sct,Each_Pair_Restarts_The_Sctred;IsInversePairreduced to a length check →A_Wrong_Inverse_Is_A_Mismatchred;DisarmConsumeremptied → the Leave/Rebuild teardown tests red;SetValid/SrdoReceivedswapped → the event-order test red.finallyrestart removed → six tests red; stale-timer protection removed → four re-arm tests red;CycleHandle?.Dispose()removed → the tracking-actor handle test red.IsRestorableObject→ the store/reset, Reset Node and "load" tests red; the static managed check reinstated → the DeclareGuard tests red.13FEh = A5hwritten before the checksums →Commit_Writes_Checksums_Then_Validred;Armscheduling regardless ofConfigurationValid→ the tampered-checksum test red;_srdo.LeaveOperational()removed fromPerformNmtReset/ApplyNmtTransition→ the reset / Enter-Pre-Operational tests red; the GFC routed through the send chain →The_Gfc_Does_Not_Wait_For_An_Unconfirmed_Pairred; the in-flight bound removed → four pairs handed over instead of one; the pending slot turned back into a drop → the coalesce test red; a refused write clearing13FEh→ the Operational-abort test red; the own GFC echo dropped → the echo test red in both worlds.13FEhrank dropped,13FEhapplied before the deferred creates, the failed-mapping check removed, theTryGetguard removed → the named description tests red.Configure_Does_Not_Acknowledge_When_The_Readback_Differsred; sub3 not written for a producer → the producer-SRVT and consumer tests red; the first delete pass dropped → the COB-ID swap test red;srvt ?? 0kept → both default-SRVT cases red; the range check inPeerSafetyConfiguration.Adddisabled → the range cases red (the Configure-side check behind it is a second line no test can reach any more and claims no kill)._slaveVerifiedset unconditionally in the hold → the failed-in-hold test red.continueon a read-but-different live word removed → the live-wins test red; the file fallback after a gate refusal reinstated → the gate test red; the dummy check, the high-bit check and the parameter name each → their test red.13FFh:nfilter in Verify removed →Verify_Ignores_The_Checksum_Of_A_Deleted_Srdored; the Operational period's token not passed toSendControlFrame, not cancelled on leave, not cancelled on dispose, or its source reused across periods → the queued-pair, pending-pair, dispose and token tests red;Array.Clearon leave removed →A_Pair_Pending_Before_A_Stop_Is_Not_Sent_After_The_Next_Startred;FromDeviceDescriptionruns the device loader's own SRDO code on a dictionary of its own, so the loader rules were mutated instead (the pair check at create, the two-following-ids check on the sub6 write, the collision check at create, the pre-defined pair, the inverted-slot check at the count and on the slot write, the application objects, the validator, the deferred creates, 1300h) → each red in its device test and, except the two inverted-slot checks taken alone, in the agreement Theory rows at node 5, 6 and 70; the deleted-record check and the file fallback inObserveForeignSrdoAsyncremoved → their tests red; the count > 16 guard disabled → both cases red;ThrowIfDisposedinGetSrdoStateremoved → its test red; eachSrdoCrc.Computebound, the bound on the unrounded value andAwayFromZerorounding → their cases red;PeerSafetyConfiguration.Addkeeping the caller's instance → its test red.SrdoMapping.Add(PdoMappingEntry)dropped → the bad-width and configured-SRDO-stays-intact tests red; the13FFh:00clamp restored → the 65/255 Configure and Verify tests red. (The PDO twin of the width gap predates this branch: PdoMapping.Add(PdoMappingEntry) accepts a default or zero-width entry #292.)PeerSafetyConfiguration.Add(direction, COB-ID pair, times, in-configuration collision) dropped → its row red.A_Direct_Safety_Write_Holding_The_Gate_Is_Stored_Before_The_Node_Enters_Operationalred (the state flips while a writer holds the gate).PDOMapping, width, access for the direction) and the call to the check itself dropped → its Theory row red.SrdoReceivedstill raised, the DeclareGuard rule for SRDO-mapped objects dropped, the producer's width check dropped (a zero-filled pair goes out), the file fallback's even-slot check dropped (also fails the validator and loader tests — one shared predicate), the GFC call not awaiting its send, the caller token not linked → each its test red.BuildPayload→ red.13FFh:00accepted with any length, the upload or the download Core reading the live binding instead of the transaction's captured description → each its test red.Task.RuninSendControlFrame(the start of the send task) can be removed without a Safety test going red: it closes a thread-scheduling window no deterministic test can open, and the check before each frame covers every testable scenario. How far the token reaches is stated in the README ("Producer") and is bounded by CanBusService.SendConfirmedAsync does not check its token before the driver call #294. Likewise dropping the_srdo.IsOperationalguard inOnSrdoPairCompletedsurvives every test. It is an equivalent mutant: the pending slot is cleared in the same actor turn that leaves Operational, and dispose is caught by the_disposedcheck. The guard stays as defence in depth; no test claims it.f5b6c79and Codex onaeb3bbc:ObserveForeignSrdoAsyncsearches the records the description implies (its highest declared communication or mapping record, as the loader and the gate read it) →A_Record_Implied_By_A_Mapping_Declaration_Is_Matchedred without it; the observer takes the peer's SDO channel without waiting and holds it across its whole live snapshot, so a safety transaction starting in between waits and a busy channel at the start makes the whole snapshot the file's →A_Safety_Transaction_Starting_Mid_Snapshot_Waits_For_The_Observerred without the hold. The PDO observer keepsmain's per-upload reads (ObserveForeignPdoAsync: a caller cutting in between its uploads mixes a live COB-ID with the file's mapping #295). An SRDO over a deactivated mapping (L = 0, §8.1.3.1) stays as designed; the thread says why. Codex on7da0944: the boot-up's step-D expectation is taken once the verification owns the slave's SDO channel, from the description bound then — the moment of the transaction's own snapshot — instead of on the actor turn that queued it →Step_D_Takes_Its_Expectation_When_It_Owns_The_Peer_Channelred without it. Codex and Cursor onee6686d: a safety transaction reads the bound description once (DescribedObjects) and derives the expectation, the gate and the pre-check from that reading — no deterministic test for an edit between two readings, the structure is the claim; a step D that found no safety DCF records nothing and the boot looks again →A_Verification_That_Found_No_Dcf_Does_Not_Count_As_Verified_For_A_Dcf_Bound_Laterred with the latch restored. Codex on47408bf(a concurrent structural edit during the one reading): a property of the mutable description model shared with the gate and the loader onmain→ CanOpenDeviceDescription: a bound description is read without synchronization against in-place structural edits #296, no change here.🤖 Generated with Claude Code