Inter-Agent Messaging
Send messages between running agents.
graith’s SQLite-backed messaging connects sessions, sessions and the user, and agents in a hierarchy.
Concepts
Streams are named message channels; every message belongs to one. Two types:
- Topic streams – created explicitly with
gr msg pub --topic <name>. Any session can publish or subscribe. - Inbox streams – named
inbox:<session-id>, created automatically per session. Used for direct messages withgr msg send.
Subscribers track read position per stream, identified by GRAITH_SESSION_ID; unread counts are per-subscriber.
Threads group related messages within a stream. Pass --thread <id> to pub/send to continue a thread, to sub to filter.
System streams are prefixed _system. for status and daemon notifications, hidden from gr msg topics unless --system is passed.
Publishing to topics
gr msg pub --topic review/auth-middleware/7f2a9 "Found a race condition in handler.go:245"
gr msg pub --topic ci/auth-middleware/7f2a9/result --file ./test-output.txt
gr msg pub --topic updates/daily/2026-07-29/summary --no-reply "Morning report is ready"
Any session can publish to any topic. The sender is auto-detected from GRAITH_SESSION_ID and GRAITH_SESSION_NAME. Outside a graith session, sender_name is empty and sender_id becomes pid:<pid>.
Topic naming
Use slash-delimited semantic paths for shared workflow topics. Put a stable, human-readable work scope near the front, put uniqueness in a dedicated run-id segment, and use short semantic leaves for the role of each stream:
ship/<work-scope>/<run-id>/tribunal/<judge>
tribunal/<work-scope>/<run-id>/judge/<agent>
pair/<work-scope>/<run-id>/chat
pair/<work-scope>/<run-id>/response/<agent>
review/<run-id>
Good scope segments are short slugs such as auth-middleware, ci-overlay, or
api-cache; good run IDs are $GRAITH_SESSION_ID, a short timestamp, or an
8-character random slug. Keep run IDs in their own segment instead of appending
timestamps or model names to the topic. Prefer semantic leaves such as chat,
ci, judge, response, stage, file, loop, and round/<n>.
Reserved stream families:
inbox:<session-id>is for direct messages and is managed by graith._system.is for daemon-authored system streams.- Shipped orchestration skills reserve roots such as
ship,tribunal,pair,review,research,chain,audit,security,advocate,impl, andred-blue.
Publisher and subscriber topic strings must match exactly. When writing prompts for child agents, expand topic placeholders to literal topic paths before launching the agent; agents should not receive template-looking topic names.
Direct messaging
gr msg send fix-auth-bug "the tests are green now, rebase on main"
gr msg send fix-auth-bug --file ./review-notes.md
gr msg send fix-auth-bug --quiet "silent context update"
gr msg send --no-reply --parent "Morning briefing complete"
send writes to the target’s inbox stream (inbox:<session-id>) and types a notification into its PTY. --quiet skips the notification; the message still reaches the inbox.
--no-reply marks a one-way message. It’s delivered and resumes a stopped
recipient normally, but recipient hints say No reply expected and suggest no
reply command. The choice is stored with the message, appears as
"no_reply": true in JSON, and works with topic publishes too.
--no-reply, --reply-to, and --quiet are independent: reply-expectation, a
route if someone does respond, and PTY suppression respectively. Daemon-authored
system notices have a separate automated identity and omit reply suggestions
without relying on no_reply.
Tree messaging
gr msg send --children "rebase on main and re-run tests"
gr msg send --parent "tests are green, ready for review"
--children sends to all descendants, --parent to the parent. Both auto-detect the current session from GRAITH_SESSION_ID.
Subscribing
# Read unread messages
gr msg sub --topic review/auth-middleware/7f2a9
# Read all messages (not just unread)
gr msg sub --topic review/auth-middleware/7f2a9 --all
# Read and acknowledge
gr msg sub --topic review/auth-middleware/7f2a9 --all --ack
# Block until a message arrives
gr msg sub --topic review/auth-middleware/7f2a9 --wait
# Stream continuously
gr msg sub --topic review/auth-middleware/7f2a9 --follow
# Filter to a specific thread
gr msg sub --topic review/auth-middleware/7f2a9 --thread abc123
# Read inbox
gr msg inbox --all --ack
Behavior
- Default: returns unread and exits; prints nothing if none.
--wait: blocks until a message arrives, then exits.--follow: streams messages as they arrive, indefinitely.--ack: marks all returned messages as read.--all: returns all messages, not just unread.
Acknowledging
gr msg ack --topic review/auth-middleware/7f2a9
Marks every message in the stream as read for the current subscriber.
Listing topics
gr msg topics
gr msg topics --system # include _system.* streams
Shows each stream with total and unread message counts.
Browsing messages while attached
While attached to a session, press ctrl+b m to open the message browser. It
opens on the latest direct message. Use j/k to move through older and newer
messages, G to jump back to the latest message, h/l to switch the selected
conversation or topic, t to browse available topics, and d to return to
direct messages. Topic names that contain / are shown as a tree; namespace
rows aggregate message counts, unread counts, and latest-message metadata, and
enter expands or collapses the selected namespace row.
The browser keeps rendering bounded to the terminal pane. If a refresh brings in
new messages while you are reading older history, the selected message stays in
place; press G when you want the latest view again. Topic leaf rows keep using
the full raw topic name when loading messages, and empty direct-message, topic,
and no-topic states are shown explicitly. Browsing a topic in the overlay is
read-only and does not mark
messages read; use gr msg sub --topic <name> --all --ack to acknowledge a
topic.
Threading
Threads structure conversations within a stream:
# Start a thread
gr msg pub --topic design/api-cache/7f2a9/chat "Proposal: new API endpoint for /users"
# Continue the thread (use the message ID from the first message as thread ID)
gr msg pub --topic design/api-cache/7f2a9/chat --thread msg_abc123 "I agree, but we should add pagination"
# Read only messages in a thread
gr msg sub --topic design/api-cache/7f2a9/chat --thread msg_abc123
# Set up a reply channel
gr msg send worker-1 "Please review this change" --reply-to review/auth-middleware/7f2a9/response/worker-1
# worker-1 can then publish results to the reply topic
Message format
In JSON output (--json or agent mode):
{
"id": "msg_abc123",
"seq": 1,
"stream": "review/auth-middleware/7f2a9",
"body": "Found a race condition in handler.go:245",
"sender_id": "session-uuid",
"sender_name": "fix-auth-bug",
"thread_id": "",
"reply_to": "",
"no_reply": true,
"created_at": "2026-06-17T10:30:00Z"
}
no_reply is omitted when false, preserving the default that an ordinary message is replyable.
From the GUI
The macOS and iOS apps send to and read a session’s inbox without the CLI:
- macOS — right-click a session in the sidebar and choose Messages….
- iOS — open a session and pick Messages from the toolbar menu.
The Messages view shows the direct-message conversation (received and sent), a compose field, and a mark-as-read action that acks the inbox. System notices (PR/CI notifications) are marked automated so they read distinctly from session/user messages.
Topic publish/subscribe (gr msg pub / gr msg sub) stays CLI-only for now.
Retention
Configure message retention in config.toml:
[messages]
max_age = "7d" # prune messages older than 7 days
max_per_stream = 1000 # keep at most 1000 messages per stream
Both are optional; unset keeps messages indefinitely. An empty max_age or an
explicit "0" is the “retain forever” sentinel. A non-empty value that doesn’t
parse, or a negative duration, is rejected at config load and reload — a typo
mustn’t silently disable cleanup and let messages and jailed comments grow unbounded.
Operational limits
The [messages] table also exposes the message log’s operational limits. Each
key is optional and falls back to the default shown; a value above its hard ceiling
is rejected at config load.
[messages]
conversation_page_size = 500 # page size when a conversation request omits a limit (default 500)
conversation_max_limit = 2000 # hard cap on messages a single conversation sorts (default 2000; ceiling 100000)
jail_list_limit = 2000 # max quarantined comments a jail listing returns (default 2000; ceiling 100000)
subscriber_buffer = 64 # per-subscriber pub/sub channel capacity (default 64; ceiling 65536)
busy_timeout = "5s" # SQLite busy/operation timeout for the messages DB ("" => 5s; explicit value must be 1ms–5m)
Lowering conversation_max_limit alone is always safe: the effective page size
clamps down to the new maximum. Only an explicit conversation_page_size larger
than conversation_max_limit is rejected at load, as a contradiction of intent.
The conversation paging bounds and jail cap are read per request, so changes take
effect on the next gr daemon reload. subscriber_buffer and busy_timeout are
fixed when the database opens, so they’re restart-only (gr daemon restart).
SQLite’s busy_timeout pragma has millisecond resolution, so a positive value
below 1ms is rejected at load; otherwise it would collapse to busy_timeout(0)
and disable lock waiting entirely.
Note. Only
subscriber_bufferis exposed here (bursty fan-out is a real load-tuning case). Other internal channel capacities (daemon control kicks, signal-request buffers) stay code-owned and aren’t configurable.
Patterns
See Agent-to-agent communication for messaging patterns: pub/sub broadcast, request/reply, coordination barriers, and hierarchical communication.