feat(bus): mail-model inbox — unread/read, keep-unread wait, reply-to, 500-char cap - #96
Merged
Merged
Conversation
anshulsao
previously approved these changes
Sep 7, 2026
anshulsao
left a comment
Member
There was a problem hiding this comment.
LGTM. Build/vet/tests pass locally; the retention-predicate refactor (shared busRollable) and the widened atomic ack guard are clean.
Three low-severity non-blockers noted (fix at leisure):
busSnippetslices the body by byte offset — can split a multi-byte rune now that the cap is 500 chars; use runes.- PreToolUse matcher is an allowlist (
Bash|Edit|Write|...), but the docstrings say "every tool" — MCP/TodoWrite calls won't surface a mid-turn stop; align docs or widen matcher. - Dropping
migrateBusKindBroadcastleaves legacykind='post'rows stranded (immortal under the new retention predicate) on very old DBs.
- flow inbox pop is the ONLY consumption API: ack and due subcommands removed, along with the whole escalation apparatus (next_notify_at / attempts columns for fresh installs, DueBusMessages, BumpNotifyAttempt, AckMessageByID). Popping answers, delivers, and clears; agents and notifier scripts just loop it. Ack-on-reply (the UserPromptSubmit hook) and wait metrics remain — that is behavior, not API surface. - No sender can message its own address: a bound session is rejected addressing its own inbox, and the human their own queue. - Reserved assignee renamed self -> user (agents misread 'message self' as talking to themselves). Existing rows migrated idempotently on open; the one-shot post->broadcast kind migration is deleted (no released DB ever needed it). - Usage, skill §4.18, and references/messaging.md rewritten to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFGNJQo5xNBBrT7rZMuUTu
…ssage --reply-to Additive, non-breaking additions on top of the pop-only bus: - `flow inbox --all` lists the whole retained queue (read + unread); default `flow inbox` still lists only unread. - `flow inbox read <id>` shows any message by id (incl. consumed) and marks it read — acks a specific message out of pop's oldest-first order. - `flow inbox pop --wait --keep-unread` is the reader/relay wake: returns the oldest unread, claims it `delivered` (loop-safe) but never acks, so a forwarder can pass mail along without consuming the human's answer. - `flow message --reply-to <id>` stamps a parent id (new nullable `reply_to` column, migrated on open); the receiver sees the lineage. - Mail nomenclature (unread/read) in human output and `--json` (`mail`). - Retention: a delivered-but-unacked human message is immortal like pending — an unanswered question never vanishes, whoever forwarded it. Skill §4.18 + references/messaging.md rewritten: mail nomenclature, two-purpose framing (reach-out-to-user / peer-collab), two Monitor recipes (consumer pop-wait, reader pop-wait --keep-unread). Vanishing-message bug ruled out as durability: the ids were auto-acked by the sending session's UserPromptSubmit hook (acked_by='prompt') before the relay forwarded them, not lost from storage. `--keep-unread` closes the delivery-starvation gap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bump busBodyMax 200 -> 500 (non-breaking; short messages unaffected). The 200 cap forced constant splitting for the Telegram relay. - Skill (SKILL.md §4.18 + references/messaging.md): document that headless `flow do --auto` agents should post regular progress via `flow message user`/`flow broadcast`, not only when blocked — a silent run looks stuck. Update the two "≤200 chars" citations to "≤500". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface a just-arrived DIRECTED inbox message right before a tool runs, so a mid-turn "stop, don't do X" reaches the agent before the action it would cancel — the case SessionStart/UserPromptSubmit/Stop nudges surface too late. The existing pending-count notice is NOT delta-gated (raw COUNT(*) of pending rows), so wiring it as-is into PreToolUse — which fires on every tool call — would re-nudge the same set every call. Instead add a bus_surfaced high-water mark: PendingDirectedUnsurfacedForTask returns only messages not yet announced, MarkSurfaced records them, so each message is surfaced at most once and quiet tool calls stay silent. Broadcasts are excluded (FYIs, not action-changing); urgent messages lead. Inform-only — hooks never consume. Wires `flow hook pre-tool-use` into settings.json via a new Install/UninstallPreToolUseHook on the harness interface (claude installs; codex no-ops), and on skill install/uninstall/auto-upgrade. bus_surfaced marks are cleaned on task close-out and pruned in SweepBus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PreToolUse delta-gated nudge previously surfaced only directed messages, excluding broadcasts as FYIs. Per refinement, it now fires on ANY unread item (message OR broadcast) addressed to the session: rename PendingDirectedUnsurfacedForTask -> PendingUnsurfacedForTask and drop the kind='message' filter. Delta-gating (bus_surfaced high-water mark), urgent lead, and inform-only (never consumes) are unchanged, so each new unread still surfaces exactly once with no per-call spam. Test flipped from "ignores broadcasts" to "surfaces broadcasts" (once, then delta-gated); wording + docs (messaging.md) updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port Anshul's #95 flag-parsing correction into this branch's docs: the mail-model rewrite of §4.18 had dropped it. references/messaging.md now states the tolerant command form (body positional or --body; --urgent/ --body/--reply-to may come in any order; unknown flags rejected, never stored as the body), and the app.go --help note lists --reply-to as a known flag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
rr0hit
force-pushed
the
flow-mail-model
branch
from
September 7, 2026 06:03
b97a77e to
4e0822b
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.
What
Mail-model enhancements to the
flowmessage bus. All changes are additive and non-breaking —flow inbox popand every existing verb/flag keep their exact current behavior. Bundles the pop-only API groundwork with the new inbox surface.Message bus (code)
flow inbox --all— list everything still retained (read + unread), each with id + status, not just unread.flow inbox read <id>— show/ack a specific message out ofpop's oldest-first order (works for already-consumed messages too — displays without re-mutating).flow inbox pop --wait --keep-unread— reader/relay wake primitive: returns the message but does not ack it (claims the rowdeliveredso a loop won't re-return it, but leaves a human-directed message unanswered/immortal so a forwarder can pass it along). Plainpop --waitis unchanged.flow message <addr> "..." --reply-to <id>— stamp a parent id so a routed reply carries lineage; the receiver sees↳ in reply to [id]: <snippet>. Validated at send time.--json(mailfield). Newreply_tocolumn migrated idempotently on open; sweep retention widened so an unacked human message is immortal like pending.Skill
references/messaging.md+SKILL.md§4.18 rewritten: mail nomenclature, two Monitor recipes (consumerpop --waitvs reader/relaypop --wait --keep-unread), two-purpose framing (reach-out-to-user / peer-collab), niche/escalate content trimmed.flow do --autoagents should post regular progress viaflow message user/flow broadcast, not only when blocked.Vanishing-message bug — investigated, RESOLVED (not a durability bug)
Both reported ids survived in the DB as
acked. Root cause: the sending session's ownUserPromptSubmithook auto-acks its outbound human-directed messages and raced the out-of-band Telegram relay. The--keep-unreadreader primitive is the fix — the relay claims the rowdelivered(which the pending-only prompt-hook ack can no longer touch) and acks it by id only after the human answers.Verification
make testgreen. All six enhancements exercised end-to-end in an isolated sandbox (22/22 assertions pass):inbox --all,read <id>(out-of-order ack + already-read),pop --keep-unread(wakes without acking, loop-safe, contrasted against ackingpop),--reply-to(lineage shown + invalid-id rejected), 500-char cap (500 accepted / 501 rejected / 300 accepted), and mail nomenclature.🤖 Generated with Claude Code