Accept Non-Adjacent Forward RM Jumps and Notify; Refuse Backward Regressions Non-Silently
Context and Problem Statement
When a CaseActor receives Add(ParticipantStatus, CaseParticipant) and
compares the reported rm state against its local replica, three cases arise:
- Adjacent valid transition — no anomaly; accept silently.
- Non-adjacent forward jump — the sender skipped intermediate states
(e.g.
RECEIVED → ACCEPTEDwithout passing throughVALID). The existingFilterParticipantStatusDimensionsNode._rm_is_acceptable()already accepts these and documents the rationale ("sender is authoritative about own RM progress"), but acceptance is silent. - Backward regression — the sender reports a state earlier on the RM
progress scale than what the receiver has already observed (e.g.
ACCEPTED → VALID).ValidateRMTransitionNoderefuses these, which is correct, but the refusal is also silent.
Two independent bugs arise from the silent treatment:
- Forward jumps are accepted without any log entry at WARNING level or any protocol-level notification. An anomaly that should trigger a clarification request passes through as if nothing unusual happened (ISSUE-2258).
- Backward regressions are refused without emitting a protocol-level note. A remote actor's replica is now permanently diverged from the receiver's, with no signal to either party.
The broader principle: CVD is inherently asynchronous. Events happen at time t₀ but are only recognised as anomalous at time t₁ > t₀. When a protocol-level anomaly is detected on receipt, the receiver must both act correctly and act non-silently. Silence on anomalies breaks the protocol's observability and makes diagnosis impossible.
Decision Drivers
- Liberal accept / Postel's law (ISSUE-2229): accept everything acceptable; refuse only what must be refused.
- Sender is authoritative about its own RM progress: refusing a non-adjacent forward jump would leave the receiver's replica permanently wrong, because the sender will not resend the intermediate transitions it never reported.
- RM is monotonic: backward regressions indicate either a protocol error or a divergent replica state. Neither is acceptable; both warrant correction.
- Non-silence principle: anomalies detected asynchronously must produce an
observable effect — at minimum a WARNING log, and on the production path an
Add(Note, VulnerabilityCase)addressed to the sender. - ADR-0049: core does not model inbound protocol error message types
(
REfrom the original BT protocol design). The pragmatic substitute isAdd(Note, VulnerabilityCase)via the existing note-emission infrastructure. - Consistency across call paths:
FilterParticipantStatusDimensionsNode(production path,add_participant_status_tree) andValidateRMTransitionNode(standalone path,append_participant_status_tree) must implement the same acceptance policy. The standalone path is test-only; it has no case context and therefore cannot emit a note, but it must still log at WARNING level.
Considered Options
- Refuse non-adjacent forward jumps — reject the
rmdimension and carry the receiver's current value forward (as RSH-05 does for other refusals). - Accept silently — current behaviour; no log, no notification.
- Accept and notify — accept the forward jump (honouring sender authority),
log at WARNING level, and on the production path emit
Add(Note, VulnerabilityCase)describing the anomaly and requesting clarification.
Decision Outcome
Chosen option: accept and notify.
Forward jumps (non-adjacent, monotonically forward)
The receiver MUST accept the reported RM state. Refusing would leave the replica permanently wrong. The receiver MUST:
- Log a WARNING naming the sender, the observed gap (
before → after), and the fact that the jump is non-adjacent. - On the production
add_participant_status_treepath, emitAdd(Note, VulnerabilityCase)requesting the sender to clarify the intermediate path it took.
Backward regressions
The receiver MUST refuse the rm update (current behaviour, preserved). In
addition it MUST:
- Log a WARNING (already done; confirmed correct).
- On the production
add_participant_status_treepath, emitAdd(Note, VulnerabilityCase)describing the refused regression.
Implementation shape
This decision constrains any implementation by the invariants below; the concrete nodes and trees that satisfy them are tracked by the governing requirements RSH-06-001 through RSH-06-005, whose verification: fields name the current tests — this ADR does not name them, so an implementation refactor does not invalidate it.
- The anomaly — a non-adjacent forward jump, or a backward regression — is detected and recorded at the moment of receipt, on whichever received path carried the declaration.
- The clarification
Add(Note, VulnerabilityCase)to the sender is emitted only by the CASE_MANAGER, because only the CASE_MANAGER adjudicates a declaration into case state. - A wholly refused backward regression still owes the sender that note, even though the refusal is a precondition-guard failure that stops the tree before its ordinary effects; the note is therefore emitted on the refusal path, once per received activity, so a redelivery does not repeat it.
- The test-only standalone path has no case context: it logs the anomaly at WARNING and emits no note.
Consequences
- Good, because replica correctness is preserved (forward jumps accepted).
- Good, because anomalies become observable at the protocol level.
- Good, because both call paths now agree: forward jumps are always accepted.
- Good, because the asynchronous non-silence principle is encoded as a spec requirement, not just a one-off fix.
- Neutral, because
Add(Note, VulnerabilityCase)is a pragmatic substitute for theRE(Report Management Error) message type from the original BT protocol. A future ADR may formaliseREinMessageSemanticsand replace the note; that change would be backward-compatible with this decision. - Bad, because note emission adds a new effect to the
add_participant_status_treepath; tests must cover the case where no anomaly is present so that the node is a no-op in the happy path.
Validation
ValidateRMTransitionNode: unit tests assert WARNING log + anomaly flag on forward gap; WARNING log + FAILURE on backward regression.FilterParticipantStatusDimensionsNode: unit tests assert anomaly flag is set when a non-adjacent forward jump is accepted.add_participant_status_treeintegration tests: note emitted on forward gap; note emitted on backward rejection; no note on adjacent transition.append_participant_status_treeunit tests: WARNING log on forward gap; no note (standalone path has no case context).
More Information
- Original protocol
REmessage type:vultron.bt.messaging.states; not yet in coreMessageSemantics. ADR-0049 explains why core does not model inbound error types. - RSH-05 (ADR-0061): per-dimension partial accept — the complement of this
decision. This ADR governs the
rmdimension's anomaly notification policy while RSH-05 governs the per-dimension filter and snapshot shape. - ISSUE-2258: the concrete bug;
ValidateRMTransitionNodeaccepted forward gaps at INFO level with no notification. - ISSUE-2229: liberal-accept epic (Postel's law).
Generated spec requirements: specs/received-status-handling.yaml RSH-06-001
through RSH-06-005.
Amendment — 2026-10-09
The "Implementation shape" section is rewritten to record decision-level invariants only; the decision itself is unchanged.
As first written, the section named concrete node classes and a factory parameter — rm_transition_anomaly, FilterParticipantStatusDimensionsNode, ValidateRMTransitionNode, EmitRMGapNoteNode, and the effect_nodes of add_participant_status_tree.
A later refactor (the received-tree factory work landed with #4310) renamed those nodes and moved the note from a single effect stage to both the accepted-path effects and the refusal-path effects, which left the ADR describing an implementation that no longer existed.
The replacement text states only what any implementation MUST satisfy — where the anomaly is detected, that the note is emitted only by the CASE_MANAGER, that a wholly refused backward regression still owes the sender a note on the refusal path once per received activity, and that the test-only standalone path logs but emits no note — and defers the live node and tree mapping to RSH-06-001 through RSH-06-005, whose verification: fields track the current tests.
This also records the missing fact the original section lacked: that a wholly refused regression is a precondition-guard failure whose note runs as a refusal effect, not an ordinary effect (#4348, found while implementing #4310).