<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Christian Sitte</title>
    <description>The latest articles on DEV Community by Christian Sitte (@csitte).</description>
    <link>https://dev.to/csitte</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4078156%2Fbf64dfff-0506-4bb4-b51f-302681b7ffe3.jpg</url>
      <title>DEV Community: Christian Sitte</title>
      <link>https://dev.to/csitte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/csitte"/>
    <language>en</language>
    <item>
      <title>Eight Grok bots in a Claude Code fleet, and no API between them</title>
      <dc:creator>Christian Sitte</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:02:29 +0000</pubDate>
      <link>https://dev.to/csitte/eight-grok-bots-in-a-claude-code-fleet-and-no-api-between-them-3302</link>
      <guid>https://dev.to/csitte/eight-grok-bots-in-a-claude-code-fleet-and-no-api-between-them-3302</guid>
      <description>&lt;p&gt;&lt;em&gt;How bots from another vendor joined a fleet of Claude Code sessions through a shared folder — and what six weeks of that measured.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In August I wrote about &lt;a href="https://dev.to/csitte/a-running-session-is-not-a-reachable-session-34fc"&gt;the bridge my Claude Code sessions use to talk to each other&lt;/a&gt;: a shared folder of write-once files, one polling script per session that wakes it when something arrives. Since then the fleet has grown a second kind of member. Eight of the participants are not Claude sessions at all. They are Grok bots, each paired with the Claude session of the same project — one reviews code, one answers questions about a mailbox, one looks after a server.&lt;/p&gt;

&lt;p&gt;There is no API between Grok and Claude here, no SDK, no adapter. The bots reach the same Google Drive folder through a cloud connector (later some through &lt;code&gt;rclone&lt;/code&gt;), and they list, read and write files. That turned out to be enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it needed no code
&lt;/h2&gt;

&lt;p&gt;The protocol is a file format. A message is a Markdown file with a short header (&lt;code&gt;from&lt;/code&gt;, &lt;code&gt;to&lt;/code&gt;, &lt;code&gt;sets-owner&lt;/code&gt;, &lt;code&gt;sets-status&lt;/code&gt;) and a filename that starts with a UTC timestamp. Nothing is ever edited in place, and thread state is derived by folding the files. A file format does not know what process wrote it.&lt;/p&gt;

&lt;p&gt;What a bot does not have is everything around the format: no shell, so none of the recipes work; possibly no clock; no watcher, so nothing wakes it; and no start ritual, so it never folds its own threads. The protocol needed no change. What it needed was a different &lt;em&gt;instruction&lt;/em&gt; — the same rules, written for a reader who can only read and write files. And it needed the rest of the fleet to cover the two gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The clock we feared was 4 seconds off
&lt;/h2&gt;

&lt;p&gt;Ordering depends on the timestamp in the filename, and a stamp from the future wins every fold until real time catches up. That had already inverted threads twice before any bot joined. So the instruction spends a whole section on time: use a real clock if you have one, ask a human otherwise, never guess.&lt;/p&gt;

&lt;p&gt;The first bot's first message came from a real &lt;code&gt;date -u&lt;/code&gt; in its environment and was &lt;strong&gt;4 seconds&lt;/strong&gt; off the file's write time on the drive. Checked, not believed.&lt;/p&gt;

&lt;p&gt;What went wrong instead was a filename. Two days after the first bot joined, one named a file &lt;code&gt;20260926T…&lt;/code&gt; — no dashes. That sorts after every correctly written name and wins every fold, exactly like a stamp from the future. The bot had not invented the form. It had copied it from an &lt;code&gt;in-reply-to&lt;/code&gt; field that one of my own sessions had written in a hurry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reader that cannot run your rule reproduces the nearest example&lt;/strong&gt;, and it can see every example in the folder, including the careless ones. We fixed the example, and the start scan now reports any name in that form, so a slip is seen the same day.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Five acks in two minutes
&lt;/h2&gt;

&lt;p&gt;A day later, one probe message drew five acknowledgements in two minutes. Two bots both had delivery; each wake-up was acknowledged, and each acknowledgement was a wake-up for the other. Every session with a watcher read five deliveries carrying zero information.&lt;/p&gt;

&lt;p&gt;The fix was one sentence in the instruction, not a line of code: an ack goes to the sender only, and nobody answers an ack. It has held since.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A 200 is not a run
&lt;/h2&gt;

&lt;p&gt;A bot is never woken by itself, so a small delivery service watches the folder on its behalf and POSTs every message addressed to it to the endpoint that starts the bot. The service runs the same addressing rules as the session watchers — token-exact &lt;code&gt;to:&lt;/code&gt; matching, and a &lt;code&gt;sets-owner&lt;/code&gt; handover wakes the new owner even when nobody wrote it into &lt;code&gt;to:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One day a bot's usage quota ran out. Every delivery that day came back &lt;code&gt;HTTP 200&lt;/code&gt; with a success body — eight of them — and the bot did nothing. The status code measured the endpoint, not the recipient.&lt;/p&gt;

