Files
stack/packages/mosaic/framework/tools/tmux
veronica dc6db5f14e
ci/woodpecker/pr/ci Pipeline failed
fix(tmux): locate the REPL input box by shape, not by a Claude-only glyph
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
2026-08-21 16:54:39 -05:00
..

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 -s of 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 (-b base64 input).
  • auto-submit-drafts.sh — watchdog that flushes stable unsubmitted prompt drafts on a coordinator pane (default target mos-claude); run it as a long-lived process alongside the coordinator session.
  • agent-send.test.sh — regression + grammar lock for agent-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.