Leave(VulnerabilityCase) Is the Canonical RM Case Closure Mechanism
Context and Problem Statement
DEMOMA-07-001 originally stated that RM case closure MUST be driven through
Add(ParticipantStatus, rm_state=RM.CLOSED) sent to the Case Actor inbox —
the same path as CS transitions (fix-ready, fix-deployed, published).
PR #1909 (issue #1858) changed RM case closure to flow through
Leave(VulnerabilityCase) → Case Actor commits a close_case
CaseLedgerEntry recording the received Leave → broadcast.
The RM transitions the Case Actor then writes for the leaving participant
follow as ordinary participant-status entries, one per transition.
Each replica advances the leaving participant to RM.CLOSED only by applying
those entries in ledger order, and never works out a closure path from its own
stored state (CM-23-001, #4091).
The three CS transitions were left unchanged.
This created a contradiction: DEMOMA-07-001 said closure must use
Add(ParticipantStatus), but the implementation used Leave. The question is:
which mechanism is canonical?
Decision Drivers
- The Case Actor should be the authority for RM closure, not individual participants asserting their own terminal state.
- A lost
Add(ParticipantStatus, rm_state=RM.CLOSED)message would allow a participant to ghost a case: their local state becomesRM.CLOSEDbut peers never learn about it. - The
Leaveround-trip makes closure observable to every replica that still receives case content, via the canonical ledger rather than as a direct state assertion from the departing participant (a replica atRM.CLOSEDreceives only its own closure entry, CM-23-004). - The existing
CloseCaseReceivedUseCaseandcreate_close_case_received_treealready implement theLeavepath as of #1909.
Considered Options
Leave(VulnerabilityCase)only — canonical closure path;Add(ParticipantStatus, rm_state=RM.CLOSED)is not a valid closure trigger.Add(ParticipantStatus, rm_state=RM.CLOSED)only — revert to originalDEMOMA-07-001intent; roll back #1909.- Either path is valid — both
Add(ParticipantStatus)andLeaveare acceptable closure triggers.
Decision Outcome
Chosen option: Leave(VulnerabilityCase) only, because:
- It makes the Case Actor the single authority for RM closure, consistent with the log-centric single-writer regime (ADR-0019, ADR-0021).
- It prevents ghost-departures: a participant cannot silently reach
RM.CLOSEDon peers without the Case Actor having committed and broadcast a ledger entry. - It is already implemented by #1909; rolling back would reintroduce the ghost-departure hazard.
Add(ParticipantStatus)is semantically a status announcement, not a closure command; conflating the two overloads the message type.
Consequences
- Good, because RM closure is observable on every replica that still receives case content (CM-23-004) via the canonical ledger chain, not inferred from direct status assertions.
- Good, because the Case Actor can enforce closure sequencing (e.g., embargo
teardown must precede closure) via the
Leavereceive path. - Good, because
AutoCloseSequenceinadd_participant_status_treebecomes correctly identified as dead code and can be removed (issue from #1910). - Neutral, because
DEMOMA-07-001must be amended to scope its clause to CS transitions only; this is a spec clarification, not a behavior change. - Bad, because
AllParticipantsRMClosedConditionNode(formerly step 5 ofadd_participant_status_tree) evaluated the all-closed condition only whenAdd(ParticipantStatus)arrived — which always precedes anyLeave. Step 5's auto-close semantics are therefore moved to theLeavereceive path (see ADR-0051, CM-23-002).
Validation
CloseCaseReceivedUseCaseinvultron/core/use_cases/received/case/lifecycle.pyis the implementation of theLeavereceive path.create_close_case_received_treeinvultron/core/behaviors/case/receive_close_case_tree.pyis the BT factory.- Tests for the
Leavereceive path:test/core/use_cases/received/test_close_case_routing.pyandtest/core/use_cases/triggers/test_close_case_trigger.py. A dedicated ledger round-trip test is planned in the sidecar implementation issues (#1916, #1917). - After #1901 lands: no
AutoCloseSequenceinadd_participant_status_tree(regression test AC on the cleanup issue from #1910).
Pros and Cons of the Options
Leave(VulnerabilityCase) only
- Good, because Case Actor is the single authority for closure (ADR-0019).
- Good, because prevents ghost-departures (lost
Add(ParticipantStatus)message). - Good, because closure is a ledger-observable event, not a state inference.
- Neutral, because
DEMOMA-07-001requires amendment.
Add(ParticipantStatus, rm_state=RM.CLOSED) only
- Good, because consistent with DEMOMA-07-001 as originally written.
- Bad, because participant asserts its own terminal state; peers must accept it.
- Bad, because a lost message allows ghost-departure.
- Bad, because rolling back #1909 reintroduces the ghost-departure hazard.
Either path is valid
- Good, because backward compatible.
- Bad, because two closure paths create ambiguity about which event is canonical.
- Bad, because
add_participant_status_treewould need to handlerm_state=RM.CLOSEDas a closure trigger, re-introducing the hazard.
Generated spec requirements: case-management.yaml CM-23-001 through CM-23-004;
multi-actor-demo.yaml DEMOMA-07-001 (amended).