Skip to content

CaseLedger Causal Ordering: CaseActor Observation Order Is the Canonical Causal Order

Context and Problem Statement

notes/case-ledger-authority.md states informally that "log_index order is causal order", a claim that was confirmed in the native-entry section of that note and again when WritePrologueLedgerEntriesNode was removed (Issue #1777). DEMOMA-22-005 relies on the same property, citing ADR-0041 as its basis. ADR-0041 establishes CaseActor-authoritative initialization — it does not establish a normative causal-ordering model.

This leaves DEMOMA-22-005 standing on an unwritten foundation. The property is real and intended; what is missing is the decision record. Three related pieces need to be unified as normative requirements:

  1. The CaseActor's obligation: log_index order is causal order from the CaseActor's own perspective.
  2. Testable temporal invariants: constraints on CaseLedgerEntry.published and log_index that conformance tests can verify.
  3. Participant emission obligations: what a well-behaved participant must do to preserve causal fidelity in the assertions it submits to the CaseActor.

Decision Drivers

  • DEMOMA-22-005 requires that invariant tests check declared causal edges against observed ledger order, but "observed ledger order is causal order" is not itself a normative spec entry.
  • The ledger is the replicated source of truth for all participants; the ordering model must be unambiguous.
  • The CaseActor is a single writer (ADR-0041, CLP-01-003), which uniquely positions it to enforce a deterministic ordering model without distributed consensus overhead.
  • Residual uncertainty about external actor clocks must be explicitly acknowledged rather than papered over.

Considered Options

A. CaseActor observation order — the log_index sequence assigned by the CaseActor's single authoritative write path defines causal order. For internally-generated events, order is known by construction. For inbound participant assertions, the CaseActor's receipt and commit order is authoritative. The CaseActor's own published timestamp stamps the ledger envelope; the participant's claimed timestamp is preserved inside the payloadSnapshot but does not determine ledger order.

B. Vector clock causal tracking — each activity carries a logical clock vector across all known participants. The ledger records vector values; receivers can reconstruct a partial causal order across participants without routing everything through the CaseActor.

C. Pure wall-clock ordering — entries are ordered by their published timestamp alone. Log index is a convenience counter; the authoritative order is timestamp sort order.

Decision Outcome

Chosen option: A, CaseActor observation order, because:

  • The CaseActor is already the single writer (ADR-0041). There is no concurrent-write hazard to model.
  • Option B requires propagating vector clocks through every wire activity. The federated protocol has no guarantee that all participants maintain compatible clock vectors, and the complexity cost is not justified when a single-writer regime already resolves the ordering problem.
  • Option C is unreliable: clock skew between the CaseActor's clock and participants' clocks means physically simultaneous events can receive arbitrary timestamp order. Timestamps are useful supporting evidence but must not be the sole determinant.

The Ordering Model

The CaseLedger is authoritative from the CaseActor's perspective only — a postal cancellation stamp, not a forensic record. The CaseActor records the sequence in which it observed and processed events. It makes no claim about when events occurred at other actors.

  • For internal events generated by the CaseActor itself (e.g., the native case-initialization entries in ADR-0041), causal order is known by construction: each entry is committed sequentially in a single execution path, so the resulting log_index sequence is the causal sequence.
  • For external events (inbound participant assertions), the CaseActor's receipt and commit order is the authoritative ordering. The CaseActor cannot verify causal claims about events it did not observe.

CaseActor Timestamp Obligation

The CaseActor MUST ensure:

  • If event A causally precedes event B (from the CaseActor's perspective), then log_index(B) > log_index(A).
  • The log_index sequence is gapless. The genesis entry carries index 0 and each later entry carries its predecessor's index plus one (CLP-14-010, SYNC-01-002). A receiver that gets an entry that does not extend its chain and has an index greater than tail_index + 1 concludes an entry is missing (SYNC-14-001). That conclusion holds only because the CaseActor never skips an index.
  • The published timestamps on consecutive ledger entries MUST be monotonically non-decreasing in log_index order. An entry stamped later in the sequence MUST NOT carry an earlier timestamp than its predecessor.
  • Every ledger entry's published timestamp MUST be on or after the case's own published timestamp. A ledger entry cannot predate the case it belongs to.

This is not a claim that B immediately follows A. Other entries may be recorded between them. The claim is only that B cannot appear before A.

Participant Emission Obligation

Participants MUST emit protocol messages in causal order, with timestamps that reflect the actual sequence of events. A participant MUST NOT batch events and emit them in arbitrary order. If A caused B caused C, the participant MUST send them in that sequence with monotonically non-decreasing published timestamps. A participant that violates this produces a malformed assertion — that is a protocol violation on the participant's part, not a CaseActor failure.

Residual Uncertainty

Even with the participant obligation above, the CaseActor cannot verify causal claims from external actors. The ledger is authoritative for what the CaseActor observed; it is not a guarantee of real-world causal fidelity when external actors self-report events. The CaseActor SHOULD reject payload assertions with timestamps that are implausibly far in the future or past relative to its own clock, but it cannot eliminate all timestamp spoofing from a malicious or malfunctioning participant.

Consequences

  • Good: the log_index sequence is the unambiguous causal order. Every conformance invariant that references causal order can be evaluated against log_index without clock comparison.
  • Good: notes/case-ledger-authority.md's informal claim is now grounded in a normative decision record that DEMOMA-22-005 can cite.
  • Good: the residual uncertainty about external actor clocks is explicitly acknowledged, rather than being an implicit gap in the spec.
  • Neutral: participants bear an obligation the CaseActor can only partially enforce (timestamp ordering is checkable; causal ordering across opaque participant-internal events is not).
  • Bad: a participant whose clock is wrong produces a structurally valid but temporally misleading assertion. The CaseActor can flag implausible timestamps (SHOULD-level) but cannot always determine the ground truth.

Validation

Enforcement splits along the two timestamps an entry carries, and the split is load-bearing — conflating them produces either a vacuous check or a false rejection (Issue #2824).

Commit timestamp (CaseLedgerEntry.published, stamped by the CaseActor). check_clp14_timestamp_invariants in test/ci/invariants/common.py, run by every scenario through universal_harness.py, verifies:

  • entry.published is non-null for all entries.
  • entry[N].published >= entry[N-1].published for all consecutive recorded entries within a case.
  • log_index values are unique within a case (CLP-14-005) and form a gapless sequence from 0 (CLP-14-010).
  • All entries in a case share the same case_id.
  • No entry's published precedes the case-creation entry's timestamp.

A single writer stamping from one clock satisfies these by construction; the harness is what catches a replica that does not.

Claimed timestamp (payloadSnapshot.published, chosen by the asserting actor). Its presence is guaranteed upstream: parse_activity refuses an inbound activity that carries no published (CLP-15-006), because ActivityStreams makes the field optional and the wire base class defaults it to now_utc — so a receiver that let it through could no longer tell a claimed time from its own clock, which is exactly how CLP-14-007 and CLP-14-008 were left vacuous (Issue #3149).

_validate_entry_timestamps, reached from every commit through CreateLogEntryNode, then refuses an assertion that:

  • predates the case by more than the configured clock-skew tolerance (CLP-14-006);
  • sits beyond the future tolerance or staleness window (CLP-14-007, CLP-14-008).

and reports without refusing an assertion whose claimed timestamp regresses behind that same actor's previous assertion (CLP-15-003) — at INFO within the clock-skew tolerance, at WARNING beyond it.

That asymmetry is the load-bearing part. CLP-15-003 binds the participant, and the CaseActor observes arrival order, not causal order: the transport gives no ordering guarantee (ADR-0037), so two assertions from one actor can legitimately be committed in the reverse of the order they were emitted, and CLP-15-003 constrains only assertions that are actually causally related. Refusing on that signal would be the reconstruction CLP-15-005 forbids — and it would discard the assertion outright, because the guarded commit precedes the effect nodes (CLP-10-006), leaving no ledger record and no retry. Recording it and reporting it keeps the signal without losing the assertion.

Monotonicity of the claimed timestamp is scoped to one actor's stream. Applying it across actors would reintroduce option C: two participants' clocks are not comparable, so a later-committed assertion from one legitimately carries an earlier claimed time than an earlier-committed one from another. For the same reason a snapshot the CaseActor builds on a participant's behalf must carry that participant's claimed time, not the CaseActor's clock — mixing the two inside one actor's stream manufactures a regression. Thresholds are tunable via AppConfig.ledger (CLP-14-009).

The participant emission obligations CLP-15-001 and CLP-15-002 are not enforceable per-assertion — that is the point of the residual uncertainty above, and CLP-15-005 forbids the attempt. They are validated at the conformance layer by check_causal_edges, which compares declared causal edges against observed ledger order across a whole scenario (DEMOMA-22-005).

More Information

  • Source concern: #2334
  • DEMOMA-22-005 now cites this ADR instead of ADR-0041.
  • notes/case-ledger-authority.md informally anticipated this decision; that note now cross-references this ADR.
  • Issue #2752 corrected the ordering model. It had said gaps in log_index were permitted, which contradicted SYNC-01-002 and the Validation section. CLP-14-010 now states the gapless rule.
  • Issue #2207 (ADR-0041 mis-citation) can be fixed once this ADR and the generated spec entries (CLP-14, CLP-15) are in place.

Generated spec requirements: specs/case-ledger-processing.yaml CLP-14-001 through CLP-14-010, CLP-15-001 through CLP-15-005.