feat: show whether a recipient is actually reading (#162) - #170
Closed
yonidavidson wants to merge 4 commits into
Closed
yonidavidson wants to merge 4 commits into
yonidavidson wants to merge 4 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.
Every `send` reported `sent <id> → <recipient>`, including the ones the recipient could never retrieve. In a session where several recipients were failing to read, the bus reported success 25 times while delivery was degraded, and no sender ever got a hint. `sent` has only ever meant QUEUED. The bus already knew enough to say more: pending inbox keys, and now a `lastRead` stamp written when an agent actually consumes its mailbox. So `send` warns when the evidence says nobody is picking the mail up — the recipient never registered under that name, or has unread stacking up with no consuming read in AGENTCOMM_STALE_READ_MS (6h). `agents` carries each agent's unread depth and how long ago it last read; `network` shows the same, plus a section for mail queued to names nobody ever registered, which a roster built from registrations alone cannot show at all. Counting is keys-only (one list, no bodies), and the read stamp rides commands agents run a handful of times a session.
yonidavidson
force-pushed
the
fix/161-mailbox-leases
branch
from
August 9, 2026 08:16
2d79625 to
6e1301f
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 #162. Top of the stack (#169 → #168 → #166), rebases to
mainas those merge.Every
sendreportedsent <id> → <recipient>, including the ones the recipient could never retrieve: 25 sends reported success in one session while delivery was degraded, and no sender ever got a hint.senthas only ever meant queued.What changed
src/bus.ts—lastReadon the agent record, stamped bymarkReadwhen an agent consumes its mailbox (inbox,ack). Only an existing registration is stamped: registrations are never purged, so a one-offinbox --as someonemust not leave a permanent ghost on the roster. PlusunreadCounts()/unread(name)— per-recipient depth from keys alone, one list, no bodies.src/cli.ts—sendwarns when the evidence says nobody is picking the mail up: the recipient never registered under that name, or has unread stacking up with no consuming read withinAGENTCOMM_STALE_READ_MS(6h). The warning rides--jsontoo.agentsgains unread depth and last-read age;networkshows the same, plus unread for nobody — mail queued to names nobody ever registered, which a roster built from registrations alone cannot show at all.Tests
test/bus.test.ts: unread counts (including unregistered mailboxes),lastReadstamped and preserved across heartbeats, and no registration conjured by a read.test/cli.e2e.test.ts: an unregistered recipient warns on the first send, a registered one only once mail stacks up unread, the warning stops after a real read, andnetwork --jsonlists the unclaimed mailbox.