devkernels
All writing
·12 min read·Ankit Mishra

Multi-Agent AI is a Distributed Systems Problem — Here's the Architecture No One Talks About

Article

Now I have everything I need. Writing the full article.

§

SUBJECT: Multi-agent AI is a dist-sys problem PREVIEW: The coordination patterns LLM frameworks skip—from a system running 6 agents in production

TITLE: Multi-Agent AI is a Distributed Systems Problem — Here's the Architecture No One Talks About

§

You're building an agent swarm. That's the trap.

Not because agents are wrong. Because you're thinking about them like functions — isolated, stateless, invocable — when every distributed systems engineer knows that's exactly how you design a system that fails catastrophically in production.

Lobsters lit up this week with the post "Multi-agentic Software Development is a Distributed Systems Problem (AGI can't save you)." The thesis is correct. The architecture it implies is not what most people are building. And the gap between "correct thesis" and "correct implementation" is where real systems eat your lunch.

I've been running a 6-agent system in production — persistent memory, cron-scheduled jobs, human-in-the-loop approval gates, cross-agent session harvesting, a WebSocket gateway with delivery recovery — for several months. Here's what distributed systems theory actually maps to when you implement it with LLMs.

§

The LangGraph Tax

Most multi-agent frameworks — LangGraph, AutoGen, CrewAI — solve the easy problem: routing.

They give you a graph where node A can call node B, node B can fork into C and D, and a supervisor can retry failures. That's a directed acyclic execution graph. It's useful. It's also roughly equivalent to what distributed systems engineers built in 1999 and called a workflow engine.

What they don't give you:

  • Persistent agent identity across sessions
  • Cross-agent shared memory with conflict resolution
  • Human approval gates with timeout and fallback
  • Delivery recovery for failed async outputs
  • Heartbeat monitoring with graceful degradation
  • Coordinated failure modes that don't cascade

This isn't a critique of those frameworks. They're building abstractions on top of LLM APIs and solving their own coordination problems. It's an observation that if you're running agents in production with real consequences — agents that write files, send messages, post content, update databases — you're operating in distributed systems territory whether your framework acknowledges it or not.

§

The Four Distributed Systems Problems You Will Hit

1. Partial Failure Is Your Default State

In distributed systems, nodes fail silently. They don't crash with a clean error — they time out, stall, return partial results, or silently drop messages.

Agents do exactly this.

Here's a real log line from a cron job execution on April 7, 2026:

2026-04-07T02:30:10.892+00:00 No reply from agent.

That's a calendar briefing job. It should run at 2:30 AM UTC, hit the Kairos agent, pull today's schedule, and deliver a formatted message via Telegram. "No reply from agent" means the job completed from the gateway's perspective — the message was dispatched — but the agent either stalled in tool execution, hit a context limit, or returned output that didn't match the expected delivery format.

The system didn't crash. The cron job ran again the next day. But the user got no briefing that morning.

In distributed systems, this is partial failure. The solution isn't retrying the same call harder — it's designing for the failure state: timeout budgets, fallback outputs, idempotent side effects.

What this looks like in practice:

def claude_call(prompt, model="haiku", timeout=120):
    """Call claude -p. Uses haiku by default (cheap)."""
    env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
    cmd = ["claude", "-p", "--model", model, prompt]
    result = subprocess.run(
        cmd, capture_output=True, text=True, timeout=timeout, env=env
    )
    if result.returncode != 0:
        return None  # caller decides what to do with None
    return result.stdout.strip()

The timeout is explicit. The return value is nullable. The caller handles the failure case. That's basic distributed systems hygiene that most agent tutorials skip entirely.

§

2. Delivery Is Not Guaranteed

Messaging systems have a fundamental property called delivery semantics: at-most-once, at-least-once, or exactly-once. At-most-once means you might lose a message. At-least-once means you might deliver it twice. Exactly-once is hard to achieve and usually expensive.

LLM agent output delivery is at-most-once by default. The agent runs, produces output, and the output either arrives or it doesn't. Most frameworks don't build recovery for this.

Here's what recovery looks like when you do build it:

[delivery-recovery] Found 1 pending delivery entries
[delivery-recovery] Recovered delivery 936c366d-6885-4336-a01c-fb040af61a88

The system maintains a delivery log. On restart, it scans for pending deliveries and retries them. This is a write-ahead log — a pattern borrowed from databases for exactly this reason: if you crash between producing a result and delivering it, you need to recover to a consistent state.

The implementation matters: deliveries must be idempotent (delivering twice is safe) and each delivery must have a unique ID (so de-duplication works). Without these two properties, recovery creates more problems than it solves.

[IMAGE: architecture diagram showing delivery-recovery flow — agent output → delivery log → delivery attempt → ack/recovery path]

§

3. Memory Is Shared Mutable State

This is the hardest one. And it's the one most people get completely wrong.

In a single-agent system, memory is simple: you append to context. The problem is context windows are finite, expensive, and non-persistent across sessions. The moment you solve this with external storage — SQLite, a vector DB, a file system — you've introduced shared mutable state.

