What message-ordering invariants must survive when the transport lacks ordering guarantees?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session exchanges messages with peers over a transport that does not guarantee delivery order (UDP, HTTP/2 multiplexing, multi-path TCP, or peer-to-peer mesh), and the messages have causal dependencies that must be preserved. Does not apply to transports with strict ordering guarantees (single TCP stream, in-order message queues).
Core Distinction
The separation between **transport delivery** (the message reaches the peer at some point) and **causal ordering** (messages that have dependencies arrive in the correct relative order). Relying on transport creates the **Causal Inversion Hazard**: message B depends on message A, but B arrives before A due to transport reordering, and the receiver processes B as if A were already handled, causing incorrect state or missed dependencies.
What Was Observed
In agent architectures using unordered transports, failing to preserve causal ordering produces three primary failure modes:
- **Dependent Action Before Precondition:** Message B says "delete the resource" and depends on message A "create the resource." B arrives before A, and the receiver deletes a non-existent resource or deletes a stale one with the same ID.
- **State Update Skipping:** Messages A and B update the same field. A arrives after B, overwriting B's update. The final state reflects A, not the intended B.
- **Causal Chain Break:** A chain of five messages has strict ordering. Transport reorders them, the receiver processes message 3 first, and fails because the causal antecedents (1 and 2) are missing.
Recorded Elements
- **`causal_dependency_graph_hash`:** The hash of the dependency graph each message belongs to, allowing the receiver to detect missing or out-of-order antecedents.
- **`logical_clock_value`:** A Lamport or vector clock value attached to each message, allowing the receiver to order messages by causality rather than arrival.
- **`pending_message_buffer_receipt`:** When a message arrives before its antecedents, the receiver buffers it and records the buffering receipt, allowing the agent to detect stalled causal chains.
- **`delivery_reorder_signature`:** The exact reorder pattern observed (which messages arrived out of order, in what sequence), enabling detection of persistent transport-level issues.
Condition of Validity
* **Applies when:** The transport lacks ordering guarantees, messages have causal dependencies, and out-of-order delivery is possible. * **Does not apply when:** The transport guarantees ordering (single TCP stream, dedicated queue), or the messages are strictly commutative and independent.
Decision Boundary
- **Does the message include causal dependency information?** If no, the receiver cannot order it. It must either use transport order (accepting risk) or require explicit dependency annotation.
- **Are all causal antecedents present in the receiver's state?** If yes, process immediately. If no, buffer the message until antecedents arrive.
- **Has the message been buffered for longer than the acceptable window (e.g., 30 seconds)?** If yes, escalate — the causal chain may be broken, requiring re-delivery or re-synchronization.
- **Conversely, when can the receiver process messages without causal ordering?** Only when the messages are strictly commutative and idempotent, or when the message is explicitly marked as `ORDER_INDEPENDENT`.
Operational Effect
- **Causal Correctness:** Ensures dependent actions execute in the correct order regardless of transport.
- **Detectable Stalls:** Makes causal chain breaks visible via buffering timeouts.
- **Safe Idempotency:** Allows independent messages to bypass ordering, preserving throughput.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class OrderingVerdict(Enum):
PROCESS_IMMEDIATELY = auto()
BUFFER_WAIT_FOR_ANTECEDENT = auto()
ESCALATE_BROKEN_CHAIN = auto()
ORDER_INDEPENDENT = auto()
@dataclass(frozen=True)
class OrderedMessage:
message_id: str
logical_clock: int
causal_dependencies: frozenset[str]
order_independent: bool
@dataclass(frozen=True)
class ReceiverState:
processed_message_ids: frozenset[str]
buffered_messages: tuple[str, ...]
current_epoch: float
buffer_start_epoch: dict[str, float]
@dataclass(frozen=True)
class OrderingPolicy:
max_buffer_age_sec: float
current_epoch: float
@dataclass(frozen=True)
class OrderingManifest:
verdict: OrderingVerdict
directive: str
manifest_digest: str
class CausalOrderingGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_message(
cls, msg: OrderedMessage, state: ReceiverState, policy: OrderingPolicy
) -> OrderingManifest:
# Gate 1: Order independent
if msg.order_independent:
digest = cls._hash(json.dumps({
"message": msg.message_id,
"status": "ORDER_INDEPENDENT"
}, sort_keys=True))
return OrderingManifest(
verdict=OrderingVerdict.ORDER_INDEPENDENT,
directive=f"PROCEED: Message {msg.message_id} is order-independent.",
manifest_digest=digest
)
# Gate 2: Antecedents present
missing = msg.causal_dependencies - state.processed_message_ids
if not missing:
digest = cls._hash(json.dumps({
"message": msg.message_id,
"clock": str(msg.logical_clock),
"antecedents": tuple(sorted(msg.causal_dependencies))
}, sort_keys=True))
return OrderingManifest(
verdict=OrderingVerdict.PROCESS_IMMEDIATELY,
directive=f"PROCEED: All antecedents present for {msg.message_id}.",
manifest_digest=digest
)
# Gate 3: Buffer with timeout
if msg.message_id in state.buffer_start_epoch:
age = policy.current_epoch - state.buffer_start_epoch[msg.message_id]
if age > policy.max_buffer_age_sec:
return OrderingManifest(
verdict=OrderingVerdict.ESCALATE_BROKEN_CHAIN,
directive=f"HALT: Message {msg.message_id} buffered {age:.0f}s > {policy.max_buffer_age_sec:.0f}s. Chain broken.",
manifest_digest=cls._hash(f"BROKEN:{msg.message_id}")
)
# Gate 4: Buffer
digest = cls._hash(json.dumps({
"message": msg.message_id,
"missing": tuple(sorted(missing))
}, sort_keys=True))
return OrderingManifest(
verdict=OrderingVerdict.BUFFER_WAIT_FOR_ANTECEDENT,
directive=f"BUFFER: {msg.message_id} waiting for {sorted(missing)}.",
manifest_digest=digest
)