fix: make two processes sharing one mailbox visible (#161) - #169
Closed
yonidavidson wants to merge 3 commits into
Closed
yonidavidson wants to merge 3 commits into
yonidavidson wants to merge 3 commits into
Conversation
`peek` shows messages without consuming; `inbox` consumes what it shows. With only those two there was no way to mark mail read that arrived any other way, so an agent that had read all five messages via `peek` and replied to every one still showed five unread — and the stop guard blocked every turn for fifteen turns demanding it read them. `agentcomm ack <id…>` / `ack --all` archives pending messages by id. It works off the KEYS (the id is in the key), so nothing is re-fetched, and ids that are not pending for you are reported with a non-zero exit instead of passing silently — acking someone else's mail is not a quiet no-op. `peek` now closes with the one-liner that clears what you handled, the stop guard says the same thing, and the guidance file teaches peek+ack as the non-destructive read path.
The daemon mirrored the entire bus — every key AND every body — and refreshed it on every poll. On a snapshot-capable backend that is one round trip, but it read every blob in the store each time: pending mail, the roster, the whole 30-day archive, and every telemetry batch. The cost of a poll therefore grew with all history, forever, on a bus where nothing but the last few messages is hot. The mirror now holds what is worth holding. `agents/` is re-read every poll (records mutate); `inbox/` is always warm; `read/` and `events/` are warm only inside a window (AGENTCOMM_MIRROR_HISTORY_MS, default 7 days). Older history keeps its KEY, so list, log, purge and channel discovery see the whole store exactly as before, and a cold body loads on demand through the `get` passthrough. Message blobs are immutable, so a body already held is never re-read either: steady state is "the roster plus whatever is new". `Snapshottable.snapshot` grew a body filter and now reports the keys it saw alongside the bodies it read — listing names is cheap, reading blobs is not.
A subagent inherits its parent's git identity and process tree, so it derives the parent's alias. Because reads consume, one `agentcomm inbox` from a subagent drains the mailbox its parent is waiting on — silently, since the alias looks exactly right. One did; it noticed only because the CLI echoed the parent's name and the agent recognised it. The smaller version of the same thing: a subagent's `register --status` overwrote the parent's line on the shared roster. Identity stays per-session on purpose — a session IS a mailbox, and the sticky fingerprint that fixed alias drift depends on it. What was missing is visibility, so while a process acts as an alias it now leaves a local lease behind, and anything destructive another live process does to that mailbox says so: a consuming read warns that it is taking mail addressed to a live holder and names the remedy (--as <alias>-<role>), and a status write warns whose line it is replacing. Heartbeats, peeks and queue claims lease quietly — a shared queue is meant to have many claimers. Leases are local files, because the case they cover — two processes of one agent session on one machine — is exactly the local case. A lease from a dead process is ignored and cleaned up, never inherited.
yonidavidson
force-pushed
the
perf/167-bounded-mirror
branch
from
August 9, 2026 08:06
1555f67 to
ec268bf
Compare
yonidavidson
force-pushed
the
fix/161-mailbox-leases
branch
from
August 9, 2026 08:06
d64e097 to
2d79625
Compare
yonidavidson
force-pushed
the
perf/167-bounded-mirror
branch
from
August 9, 2026 08:12
ec268bf to
3183f2c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #161. Stacked on #168 → #166 → #165 (rebases down to
mainas those merge).A subagent inherits its parent's git identity and process tree, so it derives the parent's alias — and because reads consume, one
agentcomm inboxfrom a subagent drains the mailbox its parent is waiting on. Silently, because the alias looks right.The one suggestion not taken, and why
The issue proposes deriving the alias from something per-process (pid, spawn nonce). That would break the model deliberately: a session is a mailbox, and the sticky per-session fingerprint is exactly what fixed alias drift in #157 — making the name per-process would re-split every mailbox the moment a wrapper process appears. What was actually missing is visibility, which is the issue's own second and third suggestions.
What changed
src/session.ts— mailbox leases. While a process acts as an alias it leaveslease-<alias>-<pid>.jsonin the state dir; a lease whose pid is dead (or older than 30 min) is ignored and removed, never inherited. Local files, because two processes of one agent session on one machine is exactly the local case.src/cli.ts—guardMailboxclassifies each command: a consuming read (inbox,ack) warns that it is taking mail addressed to a live holder, names what that process is doing, and gives the remedy (--as <alias>-<role>); a status write warns whose roster line it replaces; heartbeats,peek,waitandclaimlease quietly —waitbecause holding the mailbox is the whole point of a listener,claimbecause a shared queue is meant to have many claimers.Tests
test/mailbox-lease.e2e.test.ts: a real second process holds the alias while a consuming read runs (warns, with the remedy), a solo read stays silent and cleans up its lease, a status write reports whose line it takes while a heartbeat does not, and a dead process's lease is ignored and removed.