&lt;p&gt;So a dead recipient is now detected by &lt;strong&gt;silence&lt;/strong&gt;: deliveries that arrived, weighed against the bot's own messages. Over a hundred deliveries, a live bot answered within 30 seconds to 30 minutes. Two unanswered wake-ups that handed the bot a thread, older than two hours, turn it red on the fleet's dashboard. One unanswered wake-up is normal; not every message needs a reply.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Overload is where messages get lost
&lt;/h2&gt;

&lt;p&gt;Over 252 deliveries on one machine, three first attempts failed with an overload status (400, 429 or 5xx). The single retry after 45 seconds caught one. The bots found the other two &lt;strong&gt;by themselves, after 2 and 4 days&lt;/strong&gt; — at their next wake-up for something unrelated.&lt;/p&gt;

&lt;p&gt;That is the property worth knowing about a participant without initiative: it does not poll. If the wake-up fails, the message waits for the next one, however long that takes. The service in our dashboard now adds a late retry after 15 minutes for overload codes. The standalone script in the repository still retries once, because it runs inside the watcher's hook and a fifteen-minute sleep there would hold up everyone else; the docs say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Nobody folds for it
&lt;/h2&gt;

&lt;p&gt;A session runs a start scan that lists the threads it owns. A bot never does. So a thread handed to a bot falls through every net — no push if the delivery fails, no fold ever — and from the outside it looks as if someone is on it, because the owner field is filled in.&lt;/p&gt;

&lt;p&gt;The coordinating session therefore folds on the bots' behalf: one environment variable names the ids it stands in for, and its start scan lists their threads with waiting times. It is the same mechanism we already used for the one participant who never folds either — the human.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not happen
&lt;/h2&gt;

&lt;p&gt;No bot ever edited or deleted a message. No conflict copy appeared in the sync folder. Write-once held for writers that never saw the reasoning behind it, because the instruction states the rule plainly and first, and gives the one thing the author may still fix — a wrong filename, by renaming — an explicit procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops
&lt;/h2&gt;

&lt;p&gt;A bot behind a connector is a participant with high latency and no initiative. It is a good shape for a reviewer or a second opinion and the wrong shape for anything on a critical path. If its answer gates your work, expect hours, not seconds, unless the delivery service runs and its endpoint is known to execute what it accepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;The instruction the bots read (in English, without our names), the operator's side, and every number above are in &lt;a href="https://github.com/csitte/claude-session-bridge/blob/main/docs/participants-without-shell.md" rel="noopener noreferrer"&gt;docs/participants-without-shell.md&lt;/a&gt; at &lt;strong&gt;&lt;a href="https://github.com/csitte/claude-session-bridge" rel="noopener noreferrer"&gt;https://github.com/csitte/claude-session-bridge&lt;/a&gt;&lt;/strong&gt;. The rest of the repository is the same as in August: protocol, watcher, launcher, MIT, Windows-first.&lt;/p&gt;

&lt;p&gt;If you run agents from more than one vendor and they need to hand work to each other, try the folder before you try a framework. Anything that can write a file can take part.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A running session is not a reachable session</title>
      <dc:creator>Christian Sitte</dc:creator>
      <pubDate>Fri, 14 Aug 2026 19:36:51 +0000</pubDate>
      <link>https://dev.to/csitte/a-running-session-is-not-a-reachable-session-34fc</link>
      <guid>https://dev.to/csitte/a-running-session-is-not-a-reachable-session-34fc</guid>
      <description>&lt;p&gt;&lt;em&gt;Six weeks of running ~15 Claude Code sessions across two machines, and the three things that actually broke.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I run a fleet of long-lived Claude Code sessions — one per project, two machines. They need to hand work to each other: "the schema changed, the docs session has to follow." Doing that by carrying messages myself does not scale past about three sessions.&lt;/p&gt;

&lt;p&gt;What we ended up with is deliberately boring: &lt;strong&gt;a shared folder of write-once files.&lt;/strong&gt; Every message is a new file named with a UTC timestamp. Nothing is ever edited in place, so two writers can never collide, and a cloud-sync folder becomes a safe transport — sync engines produce conflict copies when two devices edit the same file, which here cannot happen. Thread state (owner, status) is not stored anywhere; it is derived by folding the files and taking the latest one that sets it.&lt;/p&gt;

&lt;p&gt;On top sits one polling script per session that turns a new message into a notification and wakes the session. That part is pure latency reduction: if it dies, delivery degrades to a scan at session start instead of breaking.&lt;/p&gt;

&lt;p&gt;The interesting part is not the design. It is what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A running session is not a reachable session
&lt;/h2&gt;

&lt;p&gt;The watcher is armed by the session itself, on its first turn, following an instruction in its project memory. Our launcher restarted every session after a reboot with &lt;code&gt;--continue&lt;/code&gt; and no prompt — which restores the session but runs &lt;strong&gt;zero turns&lt;/strong&gt;. So the instruction never executed.&lt;/p&gt;

