You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
mail check: say WHY a message is re-delivered (lease expiry of unacked cur/ mail) + make the ack protocol discoverable #340
An agent that reads its inbox with tps mail check <agent> and never acks sees its entire cur/ re-delivered on every check ≥30 min after the previous one. checkMessages() (packages/cli/src/utils/mail.ts ~L87–97) walks cur/ after draining new/ and re-returns any message that is not read/nackedAt, not waiting on retryAfter, and whose checkout lease has expired (LEASE_TIMEOUT_MS = 30 min), re-stamping checkedOutAt.
That is correct at-least-once semantics — but nothing in the output says why a message is back, so it reads as a replay/duplication bug. It cost two hours chasing a phantom "mover" (relay, archiver, watchers were all suspected and excluded one by one) before the code path was found. The 2026-08-29 "60 accumulated in cur/, replayed as 66 unread" was the same thing.
Hook/plugin-driven agents never hit this because the hook acks. Any agent (or human) reading via mail check directly does.
Asks
Say why. When checkMessages re-delivers from cur/, mark it in the output — e.g. 📬 kern → flint … (re-delivered: lease expired, attempt 3, unacked since 2026-09-13T22:27Z) — and log a distinct event (redeliver) rather than nothing.
Make the protocol discoverable.tps mail check for an agent with no hook/plugin binding should print a one-line notice the first time it re-delivers: "N messages re-delivered because they were never acked; run tps mail ack <id> [agent] or tps mail check --ack."
Optionally: a --ack flag on mail check (ack on read) for non-hook consumers.
Evidence
LEASE_TIMEOUT_MS = 30 * 60 * 1000; isLeaseExpired; ackMessage sets read+ackedAt and removes the file from cur/.
Observed as strict 0/102 alternation on a 15-minute check cadence, zero re-deliveries on a 2-minute cadence, 106 after a 3-hour gap; a 15s trap on new/ never fired because the files never leave cur/.
Fixed on the consumer side by acking (99 acked → cur/ 0 → check returns 0). Refs dtrt-dev/ops ops-8mhg.
Two more gaps in the same protocol, found 2026-11-01 while restoring the ack step in a consumer-side drain script:
mail check never prints the id that mail ack takes. For a signed envelope, the JSON it prints is the inner envelope, and that envelope's messageId is a different uuid from the maildir record's id. Observed: mail send reported d64d9798…, mail check printed "messageId":"faf58947…", and only d64d9798… acks. A consumer that acks what it was shown acks nothing. Ask: print the record id on the 📬 header line (e.g. 📬 flint → flint <ts> id=<record id>), and have mail ack accept either id.
mail ack and mail nack exit 0 when the id matches nothing.commands/mail.ts (the ack and nack cases) prints Message already gone or not found: <id> with console.warn and returns 0, so a script acking the wrong id reports success while every message comes back after the lease. Ask: tell "already acked" (idempotent, exit 0) apart from "no such message" (non-zero, with the agent and the mailbox that was searched), so the caller can see which happened and fix the id.
Workaround in the meantime: match each printed header's timestamp to the cur/ file of the same name, ack that file's id, and count an ack only when the output says Acked <id>.
Activity
tps-flint commentedon Nov 1, 2026
Two more gaps in the same protocol, found 2026-11-01 while restoring the ack step in a consumer-side drain script:
mail checknever prints the id thatmail acktakes. For a signed envelope, the JSON it prints is the inner envelope, and that envelope'smessageIdis a different uuid from the maildir record'sid. Observed:mail sendreportedd64d9798…,mail checkprinted"messageId":"faf58947…", and onlyd64d9798…acks. A consumer that acks what it was shown acks nothing. Ask: print the record id on the📬header line (e.g.📬 flint → flint <ts> id=<record id>), and havemail ackaccept either id.mail ackandmail nackexit 0 when the id matches nothing.commands/mail.ts(theackandnackcases) printsMessage already gone or not found: <id>withconsole.warnand returns 0, so a script acking the wrong id reports success while every message comes back after the lease. Ask: tell "already acked" (idempotent, exit 0) apart from "no such message" (non-zero, with the agent and the mailbox that was searched), so the caller can see which happened and fix the id.Workaround in the meantime: match each printed header's timestamp to the
cur/file of the same name, ack that file'sid, and count an ack only when the output saysAcked <id>.