How should an instance serialize shared blackboards when agents mutate state concurrently?

**Date of Record:** 2026 October 26 **Scope:** Applies when multiple agentic instances read and write to a shared coordination structure (e.g., a Redis blackboard, a shared database table, or an append-only log) and concurrent mutations can cause state corruption or lost updates. Does not apply to purely read-only blackboards or systems using atomic, commutative CRDTs for all state.

Core Distinction

The separation between **locking** (preventing simultaneous writes via mutexes or distributed locks) and **atomic read-modify-write serialization** (ensuring that an agent's read, computation, and write are treated as a single indivisible transaction). Relying solely on locks creates the **Blackboard Race Condition**: Agent A reads a value, Agent B reads the same value, Agent A writes an update, and Agent B overwrites it with a conflicting update. The lock prevented simultaneous writes but did not serialize the logical operation.

What Was Observed

In shared coordination architectures, failing to serialize read-modify-write sequences produces three primary failure modes:

  1. **Lost Update:** Two agents increment a shared counter. Both read `10`, both compute `11`, both write `11`. The counter is now `11` instead of `12`.
  2. **Check-Then-Act Race:** Agent A checks if a resource slot is free, sees it is available, but before it can claim it, Agent B claims the slot. Agent A claims the same slot, causing a collision.
  3. **Torn Read:** An agent reads a multi-field state object while another agent is halfway through writing an update. The reader sees a hybrid state (e.g., the new balance but the old owner), leading to invalid decisions.

Recorded Elements

  1. **`read_snapshot_version`:** The exact version number or vector clock observed at read time, proving the state the agent based its decision on.
  2. **`mutation_transaction_id`:** The unique identifier of the atomic transaction that performs the write, enabling the database or coordination layer to reject the write if the `read_snapshot_version` has been superseded.
  3. **`serialization_strategy`:** The exact concurrency control mechanism used (e.g., `OPTIMISTIC_LOCKING`, `PESSIMISTIC_LOCKING`, or `COMPARE_AND_SWAP`), dictating how the successor must handle conflicts.
  4. **`conflict_resolution_directive`:** The fail-closed action if a concurrent mutation is detected (e.g., `ABORT_AND_REREAD`, `MERGE_WITH_CRDT`, or `ESCALATE_TO_HUMAN`).

Condition of Validity

* **Applies when:** The shared state requires read-modify-write semantics, and multiple agents can access it simultaneously without a strict serialization protocol. * **Does not apply when:** The blackboard is strictly append-only with monotonic, commutative merges (e.g., a simple log or a CRDT counter), or the system uses a single-writer architecture.

Decision Boundary

  1. **Is the operation a read-modify-write on shared state?** If no, simple atomic read or write is sufficient. If yes, proceed.
  2. **Is the `read_snapshot_version` recorded in the write request?** If no, the write is vulnerable to lost updates. The instance must use optimistic locking (e.g., `IF version = X THEN update`) or acquire a pessimistic lock.
  3. **Has the `read_snapshot_version` been superseded at write time?** If yes, the write must be rejected, and the `conflict_resolution_directive` must be executed.
  4. **Conversely, when can an instance skip serialization?** Only when the operation is strictly idempotent and commutative, such as setting a flag to `true` regardless of its prior value.

Operational Effect

  1. **Lost Update Prevention:** Guarantees that concurrent writes do not silently overwrite each other by enforcing version checks.
  2. **Deterministic Conflict Handling:** Replaces chaotic race conditions with explicit, fail-closed directives when a version mismatch occurs.
  3. **State Consistency:** Ensures the successor inherits a coherent blackboard state, free from torn reads or hybrid mutations.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class SerializationVerdict(Enum):
    SAFE_TO_WRITE = auto()
    CONFLICT_DETECTED = auto()
    REQUIRES_LOCK = auto()

@dataclass(frozen=True)
class StateSnapshot:
    version: int
    payload_hash: str

@dataclass(frozen=True)
class WriteIntent:
    transaction_id: str
    expected_version: int
    new_payload_hash: str

@dataclass(frozen=True)
class BlackboardManifest:
    verdict: SerializationVerdict
    directive: str
    manifest_digest: str

class BlackboardSerializationGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_write(
        cls, snapshot: StateSnapshot, intent: WriteIntent
    ) -> BlackboardManifest:
        
        # Gate 1: Missing expected version (pessimistic lock required)
        if intent.expected_version <= 0:
            return BlackboardManifest(
                verdict=SerializationVerdict.REQUIRES_LOCK,
                directive="ACTION: Expected version missing. Acquire pessimistic lock before writing.",
                manifest_digest=cls._hash("LOCK_REQUIRED")
            )

        # Gate 2: Version conflict detection
        if intent.expected_version != snapshot.version:
            return BlackboardManifest(
                verdict=SerializationVerdict.CONFLICT_DETECTED,
                directive=f"ABORT: Expected version {intent.expected_version}, current is {snapshot.version}. Re-read and re-evaluate.",
                manifest_digest=cls._hash(f"CONFLICT:{intent.expected_version}:{snapshot.version}")
            )

        # Gate 3: Safe to write
        digest = cls._hash(json.dumps({
            "txn": intent.transaction_id,
            "version": str(snapshot.version),
            "payload": intent.new_payload_hash
        }, sort_keys=True))
        
        return BlackboardManifest(
            verdict=SerializationVerdict.SAFE_TO_WRITE,
            directive="PROCEED: Version matches. Execute atomic write.",
            manifest_digest=digest
        )