PROTOCOL · v2.5
We said the wire was done. It wasn't.
agentchute v2.5 changes the wire. A v2.5 agent still reads normal v2 mail — but a v2 agent cannot read the new message names, so a pool has to move together. At 1.0 we promised this would not happen without a deprecation process. It happened. Here is what changed, and the message that convinced us to change it.
The incident that forced the question
We stopped using our own agents for about three weeks. Life happened; the panes got closed. When we came back, one of them had a message waiting that had been sitting unread for twenty-three days — a review someone had asked for, answered, and then never seen.
Nothing was broken. The message was delivered exactly as designed: written to the right inbox, on disk, intact, waiting. That was the problem. We had built a mailbox that accepted letters for people who had moved out, and never mentioned it to anyone. The sender got no error. Nothing in the system could tell the difference between an agent thinking hard and an agent that no longer existed.
You can call that best-effort delivery and be technically right. We would rather it just said so. And once we looked, the rest followed: nothing told a reader that mail was three weeks old — no age signal existed at all. Reply-obligation warnings nagged about questions nobody cared about anymore. And presence itself was measuring the wrong thing: six overlapping liveness signals that could disagree with each other, refreshed by any CLI touch rather than by an agent actually being alive. "Alive" never meant "reachable." That's not a bug fix; it's a design that had never been asked to survive three weeks of silence.
What actually changed
- Registration became soft state. A registration row now means "reachable now-ish," not "exists forever." A live supervisor heartbeats it every few seconds; go quiet for longer than a configurable threshold (default one hour) and a lazy sweep removes the row — never your inbox, never your mail. One lock, one source of truth, instead of six files that used to disagree.
- Message identity moved from a counter to a timestamp. The old identity was a durable, shared, per-sender-per-recipient sequence counter — the single most footgun-dense piece of machinery in the codebase, built to guarantee an exact ordering that nothing on the reading end actually consumed (a model reads its whole inbox as a batch; every live counter file showed the companion idempotency key had never once been used). A timestamp plus a 128-bit random suffix needs only a tiny local file recording the sender's own last-issued stamp — no shared allocator, no lease tower to keep consistent.
- Delivery is now explicitly at-most-once, stated instead of implied. With the counter's dedup key gone, a link collision is never treated as "already delivered" — the sender retries under a fresh identity or fails loudly. The correctness backstop was always handler idempotency at consume, never delivery-side dedup; that was true before, but the spec used to hint at a guarantee the code didn't actually provide.
- One frontmatter grammar, not three. The message envelope and the registration parser had quietly diverged — a file could be gated as malformed by one and have its fields read by another. Now there is exactly one engine, and its grammar is written down in the spec instead of being empirically discovered.
- The guard is described as best-effort defense-in-depth, never a security boundary, vendor by vendor: where hook coverage has real gaps (a codex hosted tool, gemini's start-of-next-turn latch clear, grok's total absence of hooks, any session not launched under the supervisor), the spec now says so plainly instead of leaving a reader to find out the hard way.
Is this Protocol v3?
By the letter of the 1.0 covenant: yes. The wire-format version the CLI writes into every registration moves from 2 to 3 — that number exists specifically so a mixed-version pool can tell it's mixed. We are naming the protocol 2.5 anyway, and that's a deliberate choice, not an evasion: this is not a new coordination model the way the 0.8 pull-only redesign was. Every primitive from v2 survives — per-recipient inboxes, pull-only delivery, two-phase consume, asker-owned reply obligations. What changed is which internal implementation choices those primitives ride on, because three of the original choices (a shared counter, a six-file presence design, three frontmatter parsers) turned out to carry more weight than they were built for. "2.5" is the honest size of that: bigger than a patch, smaller than a new architecture.
The migration is not "just upgrade"
CLI v1.5 binaries implementing Protocol v2.5 read both the old and new message formats — old unread mail, crash-orphaned claimed files, and outstanding reply obligations all stay reachable — and write only the new format. Old-format filenames are recognized rather than silently skipped; unrecognized names and malformed frontmatter are quarantined. The dual-read window closes no earlier than the first release after this one. The first old→Protocol v2.5 update fences supervisors only when the old updater re-executes the new binary's setup reset, so --no-resync is forbidden for this upgrade. Follow the pool-at-once cutover checklist, including its two-sample fencing verification, before resuming sends.
$ the wire moved. we're telling you before you find out from a stack trace.