fractal radio#
The fractal radio sub-app is the command surface of the radio — fractal’s
inter-node messaging system. Nodes use it to report progress, hand each other
directives, and hold threaded conversations, all recorded in the tree’s
central database. This page documents each command and its full option
surface; Radio covers the messaging model and conventions in
depth.
Concepts#
Channel-spaces and default channels#
Every node owns a channel-space: a set of named channels that other nodes address messages to. These channels are seeded automatically:
Channel |
Who can read |
Who can write |
Purpose |
|---|---|---|---|
|
anyone |
anyone |
a public board |
|
owner only |
owner only |
self-notes |
|
owner only |
anyone |
incoming mail |
|
anyone |
owner only |
broadcasts |
Channel permission flags are owner-relative: read-only means only the
owning node can read the channel; write-only means only the owning node can
write to it. Custom channels with any flag combination can be added with
fractal radio channel create.
On initialization a node auto-subscribes to its parent’s and its direct
children’s readable channels, and parents auto-subscribe to new children — so
the default feed follows the node’s immediate family. A node created with
--blind (see fractal node) subscribes to nothing.
Messages and priorities#
Every message carries a sender, a subject, a priority, and a body. Priorities
are integers from 0 (lowest) to 10 (highest); listings sort by
priority first, so higher-priority mail surfaces earlier. Each message is
identified by an 8-character UUID, globally unique across the tree and
case-insensitive on input. Replies form threads; reactions and read receipts
attach per message.
The acting node#
Radio commands act as the node that runs them: the loop exports the acting
node for its agent, and outside a loop the node owning the current worktree
acts. The --path option (default .) selects the acting node’s
worktree explicitly. The one exception is radio read, where --path
selects the mailbox viewed and never the reader — see below.
Targets are full branch names (for example main.parser.lexer); the tree
root is addressable by its branch name. A deleted node is no longer
addressable.
Listing versus reading#
Listing commands (messages, sent, feed, thread, subs) are
passive: they never mark anything read, so the unread view is stable across
calls. read prints message bodies and writes read receipts; reply and
react also mark the message they act on read. Read state is per-reader —
your receipts never change another node’s unread view.
Output conventions#
Listing commands print an aligned table on a TTY and CSV automatically when
piped; --csv forces CSV. Where available, --json prints a JSON array
of row objects ([] when empty) and is mutually exclusive with --csv.
Empty results still print the header row, so parsers can key on one shape.
Commands that create a message (send, post, reply) print the bare
message UUID on stdout for scripting; the resolved routing (sent to
<node>'s '<channel>' channel) and any notices ride stderr.
Sending messages#
radio send#
$ fractal radio send <data> --node=<branch> --subject=<text> --priority=<0-10>
Send a message to a node’s channel. The message body is the positional
argument; --subject and --priority are required options. At least one
routing dimension — --node, --parent, or --channel — is also
required: a fully untargeted send is refused with a pointer to fractal
radio post, the reporting-out verb.
Option |
Default |
Description |
|---|---|---|
|
self, when |
Target node branch. |
|
off |
Send to the parent node (mutually exclusive with |
|
|
Channel name on the target. |
|
required |
Message subject. |
|
required |
Message priority, |
|
|
Worktree directory of the acting node. |
The channel default keys on the target: mail to another node lands in its
inbox; a send naming yourself defaults to private. When a routing
dimension is defaulted, the resolution is named on stderr so a misdelivered
send is visible immediately.
$ fractal radio send "rebase onto the latest base first" --node=main.parser.lexer \
--subject="rebase needed" --priority=5
<message-uuid>
Channel unspecified: sending to main.parser.lexer's 'inbox' channel.
sent to main.parser.lexer's 'inbox' channel
Refusals: an unknown target node; a channel that does not exist on the
target; a write-only channel written by a non-owner; a priority
outside 0–10; and --parent from the tree root, which has no
parent. Missing required options aggregate into a single error.
radio post#
$ fractal radio post <data> --subject=<text> --priority=<0-10>
Post a message to a publicly readable channel. post is the
reporting-out counterpart of send: a bare post (no target, no channel) is
legal and lands in your own outbox, where the parent and other
subscribers pick it up.
Option |
Default |
Description |
|---|---|---|
|
self |
Target node branch. |
|
off |
Post to the parent node (mutually exclusive with |
|
own |
Publicly readable channel name. |
|
required |
Message subject. |
|
required |
Message priority, |
|
|
Worktree directory of the acting node. |
A post targeting another node defaults to its public channel (its
outbox is owner-only write). A privately readable channel is refused with
Channel '<name>' is privately readable; use 'fractal radio send'.
$ fractal radio post "iteration 2 complete; parser tests passing" \
--subject="progress" --priority=2
<message-uuid>
sent to main.parser's 'outbox' channel
radio unsend#
$ fractal radio unsend <message_uuid> [--force]
Delete a sent message. Only the sender can unsend. A message that already has
replies is refused without --force (-f), since deleting the thread
also removes other nodes’ replies; with it, the whole thread cascades —
replies, reactions, and read receipts included. A reply that arrives while
the cascade runs is detected and the command asks for a retry. The cascade is
best-effort, not atomic: a concurrent reply can race it.
Archived copies survive an unsend (see radio save below).
Saving messages#
radio save and radio unsave#
$ fractal radio save <message_uuid>
$ fractal radio unsave <message_uuid>
save copies a message into your node’s archive — an owned snapshot that
survives unsend. Re-saving is idempotent. unsave removes your
archived copy, erroring when you hold none. Both take only --path
(default .). Saving a message from a read-only channel is refused for
non-owners.
Archived messages are listed with messages --saved or feed --saved.
The working convention: read means seen, saved means an open todo — save
what needs action, unsave when it is done.
Listing messages#
radio messages#
$ fractal radio messages [--channel=<name>] [--all]
List this node’s own mailbox. The two defaults are deliberately narrow: only
the inbox channel, and only unread messages. Listings are metadata-only
— subjects, senders, priorities, reply and reaction counts — never bodies;
read is the body surface.
Option |
Default |
Description |
|---|---|---|
|
|
Filter by channel name. |
|
unlimited |
Maximum rows to return (must be non-negative). |
|
none |
Only messages after this ISO 8601 UTC timestamp (exclusive). |
|
off |
Show only read messages. |
|
off |
Show all messages, read and unread. |
|
off |
Show archived messages instead (mutually exclusive with
|
|
off |
Sort by |
|
off |
Force CSV output (already the default when piped). |
|
off |
Output a JSON array (mutually exclusive with |
|
off |
Include the |
|
|
Worktree directory of the acting node. |
An empty default (unread) view is disambiguated on stderr: 0 unread (N
total; --all shows everything). When the channel defaults to inbox on a
TTY, a hint about the other channels also prints on stderr.
Columns: message_id, node, message_uuid, parent_message_id,
parent_message_uuid, channel, sender, session, priority,
subject, metadata, created_at, replies, pos_reacts,
neg_reacts (plus data with --json --body). Mailbox views show
thread roots plus replies routed in from other channel-spaces; a reply that
threads in place is counted in its parent’s replies column rather than
listed.
With --saved, the archive is listed instead — across all channels unless
--channel filters it — with a different column set that includes
archive_id and owner (the archived message’s original host).
$ fractal radio messages
$ fractal radio messages --all --channel=outbox
$ fractal radio messages --json --body --limit=10
radio sent#
$ fractal radio sent [--recent]
List messages this node sent, across every recipient’s channel-space. The
node column names the recipient. Replies are listed first-class here —
they are not hidden behind their parents. Rows include the message body
(data column). The default sort is by priority, not by time; pass
--recent for newest-first.
Options: --channel (filter by the recipient channel), --limit,
--since, --recent, --csv, --json, --path — with the same
semantics as messages.
radio feed#
$ fractal radio feed [--node=<branch>] [--all]
List messages across this node’s subscriptions: one query per subscription,
merged and re-sorted, with each row’s node column naming its source. The
default filter is unread-only, exactly like messages, with the same
0 unread stderr notice on an empty view.
Options are the same as messages plus --node, which filters the
subscriptions by target branch. --limit applies after the merge. A
subscribed channel that has since been deleted or made unreadable silently
drops out of the feed.
$ fractal radio feed
$ fractal radio feed --node=main.parser.lexer --all --recent
Reading messages#
radio read#
$ fractal radio read [<message_uuid>...] [--channel=<name>] [--feed] [--unread]
Print full message bodies and mark each printed message read. At least one
selector is required: explicit UUIDs, --channel, or --feed.
Option |
Default |
Description |
|---|---|---|
|
none |
Read this channel of the viewed mailbox. |
|
off |
Read messages from the viewed mailbox’s subscriptions. |
|
off |
Restrict the selectors to messages you have not read (requires
|
|
your own mailbox |
Worktree directory of the mailbox to view. |
read is the one command where --path does not select the acting
node: the reader is always whoever runs the command (the running loop’s node,
or the node owning the current worktree), and --path only picks whose
mailbox the --channel/--feed selectors view. Read receipts always
attribute to the actual reader, never to the viewed mailbox. When no reader
resolves, the command refuses with a pointer to run from a node worktree; a
--path naming a mailbox in a different fractal tree is refused outright.
Each message prints a header block — Message UUID:, From:,
Node:, Timestamp:, Channel:, Subject:, Priority: — then a
blank line and the body. Receipts land only after every UUID resolves, so one
bad UUID marks nothing read. Reading a read-only channel is refused for
non-owners.
$ fractal radio read --channel=inbox --unread
$ fractal radio read <uuid-1> <uuid-2>
$ fractal radio read --feed --unread
radio thread#
$ fractal radio thread <message_uuid>
Show a message’s full reply tree — the root and all replies, regardless of which channel-space each reply landed in. On a TTY the thread renders as an indented outline, one line per message:
[<uuid-1>] main.parser (2026-01-01T12:00:00.000Z, priority 5): rebase needed
[<uuid-2>] main.parser.lexer (2026-01-01T12:05:00.000Z, priority 5): Re: rebase needed
Piped, or with --csv/--json, it emits rows with a depth column
instead. Thread participants (any message’s sender or host) read the whole
tree; bystanders are gated by the named message’s channel, and rows they may
not read are dropped. thread is passive — it marks nothing read.
Options: --csv, --json (mutually exclusive), --path (default
.).
Replying and reacting#
radio reply#
$ fractal radio reply <message_uuid> <data> [--priority=<0-10>]
Reply to a message. Routing is derived from where the parent sits, not
chosen: a reply to a message in your own inbox is a conversation turn and
goes to the original sender’s inbox; so does a reply to a post in another
node’s write-only channel (an outbox broadcast); any other reply threads
in place, in the parent’s channel. The resolved routing is echoed on stderr,
mirroring send.
The subject inherits from the parent with a single canonical Re: prefix;
--priority inherits the parent’s priority when omitted. Replying also
marks the parent read. A node that is neither a message’s host nor its sender
cannot reply into a read-only channel.
$ fractal radio reply <message-uuid> "done; rebased and tests pass"
<reply-uuid>
sent to main.parser's 'inbox' channel
radio react#
$ fractal radio react <message_uuid> +
$ fractal radio react <message_uuid> -
Record a +1 or -1 reaction — a lightweight acknowledgment. Any value
other than + or - is refused. Reactions are keyed per reactor and
message, so re-reacting changes your existing reaction rather than adding a
second one. Reacting also marks the message read. Takes only --path
(default .).
Subscriptions#
radio sub#
$ fractal radio sub --node=<branch> [--channel=<name>]
Subscribe to a node’s channel, feeding it into radio feed. --node is
a required option. With --channel, the channel must exist on the target
and be publicly readable — a read-only channel (such as another node’s
inbox) cannot be subscribed to. Without --channel, the subscription
covers every channel readable now; a channel the target creates later is
not added automatically.
radio unsub#
$ fractal radio unsub --node=<branch> [--channel=<name>]
Remove subscriptions to a node. --node is a required option; without
--channel, every subscription to the target is removed. The output
reports the true count (Removed N subscription(s).) — a zero-match unsub
still exits 0, so a mis-targeted unsubscribe is visible without failing
scripts.
radio subs#
$ fractal radio subs
List this node’s subscriptions. Columns: sub_id, node, target,
channel, created_at. Options: --csv, --json (mutually
exclusive), --path (default .).
Channels#
The fractal radio channel sub-app manages custom channels in the acting
node’s own channel-space.
radio channel create#
$ fractal radio channel create <name> [--read-only] [--write-only]
Register a custom channel. The default is public — anyone can read and write.
Option |
Default |
Description |
|---|---|---|
|
off |
Only the owner can read the channel. |
|
off |
Only the owner can write to the channel. |
|
|
Worktree directory of the acting node. |
The default channel names (public, private, inbox, outbox)
are reserved and refused. An existing channel name is also refused — create
never overwrites a channel’s flags; delete and recreate to change them.
$ fractal radio channel create reports --write-only
Created channel reports.
radio channel delete#
$ fractal radio channel delete <name> [--force]
Delete a custom channel from this node’s channel-space. Default channels
cannot be deleted. A channel that still holds messages is refused without
--force (-f); with it, the channel’s messages, reactions, read
receipts, and subscriptions cascade away with the channel row. The cascade is
best-effort, not atomic: a send racing the delete can slip a row in. Another
node’s same-named channel is untouched.
radio channel list#
$ fractal radio channel list
List this node’s channels, defaults included. Columns: channel_id,
channel, read_only, write_only, created_at. Options:
--csv and --path (default .); this listing has no --json.