SendMessage concurrent writes lose messages via array rewrite

Resolved 💬 5 comments Opened Apr 27, 2026 by CH-JayShanmugam Closed Jul 2, 2026

Summary
When two subagents call SendMessage targeting the same recipient simultaneously, messages are silently lost — the inbox file ~/.claude/teams/<team>/inboxes/<recipient>.json is a single JSON array rewritten on every write (read-modify-write), with no locking. Both writers read the same N-element array, both write back N+1, only one wins; the other's message vanishes with no error surfaced to the sender.

Reproduction
Synthetic, deterministic:

  1. Start a session with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1. Create a team. Spawn three subagents: team-lead + agent-A + agent-B.
  2. From the orchestrator, send a single message instructing both A and B to SendMessage to team-lead "now." Both will fire the SendMessage tool roughly simultaneously.
  3. Inspect ~/.claude/teams/<team>/inboxes/team-lead.json. Sometimes only one of the two messages appears in the array. Frequency depends on timing; in our tests, observable ~1-in-N attempts where N is small (10s).

Alternatively, reproduce via filesystem-only test:

# Simulate two concurrent SendMessage writes
F=~/test-inbox.json
echo '[]' > $F

( jq '. + [{"from":"A","ts":1}]' $F > $F.tmp.A && mv $F.tmp.A $F ) &
( jq '. + [{"from":"B","ts":2}]' $F > $F.tmp.B && mv $F.tmp.B $F ) &
wait
cat $F # often shows only one of the two messages

The harness's SendMessage does not appear to use flock or per-message files, exhibiting exactly this pattern.

Observed behavior

  • Inbox file shape is a single JSON array of message objects:

[
{"from": "...", "text": "...", "summary": "...", "timestamp": "...", "color": "...", "read": true},
...
]

  • The array grows linearly across a session (54 entries in team-lead.json after a ~hour-long session in our test) — confirming whole-file rewrite, not line-append (NDJSON would let us correlate file size with incremental growth).
  • Protocol JSON (idle_notification, shutdown_approved) is also stored in the same array, indicating the array is the canonical write target for ALL message types.
  • We have not yet caught a concrete loss with full forensics because we lacked instrumentation, but the pattern is structurally vulnerable. The synthetic repro above demonstrates the race.

Expected behavior
Three viable fixes, in order of robustness:

  1. Per-message file with monotonic key (most robust):

inboxes/<recipient>/<unix-ms>-<sender>-<random>.json

Race-free by construction. Easy to age out (delete after read or after N days). Matches the _archive/<team>-<timestamp> sharding pattern already used for archived teams.

  1. NDJSON append with O_APPEND:

inboxes/<recipient>.ndjson # one JSON object per line, append-only

POSIX guarantees write() to an O_APPEND fd is atomic for sizes ≤ PIPE_BUF (4096 bytes). Larger messages still need locking, but most messages are small.

  1. flock(2) around read-modify-write:

fd = open("inbox.json", O_RDWR)
flock(fd, LOCK_EX)
// read, modify, write
flock(fd, LOCK_UN)

Easiest to implement, but adds latency under contention.

Why this matters
Inbox delivery is the only inter-agent communication channel in the agent-teams system. Silent message loss means:

  1. An agent's "task complete, here are the results" message can vanish — the orchestrator believes the agent is still working and waits indefinitely.
  2. Critical handoffs (e.g., Architect → Backend-dev → Code-reviewer in our Gate-1/Gate-2 review pattern) can lose the intermediate review payload — the next agent gets the spawn signal but not the diff context.
  3. The race is most likely to happen exactly when two agents are most productive (both finishing tasks at roughly the same time, both reporting to team-lead) — the worst possible moment.
  4. There is no error surfaced to the sender. SendMessage returns "delivered" because the local write succeeded; the loss happens at merge time when the second writer overwrites the first.

This is silent data loss in the team's primary IPC channel. Hard to detect without instrumentation; easy to mistake for an agent "going silent" or "going off-task."

Environment

OS: macOS 24.6.0 (Darwin Kernel) ARM64
claude: 2.1.119 (Claude Code)
bun: 1.3.5
Filesystem: APFS
Files: ~/.claude/teams/<team>/inboxes/<recipient>.json
Flag: CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