{"slug": "how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it", "title": "How parallel AI agents should talk to each other (and the bug that proved it)", "summary": "A developer building parallel coding agents found that relaying messages between sessions via copy-paste loses provenance and resembles prompt injection, so they replaced it with references to committed artifacts. They then discovered a bug in their inbox tool where a regex matched the word 'done' in boilerplate text, hiding open messages, highlighting the need for negative-control tests on real inputs.", "body_md": "If you run more than one coding agent at a time, you hit a problem nobody has a settled answer for: **how do two agent sessions message each other?** Not the model talking to a tool — two independent sessions, running in parallel, that need to hand off a decision or a result.\n\nThe obvious channel is a human relaying copy-paste. I spent a day watching that fail in two specific ways, then replaced it, then found a bug in the replacement that is the best argument for the whole approach. Here's the pattern and the evidence.\n\nIt fails for two reasons that have nothing to do with typos.\n\n**1. It loses provenance.** When a message arrives as pasted text, the receiver cannot prove who wrote it. That matters more than it sounds. Two messages reached a session this way in one day: one **asserted a state that had never happened** (\"you enabled X\" — to a session that had done no such thing), and one reported **CI green on a commit that was already two commits stale**. Both were caught. They were caught *only because the receiver independently checked* — the message itself carried zero evidence.\n\n**2. It looks exactly like a prompt injection.** \"Read this and do it\" is the shape of an attack. A well-behaved agent should be suspicious of instructions with no verifiable origin — which means the safe agent and the useful relay are in direct conflict.\n\nThe fix is to stop sending *content* and start sending a *reference to a committed artifact*:\n\n`Read <path> and follow it.`\n\n```\ngit log -1 --format='%an %ci' -- path/to/message.md\n```\n\nNow the message has an author, a timestamp, and an immutable history. \"Read this and do it\" stops being injection-shaped because the receiver can verify the sender before it acts — and can refuse anything whose provenance it doesn't like. Refusal becomes a *feature*, not a failure.\n\nAdd a naming convention so a session can find its own mailbox — `message-to-<recipient>-<topic>-<date>.md`\n\n— and a tiny script that lists the open ones at session start. Round-trip on this channel that day: a message sent, read, acted on, and replied to in **nine minutes**, with a verifiable trail at every step.\n\nHere's the part I didn't plan. I wrote the \"list my open messages\" tool. It ran, and it reported ** 0 open** — while two real messages sat unread. The tool hid its own inbox.\n\nThe cause was one line. Each message carries a status, and the closer looks like this:\n\n```\n// BROKEN: \"done\" appears in the boilerplate of every OPEN message too\nfunction isClosed(statusLine) {\n  return /\\bdone\\b/i.test(statusLine);\n}\n```\n\nEvery *open* message's status line reads: `STATUS: open · Set to \"done\" after you consume it.`\n\nThe word `done`\n\nis right there in the instructions — so the check marked open messages closed and filtered them out. The fix reads the **value**, not the line:\n\n```\n// FIXED: read only the token right after STATUS:\nfunction isClosed(statusLine) {\n  const m = statusLine.match(/STATUS:\\s*\\**\\s*([a-z-]+)/i);\n  return (m ? m[1].toLowerCase() : \"\") === \"done\";\n}\n```\n\nThe lesson isn't \"parse carefully.\" It's this: **a green result reads as \"safe\" when it often means \"the check can't see.\"** Before you trust any all-clear — a gate, a linter, an inbox that says empty — prove the red can appear. One test would have caught it:\n\n```\n// negative control: an OPEN message whose text also contains \"done\" must still surface\nassert(isClosed('STATUS: open · Set to \"done\" after you consume') === false);\n```\n\nMy synthetic test fixture used a clean status line, so it never contained the trap. The real message did. That is the whole case for dogfooding on real inputs, and for writing the test that tries to make green turn red *before* you rely on it.", "url": "https://wpnews.pro/news/how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it", "canonical_source": "https://dev.to/ahmadammar/how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it-2mh1", "published_at": "2026-08-21 17:21:19+00:00", "updated_at": "2026-08-21 17:44:48.480883+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it", "markdown": "https://wpnews.pro/news/how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it.md", "text": "https://wpnews.pro/news/how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it.txt", "jsonld": "https://wpnews.pro/news/how-parallel-ai-agents-should-talk-to-each-other-and-the-bug-that-proved-it.jsonld"}}