Shared mutable state is the source of most bugs in distributed systems.

A 6-agent system with persistent memory has three distinct memory problems:

1. Write conflicts. Agent A and agent B both update their view of "what the user is working on." Which one wins? What's the merge strategy?

2. Staleness. Agent A reads memory, makes a decision, takes an action. Meanwhile, agent B has updated that memory. Agent A's action is now based on stale state.

3. Cross-agent consistency. The learning agent (Athena) knows you've been studying system design. The ops agent doesn't. When ops generates a deployment script, should it know your current learning context? How does that knowledge propagate?

The real system stores memory in three separate SQLite databases — one per high-activity agent — plus JSONL session files for raw conversation history:

/root/.openclaw/memory/main.sqlite      # 21.7 MB
/root/.openclaw/memory/learning.sqlite  # 13.3 MB
/root/.openclaw/memory/portfolio.sqlite # 12.8 MB

Cross-agent memory synchronization runs via a scheduled harvest script:

# harvest-sessions.py
AGENT_NAMESPACES = ["main", "learning", "calendar", "architect", "ops", "portfolio"]
# Scans sessions for: corrections, projects, decisions
# Tracks last-processed position per agent in .harvest-state.json

This is event sourcing — the session JSONL files are the append-only log, and the harvest script builds read models (memory stores) from that log. It's not called event sourcing in the codebase. But that's what it is.

The key insight: memory is an infrastructure problem, not a prompt engineering problem. You can't solve write conflicts with a better system prompt. You need a storage architecture with explicit consistency guarantees.

§

4. Coordination Requires a Protocol, Not a Vibe

The most common multi-agent pattern in tutorials: agent A does work, agent B does more work, a supervisor decides what's done. The coordination mechanism is implicit — the supervisor just... knows.

In production, coordination requires explicit protocols.

The hardest coordination problem in LLM systems isn't agent-to-agent — it's human-in-the-loop. You need a human to approve something before the system continues. The human is unreachable for an unknown amount of time. The system has to wait, time out, and handle the case where no approval arrives.

Here's the actual implementation for content pipeline approval gates:

# telegram_wait.py
def wait_for_approval(message, timeout_hours=4):
    """Send message, poll /tmp/pipeline-replies/ for response.
    Falls back to Telegram getUpdates if relay unavailable.
    Returns: (approved: bool, response_text: str)
    """
    send_via_relay(message)
    deadline = time.time() + (timeout_hours * 3600)
    
    while time.time() < deadline:
        reply = check_reply_file("/tmp/pipeline-replies/")
        if reply:
            return parse_approval(reply)
        time.sleep(30)
    
    return False, "timeout"  # rejection by default on timeout

Four hours. If no approval arrives in four hours, the pipeline rejects and the content doesn't publish. Rejection is the safe default on timeout. This matters: in distributed systems, the unsafe default is to proceed — "if we haven't heard no, assume yes." That's how you publish content that wasn't approved.

The file-based polling /tmp/pipeline-replies/ is a low-tech coordination primitive. The Telegram relay drops reply files there when a message arrives. This decouples the pipeline from needing a direct connection to Telegram — the relay handles delivery and the pipeline only reads files.

§

The Gateway Pattern You Actually Need

