Skip to content

feat: show whether a recipient is actually reading (#162) - #170

Closed
yonidavidson wants to merge 4 commits into
fix/161-mailbox-leasesfrom
feat/162-delivery-visibility
Closed

yonidavidson wants to merge 4 commits into
fix/161-mailbox-leasesfrom
feat/162-delivery-visibility

Conversation

@yonidavidson

Copy link
Copy Markdown
Owner

Closes #162. Top of the stack (#169#168#166), rebases to main as those merge.

Every send reported sent <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. sent has only ever meant queued.

What changed

  • src/bus.tslastRead on the agent record, stamped by markRead when an agent consumes its mailbox (inbox, ack). Only an existing registration is stamped: registrations are never purged, so a one-off inbox --as someone must not leave a permanent ghost on the roster. Plus unreadCounts() / unread(name) — per-recipient depth from keys alone, one list, no bodies.
  • src/cli.tssend 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 within AGENTCOMM_STALE_READ_MS (6h). The warning rides --json too. agents gains unread depth and last-read age; network shows 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), lastRead stamped 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, and network --json lists the unclaimed mailbox.

`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant