send-message.sh confirmed delivery by grepping the captured pane for a prompt glyph (`❯`, a leading `>`, or `│ >`). Those are Claude Code shapes. pi draws its input box as two horizontal `─` rules with the input between them and no glyph anywhere, so on a pi pane the grep matched nothing, the `delivered` and `draft` branches were unreachable, and every send exited 2 with "may be UNDELIVERED" after burning all its retries — on messages that had in fact been delivered. Measured on five live seats: pi seats vision, tess and medic returned 0 glyph matches on non-empty captures (3653 / 1086 / 1890 bytes); Claude seats fred and tuesday returned 3 and 1. This is a different arm from #1257. On the measured host `tmux capture-pane -t <session> -p` and `-t <session>:0.0 -p` both return the same 3653 bytes, so #1257's empty-capture mechanism does not reproduce here; fixing only that would leave every pi send at rc=2 and would verify green against a Claude pane. The probe becomes locate_input_box(), which returns a status code instead of a string. Found-but-empty is a real answer (an empty input box is what a submitted message leaves behind) and is now distinguishable from not-found, which the old `[ -z "$promptline" ]` test conflated. Box detection anchors on the LAST pair of `─` rules: agent output can contain its own rules, but nothing is drawn below the input box except the status line. Adding a runtime means adding its shape in that one function. A missing shape does not degrade gracefully — it turns every send to that runtime into a false "may be UNDELIVERED", which is exactly what this fixes. Tests: two fixtures added to test-send-message-verdict.sh driving a glyphless box-drawn REPL, one that submits (expect exit 0 delivered) and one that holds the text in the box (expect exit 2 draft). Patched suite PASS=5 FAIL=0. Control against the pristine origin/next send-message.sh: PASS=3 FAIL=2, fixture 4 failing as `rc=2 ... REPL input prompt not locatable after 3 attempts — message may be UNDELIVERED`, which is the defect reproduced in a test. Sibling suites unchanged and green: test-send-message-socket.sh rc=0, agent-send.test.sh PASS=19 FAIL=0. Note the suite is in test-enumeration-exclusions.txt (the CI image ships no tmux), so these fixtures are verified locally only. That exclusion and its #1017 burndown reason are unchanged by this commit. Not addressed here: auto-submit-drafts.sh lines 20 and 26 carry the same Claude-only `❯` assumption. Separate surface, separate change. Closes #1362
Inter-Agent tmux Comms — Standard & Tooling
Reliable, self-identifying messaging between Mosaic agents running in tmux panes (Claude Code / Codex / OpenCode REPLs), across hosts.
The addressing standard (required)
Every cross-agent tmux message MUST begin with an addressing preamble:
[<src_host>:<src_session> -> <dst_host>:<dst_session>] <message>
host=hostname -sof the machine the agent runs on (e.g.web1,sb-it-mgr-0-lt).session= the tmux session name (e.g.mos-claude,rev0-4,installer-1).- Replies FLIP the preamble: the recipient answers with
[<dst> -> <src>] ....
Why: a fresh or context-wiped agent always knows who sent a message and to whom. No ambiguity about origin or lane after a tmux wipe / session restart.
Example exchange:
[web1:mos-claude -> sb-it-mgr-0-lt:installer-1] status on #29?
[sb-it-mgr-0-lt:installer-1 -> web1:mos-claude] Q2 done, opening PR #34.
The helper: agent-send.sh
Prepends the preamble automatically (auto-detecting your own host:session) and
delivers reliably to local OR remote panes.
# Local target (same host, default tmux server)
agent-send.sh -s <dst_session> -m "message"
# Local target on a Mosaic fleet socket
agent-send.sh -L mosaic-fleet -s '=coder0' -m "message"
# Remote target (over ssh)
agent-send.sh -H user@host -s <dst_session> -m "message"
# From a file / stdin
agent-send.sh -H user@host -s <dst_session> -f msg.txt
echo "msg" | agent-send.sh -s <dst_session>
Key flags: -L named tmux socket · -s dst session (required) · -H ssh target for remote · -n dst
hostname for the preamble (else auto-resolved) · -m/-f/stdin body · -S
override source label · -v verbose · -r N Enter-flush attempts.
For durable fleet use, prefer exact tmux targets such as =coder0. The helper
normalizes exact session targets to pane-qualified targets internally so pane
commands do not fall back to tmux's prefix matching behavior.
Named socket isolation
Durable Mosaic fleets should use a dedicated tmux socket, for example:
tmux -L mosaic-fleet ls
agent-send.sh -L mosaic-fleet -s '=coder0' -m "status?"
send-message.sh -L mosaic-fleet -t '=coder0' -m "raw pane message"
This keeps fleet operations away from the user's default tmux server. It is the safe rollout path on hosts that already have manual tmux sessions.
Why a helper exists (the submission gotcha)
Pasting into an interactive REPL via raw tmux send-keys is unreliable: a
trailing Enter is frequently swallowed and the message sits as an unsubmitted
draft ("Press up to edit queued messages"). Over an ssh -> nested tmux hop the
plain Enter keyname often does not register at all — C-m is needed.
send-message.sh solves this for a local pane: bracketed-paste the body
(so multi-line content doesn't submit early), pause, then send Enter as its own
keystroke and flush with a second, verifying against a draft heuristic.
agent-send.sh solves the remote case by shipping send-message.sh over ssh
(ssh host bash -s -- ... < send-message.sh) and running it local to the target
pane — so the reliable send-keys always happens on the pane's own host. The remote
needs only bash + tmux + base64; no mosaic install required there. The
message crosses the wire as base64 (-b) to avoid all shell-quoting hazards.
Files
agent-send.sh— inter-agent wrapper (preamble + local/remote dispatch).send-message.sh— low-level reliable single-pane submitter (-bbase64 input).auto-submit-drafts.sh— watchdog that flushes stable unsubmitted prompt drafts on a coordinator pane (default targetmos-claude); run it as a long-lived process alongside the coordinator session.agent-send.test.sh— regression + grammar lock foragent-send.sh.test-send-message-socket.sh— smoke test for named-socket isolation.
Distribution
These live in the installed framework copy at
~/.config/mosaic/tools/tmux/. install.sh rsyncs the framework source tree
to each host, so to propagate permanently, land both files in the framework
source repo and re-run the installer on each host. Until then, agent-send.sh
already works against any reachable host because it ships send-message.sh over
ssh per-send — no pre-install on the target host is needed to send to it.