Every multi-agent system eventually needs a central gateway — not for routing (that's what most people build) but for:

  • Authentication: every agent call is authenticated, not just the entry point
  • Rate limiting and backpressure: prevent one agent from saturating resources
  • Connection multiplexing: multiple agents share underlying API connections
  • Log aggregation: all inter-agent messages pass through one observable point

The OpenClaw gateway runs as a persistent WebSocket server on localhost:18789, bound to loopback only. Each agent authenticates with a token before any message exchange. The binding to loopback is not accidental — it's a network isolation boundary. External processes can't reach the gateway without going through the machine's auth layer.

[gateway] listening on ws://127.0.0.1:18789
[heartbeat] started
[health-monitor] started (interval: 300s, grace: 60s)
[telegram] [athena] starting provider
[telegram] [architect] starting provider
[telegram] [default] starting provider
[telegram] [kairos] starting provider
[telegram] [midas] starting provider

The health monitor runs every 300 seconds with a 60-second grace period. This is liveness detection — if an agent stops responding to heartbeats, the system knows before a real job fails. The alternative is discovering failures when a user asks for something and gets silence.

The gateway runs as a systemd user service:

# /root/.config/systemd/user/openclaw-gateway.service
ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js \
    gateway --port 18789
Restart=always
RestartSec=5
StandardOutput=/root/.openclaw/gateway.log

Restart=always with a 5-second backoff. When the gateway crashes — and it will crash — systemd restarts it. The agents reconnect. No manual intervention. This is crash recovery as infrastructure, not exception handling.

§

Observability Is Non-Negotiable

Here's a dashboard endpoint that aggregates system state across all six agents:

# /api/data — master data aggregation
{
    "agents": {
        "main":      {"status": "green",  "detail": "default bot"},
        "architect": {"status": "green",  "detail": "architect bot"},
        "ops":       {"status": "green",  "detail": "headless"},
        "calendar":  {"status": "amber",  "detail": "partial"},
        "learning":  {"status": "green",  "detail": "athena bot"},
        "portfolio": {"status": "red",    "detail": "no Notion DB"}
    },
    "gateway": "localhost:18789",
    "pipelines": { ... }
}

Portfolio is red because it's missing a Notion database binding. Calendar is amber because it has partial configuration. These aren't errors — they're system states. The difference between a well-designed multi-agent system and a mess is whether you can look at a dashboard and understand the state of every agent in 10 seconds.

The heartbeat guard adds a pre-screening layer before waking the LLM for health checks:

// heartbeat-guard.js
// Rule-based pre-checks before LLM invocation:
// - Check ACTIVE file in memory/index
// - File staleness against memory/index.json
// - Log file TTL (30 days)
// Output: HEARTBEAT_OK (no LLM call) or signal message (wake LLM)

This is a two-tier health check: cheap rule-based checks first, expensive LLM invocation only when the rules say something is actually wrong. In distributed systems, this is equivalent to using a health probe that checks a /health endpoint before routing traffic. Cheap, fast, reliable — reserve the expensive path for actual anomalies.

§

The Pipeline as a Distributed Transaction

The content pipeline runs as a multi-stage distributed transaction with rollback on failure:

nova_gather → nova_analyze → nova_present
    → (human approval)
    → kira_canonical → kira_linkedin → kira_instagram → kira_medium → kira_blog
    → lex_review
    → (human preview approval)
    → render → preview → publish → notion_update → archive

A real execution: content-v2-2026-04-06-1133. Started 11:33:01 UTC, finished 13:46:11 UTC. Total wall time: 2 hours 13 minutes — most of which was waiting for human approval between stages.

That's not slow. That's correct. Human-in-the-loop stages are synchronization barriers. The system waits. The human approves. The system proceeds.

This is two-phase commit with a human as the transaction coordinator. The analogy isn't perfect, but the failure modes are identical: if the human never approves, the transaction aborts. If approval arrives after timeout, the system has already aborted and the approval is ignored. If the system crashes mid-publish, delivery recovery kicks in.

[IMAGE: pipeline stage diagram with human approval gates highlighted as synchronization barriers]

§

Where This Falls Short

The session memory model has a fundamental weakness: cross-agent consistency is eventual, not immediate.

The harvest script runs on a schedule. Between runs, agents are operating on divergent views of shared memory. If the learning agent updates "current focus area" and the main agent makes a recommendation 10 minutes later, the main agent is working from stale data.

The real fix is a synchronous shared memory layer — a single SQLite database with proper locking that all agents read from, rather than per-agent databases synchronized asynchronously. That creates a different problem: lock contention and slower agent responses. There's no free lunch.

The second failure mode: the approval gate timeout is a policy, not a guarantee. If the Telegram relay is down when the pipeline sends its approval request, the pipeline waits for 4 hours and then rejects. The user never saw the request. The content never got reviewed. The system silently ate the work.

Solving this properly requires delivery confirmation on the request side — "did the user actually receive the approval request?" — not just on the response side. That's a harder problem than most agent tutorials acknowledge.

§

What Distributed Systems Actually Teaches You Here

The Lobsters article is right that AGI can't save you. But the more useful framing is this:

Multi-agent AI is not a prompt engineering problem. It's a systems engineering problem with LLMs as the compute layer.

The patterns that work aren't new. They're the same patterns distributed systems engineers have been using for 30 years:

| DS Pattern | Agent Equivalent | |---|---| | Write-ahead log | Session JSONL files + delivery log | | Event sourcing | Memory harvest from session history | | Two-phase commit | Human approval gates in pipelines | | Liveness detection | Heartbeat guard + health monitor | | Circuit breaker | Timeout + rejection-by-default | | Crash recovery | systemd Restart=always | | Coordination primitive | File-based polling for approval replies |

None of this requires a distributed systems PhD. It requires recognizing that when you have multiple agents with shared state, async communication, and human coordination — you're building a distributed system. Use the vocabulary. Use the patterns. Don't reinvent them.

§

The synthesis: agents that matter are agents with side effects, and side effects require transactional discipline.

What to do this week: pick the most critical inter-agent handoff in your current system. Write down what happens when that handoff fails silently. If you don't have an answer, that's your gap — design the failure mode before you need it.

Next: I'm going to cover the memory layer specifically — schema design, conflict resolution strategies, and what "cross-agent consistency" actually means when you have 6 agents writing to overlapping knowledge domains. That'll be a paid post.

§

If this was useful, paid subscribers get this depth 2-3x per week. [Subscribe — $8/month or $80/year]

END

Newsletter

Enjoyed this?

One deep-dive per week — distributed systems, AI infra, the stuff I actually ship.