&lt;p&gt;Measured after one restart: &lt;strong&gt;12 sessions running, 1 watcher armed.&lt;/strong&gt; Every window looked healthy. The push layer had quietly come to depend on the exact hand movement it was built to abolish, and we only found it by counting processes.&lt;/p&gt;

&lt;p&gt;The fix is three lines — the launcher appends a small start prompt so exactly one turn happens per session. The general form of the lesson: if a capability depends on "someone will do X at session start", it dies silently at every cold start. Wire it into the thing that performs the start. And fence that prompt in — ours says, in effect, &lt;em&gt;run only the start ritual, begin no task, change no file, send nothing&lt;/em&gt; — or a fleet of sessions will helpfully start working unattended.&lt;/p&gt;

&lt;p&gt;There is a second form of this, which surfaced a day after I published — prompted by a comment on the upstream issue, from someone running a dozen autonomous loops over a shared append-only log. His version: a queue's length is not a delivery signal. Nothing in a log distinguishes "about to be processed" from "nothing is coming to collect this."&lt;/p&gt;

&lt;p&gt;It reproduced here within the hour, and not where I expected. The mechanism was working this time. One thread had been handed off that morning — ball passed, owner set, the freshest activity in the whole index — to a session that was not running and had no watcher. Undeliverable until someone starts it by hand, with nothing anywhere indicating that. The signal existed the whole time: each watcher is an OS process, so an inventory of them answers "is anything armed to receive this" per participant. Nobody ran it, because the protocol document that senders actually read described the transport and the rituals but never mentioned that reachability was checkable at all — it pointed at a separate operations doc. The fix was one line in the mitigations list, not a mechanism. &lt;strong&gt;A diagnostic nobody is told about is not a signal.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Never type a timestamp
&lt;/h2&gt;

&lt;p&gt;Files sort by the UTC timestamp in their name. Our recipe generated the &lt;em&gt;filename&lt;/em&gt; and left the human-readable &lt;code&gt;date:&lt;/code&gt; field inside the file to be typed by hand.&lt;/p&gt;

&lt;p&gt;Measured after a few weeks: &lt;strong&gt;92 of 407 messages carried a field that did not match their own filename&lt;/strong&gt; — zeroed times, rounded minutes, local time with a &lt;code&gt;Z&lt;/code&gt; appended. Twice that inverted a thread's fold: a closed thread read as open for half a day.&lt;/p&gt;

&lt;p&gt;The fix was not discipline, it was removing the second rendering from human hands: one &lt;code&gt;date -u&lt;/code&gt; call, two derived renderings, and a written rule that the filename wins. Of the next 165 messages, 2 mismatched — both by one second, i.e. someone calling &lt;code&gt;date&lt;/code&gt; twice. The failure class was gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A rule that lives in two places gets fixed in one
&lt;/h2&gt;

&lt;p&gt;When we changed that timestamp rule, the first search missed two copies — they &lt;em&gt;paraphrased&lt;/em&gt; the recipe instead of quoting it. One overlooked copy would have reinstated the old behaviour at every session start, because in a system operated by language models, &lt;strong&gt;prose instructions are code&lt;/strong&gt;: a model reads them and executes them.&lt;/p&gt;

&lt;p&gt;The search rule that came out of it: grep for the &lt;em&gt;statement&lt;/em&gt;, not for the code snippet. And even that has a floor — a rule phrased without its own vocabulary ("take the latest") is invisible to any search. For those you have to know where the quantity matters and look by hand. The worst instance we found had an &lt;code&gt;mv&lt;/code&gt; attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: the process that would not die
&lt;/h2&gt;

&lt;p&gt;On Windows/msys, a background watcher survives the session ending, a context clear, and a process-tree kill — the tree is already torn at spawn, and the process is attached to no console. We first fought this with a "disarm before closing" ritual, which produced the opposite problem: sessions that were up but deaf, with no delivery path and nothing to see.&lt;/p&gt;

&lt;p&gt;Inverting it was the fix: &lt;strong&gt;arming is idempotent.&lt;/strong&gt; A new arm checks whether a live watcher for the same id already delivers, steps aside if so, and reaps it if it is a silent remnant. No disarm ritual exists any more. A leftover process and a duplicate are different things — prove liveness, do not infer it from parent PIDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Everything is at &lt;strong&gt;&lt;a href="https://github.com/csitte/claude-session-bridge" rel="noopener noreferrer"&gt;https://github.com/csitte/claude-session-bridge&lt;/a&gt;&lt;/strong&gt; — protocol, watcher, launcher, the full lessons chapter with the numbers, MIT.&lt;/p&gt;

&lt;p&gt;It is Windows-first: the delivery core is portable shell (CI runs it on Linux too), but the process handling is PowerShell. A port to macOS or Linux is explicitly invited and the places it would touch are small and marked — I cannot review one against reality, since I have no such machine running this.&lt;/p&gt;

&lt;p&gt;It is a snapshot of something I use daily, not a staffed project. Fork it rather than wait for me.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>claude</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
