fix(tmux): explicit transport-only dispatch and safe remote quoting (#1496)

This commit is contained in:
2026-09-08 12:41:25 -05:00
parent 67eaf6fb47
commit 69f10a4062
71 changed files with 6564 additions and 316 deletions
@@ -0,0 +1,118 @@
# 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.
```bash
# 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:
```bash
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. It does not send automatic extra Enters. The legacy `-r` option
is accepted for compatibility but no longer authorizes flushes.
Exit 0 requires newly visible whole-message evidence relative to a pre-paste
capture and a cleared supported editor region. This is a visual heuristic,
not recipient acknowledgement or proof of processing. Static/repeated history,
a queued banner alone, unsupported editor/footer layouts, failed captures,
and ambiguous observations return unconfirmed (exit 2). Do not blindly resend
an unconfirmed message: it may already have been accepted.
Pi rule matching groups UTF-8 literals so C and UTF-8 locales behave consistently.
The supported rule-pair shape requires a path/branch footer immediately below it
and at most four nonblank footer lines. Prompt-only layouts require a final
nonblank prompt line. These narrow shapes can refuse legitimate custom layouts;
visual resemblance is not an authenticated editor boundary. Ten seconds is a
chosen observation budget, not a guaranteed response/redraw time.
`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
`~/.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.
@@ -0,0 +1,233 @@
#!/usr/bin/env bash
# agent-send.sh — standard inter-agent tmux messaging for the Mosaic stack.
#
# WHAT IT DOES
# Sends a message to another agent's tmux pane (local or on a remote host)
# with the canonical addressing preamble prepended:
#
# [<src_host>:<src_session> -> <dst_host>:<dst_session>] <message>
#
# The preamble makes every inter-agent message self-identifying, so a fresh
# or context-wiped agent always knows who sent a message and to whom — no
# ambiguity about lanes or origin. Recipients replying should FLIP the
# preamble: [<dst> -> <src>] ... (this tool sends; it does not auto-reply).
#
# Optionally tags the message with a TRIAGE CLASS (see -C / --class) so a
# comms daemon can route it (deliver-to-agent vs log-and-drop) from an exact
# field instead of re-deriving intent from the body.
#
# WHY A WRAPPER
# Reliable submission into an interactive REPL (Claude Code / Codex) is fiddly:
# a trailing Enter is often swallowed and the message sits as an unsubmitted
# DRAFT. tools/tmux/send-message.sh already solves that for a LOCAL pane via
# bracketed-paste + Enter-flush + draft-detection. For REMOTE targets this
# wrapper SHIPS send-message.sh over ssh (stdin) and runs it there, so the
# reliable send-keys happens local to the target pane — sidestepping the
# ssh->nested-tmux Enter/C-m swallow entirely. No mosaic install needed on
# the remote host; only bash + tmux + base64 (standard).
#
# USAGE
# agent-send.sh [-L socket] -s <dst_session> -m "message" # local target
# agent-send.sh [-L socket] -H user@host -s <dst_session> -m "message" # remote target
# agent-send.sh [-L socket] -H user@host -n <dst_hostname> -s <sess> -f msg.txt
# agent-send.sh -s mos-claude --class terminal-log -m "ACK — received"
# echo "msg" | agent-send.sh [-L socket] -H user@host -s <dst_session>
#
# OPTIONS
# -L NAME tmux socket name passed to `tmux -L NAME` on the target host
#
# Exit 4: local target session exists on multiple socket servers and no
# -L / MOSAIC_TMUX_SOCKET disambiguated it (B1 stale-twin guard).
# -s DST_SESSION target tmux session (or session:window.pane) [required]
# -H SSH_TARGET ssh target (user@host) for a remote pane; omit for local
# -n DST_HOST hostname to show in the preamble for the target.
# Default: local hostname, or (remote) resolved via one ssh.
# -m MESSAGE message text (single- or multi-line)
# -f FILE read message from FILE instead of -m
# -C CLASS triage class for a comms daemon. One of:
# terminal-log log-only; never needs the agent's attention
# actionable carries a decision/blocker/gate — deliver
# human from a human operator — deliver
# reaction an emoji/ack reaction
# digest machine-wake, coalescible; batched wake/heartbeat signal
# Long form: --class CLASS (or --class=CLASS). When SET, the
# preamble carries a ` class=<CLASS>` token INSIDE the bracket:
# [<src> -> <dst> class=terminal-log] <message>
# When OMITTED, NO token is emitted and the preamble is
# byte-for-byte identical to the classic format. Consumers MUST
# treat an absent class as 'actionable' (fail-safe: agent sees it).
# -S SRC_LABEL override source label "<host>:<session>" (default: auto)
# -r N Legacy compatibility option; no automatic extra Enter
# -v verbose: print pane tail after delivery
# -h help
#
# PREAMBLE GRAMMAR (for consumers / daemons mirroring this producer)
# ^\[(\S+) -> (\S+?)(?: class=(terminal-log|actionable|human|reaction|digest))?\] (.*)$
# group 1 = src label group 2 = dst host:session
# group 3 = class (absent => actionable) group 4 = message body
#
# EXIT CODES (passed through from send-message.sh, except 4)
# 0 observed correlated editor transition (not ACK) · 1 target not found
# 2 unconfirmed or draft · 3 usage error
# 4 agent-send refusal: local target session exists on multiple socket
# servers and no -L / MOSAIC_TMUX_SOCKET disambiguated it (B1)
set -uo pipefail
SELF_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
# Sender is overridable via env purely for testing (inject a capture stub). The
# default is the canonical send-message.sh beside this script; production callers
# never set AGENT_SEND_SENDER, so behavior is unchanged.
SENDER="${AGENT_SEND_SENDER:-$SELF_DIR/send-message.sh}"
# Translate the long option --class[=value] into "-C value" so getopts (which is
# short-option-only) can parse it. Every other argument passes through untouched,
# so callers that never use --class hit the exact original getopts path.
args=()
while [ $# -gt 0 ]; do
case "$1" in
--class) [ $# -ge 2 ] || { echo "ERROR: --class requires a value" >&2; exit 3; }
args+=(-C "$2"); shift 2 ;;
--class=*) args+=(-C "${1#*=}"); shift ;;
*) args+=("$1"); shift ;;
esac
done
set -- ${args[@]+"${args[@]}"}
DST_SESSION=""; SSH_TARGET=""; DST_HOST=""; MSG=""; FILE=""; SOCKET_NAME=""
SRC_LABEL=""; RETRIES=2; VERBOSE=0; CLASS=""
usage() { sed -n '2,/^set -uo pipefail/{/^set -uo pipefail/d;p}' "$0"; exit "${1:-3}"; }
while getopts "L:s:H:n:m:f:S:r:C:vh" o; do
case "$o" in
L) SOCKET_NAME=$OPTARG ;;
s) DST_SESSION=$OPTARG ;; H) SSH_TARGET=$OPTARG ;; n) DST_HOST=$OPTARG ;;
m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; S) SRC_LABEL=$OPTARG ;;
C) CLASS=$OPTARG ;;
r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
esac
done
[ -n "$DST_SESSION" ] || { echo "ERROR: -s DST_SESSION is required" >&2; usage 3; }
[ -x "$SENDER" ] || { echo "ERROR: send-message.sh not found beside this script" >&2; exit 3; }
# Validate the triage class only when one was given. An absent class emits NO
# token (preamble byte-identical to the classic format); the consumer defaults
# absent => actionable.
CLASS_TOKEN=""
if [ -n "$CLASS" ]; then
case "$CLASS" in
terminal-log|actionable|human|reaction|digest) CLASS_TOKEN=" class=${CLASS}" ;;
*) echo "ERROR: invalid --class '$CLASS' (allowed: terminal-log, actionable, human, reaction, digest)" >&2; exit 3 ;;
esac
fi
# Message body from -f / -m / stdin.
if [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE")
elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat)
fi
[ -n "$MSG" ] || { echo "ERROR: empty message (use -m, -f, or stdin)" >&2; exit 3; }
# Source label: this agent's host:session (auto-detected, overridable).
if [ -z "$SRC_LABEL" ]; then
src_host=$(hostname -s 2>/dev/null || echo "?")
src_sess=${MOSAIC_AGENT_NAME:-}
if [ -z "$src_sess" ]; then
if [ -n "${TMUX:-}" ]; then
# Inside tmux: display-message resolves against this client's own session.
src_sess=$(tmux display-message -p '#S' 2>/dev/null || echo "?")
else
# Outside tmux with no name: display-message reports the LAST-ACTIVE
# session — someone else's identity (measured 2026-08-20: a nameless
# non-tmux sender was stamped "peggy", a live seat, forged silently).
# Stamp an explicit unverified label instead; deliberate senders use -S.
src_sess="unverified"
fi
fi
SRC_LABEL="${src_host}:${src_sess}"
fi
# Destination host label for the preamble.
if [ -z "$DST_HOST" ]; then
if [ -n "$SSH_TARGET" ]; then
DST_HOST=$(ssh -o ConnectTimeout=8 -o BatchMode=yes "$SSH_TARGET" 'hostname -s' 2>/dev/null || echo "${SSH_TARGET#*@}")
else
DST_HOST=$(hostname -s 2>/dev/null || echo "local")
fi
fi
PREAMBLE="[${SRC_LABEL} -> ${DST_HOST}:${DST_SESSION}${CLASS_TOKEN}]"
FULL="${PREAMBLE} ${MSG}"
B64=$(printf '%s' "$FULL" | base64 -w0)
vflag=""; [ "$VERBOSE" = 1 ] && vflag="-v"
# Exact session matching for the sender target (codex PR #1466): without
# '=', tmux target syntax accepts an unambiguous PREFIX, so a delivery
# aimed at session X can land in X-old. Compound targets (session:win.pane)
# and already-exact ('=...') forms pass through untouched. Computed BEFORE
# socket discovery so the discovery probes use the same target semantics
# (probing '==name' for an already-exact input was a false-negative hit).
DST_TARGET="$DST_SESSION"
case "$DST_SESSION" in
=*) ;;
*:*)
# Compound target (session:win.pane): pin the SESSION component exact
# (=session:win.pane); unpinned, the session part still prefix-matches
# (codex PR #1466: 'agent:0.0' can resolve into 'agent-old').
DST_TARGET="=${DST_SESSION%%:*}:${DST_SESSION#*:}"
;;
*) DST_TARGET="=$DST_SESSION" ;;
esac
# Socket default resolution (B1, 2026-08-29). Precedence: explicit -L >
# launcher-exported MOSAIC_TMUX_SOCKET > unique socket hit > refusal on
# ambiguity > tmux default socket. The ambiguity refusal fires ONLY when
# no explicit or env choice exists and the session name lives on multiple
# servers (measured 2026-08-28/29: tasking sends landed in a stale
# default-socket twin; rc 0 reported honest delivery to the wrong pane).
# Socket discovery scans tmux's own socket dir, ${TMUX_TMPDIR:-/tmp}/tmux-UID
# (codex PR #1466: TMPDIR is not where tmux keeps -L sockets).
# MOSAIC_TMUX_SOCKET is LOCAL-host state (launcher-exported): it must not
# leak into remote sends, where -L would target a socket on the remote
# host (codex PR #1466).
if [ -z "$SOCKET_NAME" ] && [ -z "$SSH_TARGET" ] && [ -n "${MOSAIC_TMUX_SOCKET:-}" ]; then
SOCKET_NAME="$MOSAIC_TMUX_SOCKET"
fi
if [ -z "$SOCKET_NAME" ] && [ -z "$SSH_TARGET" ]; then
socket_dir="${TMUX_TMPDIR:-/tmp}/tmux-$(id -u)"
hits=""
for sf in "$socket_dir"/*; do
[ -S "$sf" ] || continue
sname="${sf##*/}"
# '=' forces exact session-name matching: tmux target syntax otherwise
# accepts an unambiguous PREFIX, so a session named X-old on a socket
# would count as a false hit for target X (codex PR #1466).
# Silence BOTH streams: has-session writes nothing to stdout, but a stub
# (test fake) may — leaked probe stdout polluted this tool's stdout and
# broke callers that read it (measured 2026-09-07, agent-send.test #9b).
tmux -L "$sname" has-session -t "$DST_TARGET" >/dev/null 2>&1 && hits="$hits$sname"$'\n'
done
hit_count=$(printf '%s' "$hits" | grep -c . || true)
if [ "$hit_count" -gt 1 ]; then
echo "agent-send.sh: REFUSING - session '$DST_SESSION' exists on multiple sockets:" >&2
printf ' %s\n' $hits >&2
echo " Pass -L <socket> explicitly (or export MOSAIC_TMUX_SOCKET to disambiguate)." >&2
exit 4
elif [ "$hit_count" -eq 1 ]; then
SOCKET_NAME="$(printf '%s' "$hits")"
fi
fi
socket_args=()
if [ -n "$SOCKET_NAME" ]; then
socket_args=(-L "$SOCKET_NAME")
fi
if [ -z "$SSH_TARGET" ]; then
# Local pane: call the canonical sender directly.
exec "$SENDER" "${socket_args[@]}" -t "$DST_TARGET" -b "$B64" -r "$RETRIES" $vflag
else
# Remote pane: ship the sender over ssh and run it local to the target.
ssh -o ConnectTimeout=10 "$SSH_TARGET" \
"bash -s -- ${socket_args[*]@Q} -t '$DST_TARGET' -b '$B64' -r '$RETRIES' $vflag" < "$SENDER"
fi
@@ -0,0 +1,188 @@
#!/usr/bin/env bash
# agent-send.test.sh — regression + grammar lock for agent-send.sh --class.
#
# Strategy: inject a capture stub via AGENT_SEND_SENDER that decodes the -b
# base64 payload and prints the FULL message (preamble + body) so we can assert
# the exact bytes on the wire. Local path only (no ssh), -n pins the dst host so
# the preamble is deterministic across machines.
#
# Guarantees locked here:
# 1. REGRESSION BAR — no --class => preamble byte-for-byte identical to classic.
# 2. --class <c> => ` class=<c>` token emitted inside the bracket.
# 3. --class=<c> (equals form) parses identically to the space form.
# 4. -C <c> short form parses identically.
# 5. invalid class => exit 3, nothing sent.
# 6. --class with no value => exit 3.
# 7. the documented consumer regex parses producer output for every class.
# 8. MOSAIC_AGENT_NAME is authoritative for sender identity.
# 9. sender fallback queries local tmux, never the destination -L socket.
# 10. an undeterminable sender is stamped as "?".
# 11. --class digest is accepted (machine-wake, coalescible canon class).
# 12. -C digest short form parses identically.
# 13. the documented consumer regex parses producer output for class=digest.
set -uo pipefail
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
TOOL="$HERE/agent-send.sh"
# Capture stub: stands in for send-message.sh. Decodes -b and prints the payload.
STUB=$(mktemp)
FAKE_BIN=$(mktemp -d)
trap 'rm -f "$STUB"; rm -rf "$FAKE_BIN" "$SCRATCH_TMPDIR"' EXIT
cat >"$STUB" <<'STUB_EOF'
#!/usr/bin/env bash
set -uo pipefail
b64=""
while getopts "L:t:b:r:v" o; do case "$o" in b) b64=$OPTARG ;; *) : ;; esac; done
printf '%s' "$b64" | base64 -d
STUB_EOF
chmod +x "$STUB"
# Fake tmux distinguishes the sender's default socket from a destination socket.
cat >"$FAKE_BIN/tmux" <<'TMUX_EOF'
#!/usr/bin/env bash
set -uo pipefail
case "${FAKE_TMUX_MODE:-sessions}" in
unavailable) exit 1 ;;
sessions)
if [ "${1:-}" = "-L" ]; then
printf '%s\n' 'destination-holder'
else
printf '%s\n' 'local-agent'
fi
;;
esac
TMUX_EOF
chmod +x "$FAKE_BIN/tmux"
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); printf 'ok %s\n' "$1"; }
no() { FAIL=$((FAIL+1)); printf 'FAIL %s\n %s\n' "$1" "$2"; }
# Run the tool with the stub injected; echoes captured payload on stdout.
run() { AGENT_SEND_SENDER="$STUB" bash "$TOOL" -S a:src -n dsthost "$@"; }
# Hermetic auto-label runs: TMUX is controlled explicitly so results never
# depend on whether the caller running this suite sits inside tmux — and
# TMUX_TMPDIR is pinned to an empty scratch dir so socket discovery never
# sees the HOST's sockets (measured 2026-09-07: with default+mosaic-fleet
# live, discovery saw the fake answer 'mos' on both and B1-refused rc 4
# before the stub ever ran; a one-socket host passed, so this only bites
# multi-socket hosts).
SCRATCH_TMPDIR=$(mktemp -d)
run_auto() { # models a sender OUTSIDE tmux (no client context)
env -u MOSAIC_AGENT_NAME -u TMUX TMUX_TMPDIR="$SCRATCH_TMPDIR" \
AGENT_SEND_SENDER="$STUB" PATH="$FAKE_BIN:$PATH" \
bash "$TOOL" -n dsthost "$@"
}
run_auto_in_tmux() { # models a sender INSIDE tmux (client context exists)
env -u MOSAIC_AGENT_NAME TMUX=/fake/socket \
AGENT_SEND_SENDER="$STUB" PATH="$FAKE_BIN:$PATH" \
bash "$TOOL" -n dsthost "$@"
}
# Documented consumer grammar — the daemon will mirror exactly this.
GRAMMAR='^\[(\S+) -> (\S+) class=(terminal-log|actionable|human|reaction|digest)\] (.*)$'
GRAMMAR_NOCLASS='^\[(\S+) -> (\S+)\] (.*)$'
# 1. REGRESSION BAR: classic preamble, byte-for-byte.
got=$(run -s mos -m "hello world")
want='[a:src -> dsthost:mos] hello world'
[ "$got" = "$want" ] && ok "regression: no --class is byte-identical" \
|| no "regression: no --class is byte-identical" "got=[$got] want=[$want]"
# 2. --class space form emits the token.
got=$(run -s mos --class terminal-log -m "ACK")
want='[a:src -> dsthost:mos class=terminal-log] ACK'
[ "$got" = "$want" ] && ok "--class terminal-log emits token" \
|| no "--class terminal-log emits token" "got=[$got] want=[$want]"
# 3. --class=value equals form.
got=$(run -s mos --class=actionable -m "decide X")
want='[a:src -> dsthost:mos class=actionable] decide X'
[ "$got" = "$want" ] && ok "--class=actionable (equals form)" \
|| no "--class=actionable (equals form)" "got=[$got] want=[$want]"
# 4. -C short form.
got=$(run -s mos -C human -m "from a person")
want='[a:src -> dsthost:mos class=human] from a person'
[ "$got" = "$want" ] && ok "-C human (short form)" \
|| no "-C human (short form)" "got=[$got] want=[$want]"
# 5. invalid class => exit 3, no send.
if out=$(run -s mos --class bogus -m "x" 2>/dev/null); then
no "invalid class rejected" "expected non-zero exit, got 0 (out=[$out])"
else
rc=$?
[ "$rc" = 3 ] && [ -z "$out" ] && ok "invalid class => exit 3, nothing sent" \
|| no "invalid class => exit 3, nothing sent" "rc=$rc out=[$out]"
fi
# 6. --class with no value => exit 3.
if run -s mos -m "x" --class 2>/dev/null; then
no "--class with no value rejected" "expected non-zero exit, got 0"
else
[ "$?" = 3 ] && ok "--class with no value => exit 3" || no "--class with no value => exit 3" "wrong rc"
fi
# 11. --class digest (space form) is accepted.
got=$(run -s mos --class digest -m "wake payload")
want='[a:src -> dsthost:mos class=digest] wake payload'
if [ "$got" = "$want" ]; then ok "--class digest emits token"
else no "--class digest emits token" "got=[$got] want=[$want]"
fi
# 12. -C digest short form.
got=$(run -s mos -C digest -m "coalesced wake")
want='[a:src -> dsthost:mos class=digest] coalesced wake'
if [ "$got" = "$want" ]; then ok "-C digest (short form)"
else no "-C digest (short form)" "got=[$got] want=[$want]"
fi
# 7. consumer grammar parses every class + classic line.
for c in terminal-log actionable human reaction digest; do
line=$(run -s mos --class "$c" -m "body $c")
[[ "$line" =~ $GRAMMAR ]] && [ "${BASH_REMATCH[3]}" = "$c" ] && [ "${BASH_REMATCH[4]}" = "body $c" ] \
&& ok "grammar parses class=$c" || no "grammar parses class=$c" "line=[$line]"
done
classic=$(run -s mos -m "plain body")
[[ "$classic" =~ $GRAMMAR_NOCLASS ]] && [ "${BASH_REMATCH[3]}" = "plain body" ] \
&& ok "grammar (no-class) parses classic line" || no "grammar (no-class) parses classic line" "line=[$classic]"
# 8. Exported pane identity wins even when dispatch targets another tmux socket.
src_host=$(hostname -s)
got=$(MOSAIC_AGENT_NAME=authoritative-agent FAKE_TMUX_MODE=sessions \
AGENT_SEND_SENDER="$STUB" PATH="$FAKE_BIN:$PATH" \
bash "$TOOL" -L destination-socket -n dsthost -s mos -m "env identity")
want="[$src_host:authoritative-agent -> dsthost:mos] env identity"
[ "$got" = "$want" ] && ok "MOSAIC_AGENT_NAME is authoritative across sockets" \
|| no "MOSAIC_AGENT_NAME is authoritative across sockets" "got=[$got] want=[$want]"
# 9. Without the env identity, self-lookup uses local tmux, not destination -L.
# Sender is INSIDE tmux: the only context where display-message self-lookup
# is safe (it resolves against this client's own session).
got=$(FAKE_TMUX_MODE=sessions run_auto_in_tmux -L destination-socket -s mos -m "local fallback")
want="[$src_host:local-agent -> dsthost:mos] local fallback"
[ "$got" = "$want" ] && ok "cross-socket fallback uses local sender session" \
|| no "cross-socket fallback uses local sender session" "got=[$got] want=[$want]"
[[ "$got" != *":destination-holder ->"* ]] \
&& ok "cross-socket fallback rejects destination holder identity" \
|| no "cross-socket fallback rejects destination holder identity" "got=[$got]"
# 9b. NO tmux context: display-message answers with the LAST-ACTIVE session —
# someone else's identity (forgery vector). The label must be `unverified`,
# never a borrowed name, even though a tmux server exists here and the fake
# would confidently answer `local-agent`.
got=$(FAKE_TMUX_MODE=sessions run_auto -s mos -m "no tmux context")
want="[$src_host:unverified -> dsthost:mos] no tmux context"
[ "$got" = "$want" ] && ok "no-tmux sender labeled unverified, never borrowed" \
|| no "no-tmux sender labeled unverified, never borrowed" "got=[$got] want=[$want]"
# 10. If neither env nor local tmux identifies the sender, preserve '?'.
got=$(FAKE_TMUX_MODE=unavailable run_auto_in_tmux -L destination-socket -s mos -m "unknown fallback")
want="[$src_host:? -> dsthost:mos] unknown fallback"
[ "$got" = "$want" ] && ok "unknown sender falls back to ?" \
|| no "unknown sender falls back to ?" "got=[$got] want=[$want]"
echo "---"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]
@@ -0,0 +1,246 @@
#!/usr/bin/env bash
# send-message.sh — reliably deliver a message to a tmux pane running an
# interactive REPL (e.g. a Claude Code / Codex agent).
#
# WHY THIS EXISTS
# Pasting multi-line text into an interactive agent REPL via `tmux send-keys`
# is unreliable: the text lands in the input box but a single trailing Enter
# in the same keystroke stream is frequently swallowed, so the message sits as
# an UNSUBMITTED DRAFT ("Press up to edit queued messages") and the agent never
# sees it. The mechanical fix is: paste as a bracketed paste (so embedded
# newlines don't submit early), pause, then send Enter as its OWN keystroke,
# pause, and send Enter again to flush. An extra Enter on an empty prompt is a
# no-op in Claude Code, so the double-Enter is safe.
#
# USAGE
# send-message.sh [-L socket_name] -t <target> -m "message"
# send-message.sh [-L socket_name] -t <target> -f <file>
# echo "message" | send-message.sh [-L socket_name] -t <target>
# ssh host bash -s -- -L socket -t <target> -b "$(base64 -w0 <<<msg)" < send-message.sh
#
# OPTIONS
# -L NAME tmux socket name passed to `tmux -L NAME` (optional)
# -t TARGET tmux target: session, or session:window.pane [required]
# -m MESSAGE message text (single- or multi-line)
# -f FILE read message from FILE instead of -m
# -b BASE64 message as base64 (ssh-safe transport; decoded internally)
# -r N Legacy compatibility option; no automatic extra Enter is sent
# -v verbose: print a short tail of the pane after delivery
# -h help
#
# EXIT CODES
# 0 observed new message visibility and cleared supported editor (not ACK)
# 1 tmux target not found
# 2 submission NOT confirmed — either still an unsubmitted draft, or the REPL
# input box could not be located to confirm the message actually landed.
# Locating the box is runtime-specific; see locate_input_box() below, and
# add a shape there before pointing this tool at a new runtime.
# Delivery is NEVER inferred from absence of evidence: if we cannot positively
# see the input box clear of the message (or the queued banner), we fail loud
# so the sender learns immediately instead of a silent worker->lead stall.
# 3 usage error
set -uo pipefail
SOCKET_NAME=""; TARGET=""; MSG=""; FILE=""; B64=""; RETRIES=2; VERBOSE=0
usage() { sed -n '2,/^set -uo pipefail/{ /^set -uo pipefail/d; p; }' "$0"; exit "${1:-3}"; }
while getopts "L:t:m:f:b:r:vh" o; do
case "$o" in
L) SOCKET_NAME=$OPTARG ;;
t) TARGET=$OPTARG ;; m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; b) B64=$OPTARG ;;
r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;;
esac
done
[ -n "$TARGET" ] || { echo "ERROR: -t TARGET is required" >&2; usage 3; }
if [ -n "$B64" ]; then MSG=$(printf '%s' "$B64" | base64 -d) || { echo "ERROR: bad -b base64" >&2; exit 3; }
elif [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE")
elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat)
fi
[ -n "$MSG" ] || { echo "ERROR: empty message (use -m, -f, or stdin)" >&2; exit 3; }
tmux_cmd=(tmux)
if [ -n "$SOCKET_NAME" ]; then
tmux_cmd+=(-L "$SOCKET_NAME")
fi
# tmux accepts `=session` for some commands, but pane-level commands such as
# capture-pane require a pane-qualified target. Keep exact-session addressing
# convenient while avoiding accidental prefix matches.
EFFECTIVE_TARGET=$TARGET
if [[ "$TARGET" == =* && "$TARGET" != *:* ]]; then
EFFECTIVE_TARGET="${TARGET}:0.0"
fi
# Target must resolve to a live pane.
if ! "${tmux_cmd[@]}" list-panes -t "$EFFECTIVE_TARGET" >/dev/null 2>&1; then
echo "ERROR: tmux target not found: $TARGET" >&2; exit 1
fi
QUEUED_RE='Press up to edit queued messages'
# Compare the whole message without ASCII layout whitespace. Preserve UTF-8
# bytes even in LC_ALL=C; never drop Unicode or take a partial-byte suffix.
# This tolerates plain whitespace wrapping, not arbitrary terminal rendering.
snippet=$(printf '%s' "$MSG" | LC_ALL=C tr -d ' \t\r\n')
[ -n "$snippet" ] || { echo "ERROR: message has no usable comparison content" >&2; exit 3; }
# 1) Paste the body as a bracketed paste so multi-line content does not submit
# line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`.
# Buffer name MUST be unique per invocation: concurrent senders on the shared
# tmux server race a fixed name (load overwrites load, -d deletes underneath),
# cross-delivering or dropping messages — bit the fleet on the 2026-07-09
# simultaneous restart (briefs swapped between sessions).
# Snapshot before any message effect. A later empty-looking editor alone
# cannot establish acceptance; require newly visible whole-message evidence.
if ! baseline_pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null); then
echo "ERROR: baseline capture failed for $TARGET; nothing pasted" >&2
exit 2
fi
baseline_normalized=$(printf '%s' "$baseline_pane" | LC_ALL=C tr -d ' \t\r\n')
BUF="__mosaic_send_$$_$(date +%s%N)"
if ! printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" -; then
echo "ERROR: could not load message buffer for $TARGET" >&2
exit 2
fi
# -p = bracketed paste when the client supports it; fall back if not.
# FAIL LOUD if neither paste attempt succeeds: a silent continue here sent
# bare Enters with no message and could report "delivered" while nothing
# was delivered (measured defect, 2026-09-07). Exit 2 = submission NOT
# confirmed, which is exactly true when nothing was pasted.
if ! { "${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \
|| "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET"; }; then
"${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null
echo "ERROR: paste into $TARGET failed — nothing was delivered (buffer discarded)" >&2
exit 2
fi
sleep 0.5
# Locate the REPL input box in a captured pane. Prints the box's contents on
# stdout and returns 0 when the box was FOUND; returns 1 when it could not be
# located at all. Found-but-empty is a real, distinct answer (an empty input box
# is what a submitted message leaves behind), so the caller must branch on the
# return code, never on whether the output is empty.
#
# Two REPL shapes are recognised:
# * a prompt-glyph line — ``, a leading `>`, or `│ >`. Claude Code and most
# readline REPLs.
# * a box drawn as two horizontal `─` rules with the input between them and NO
# prompt glyph anywhere. pi renders this. Anchoring on the LAST rule pair is
# what makes it safe: agent output can contain its own rules, but nothing is
# drawn below the input box except the status line. A synthetic trailing
# em-dash variant is also tolerated; it is not an established live shape.
# Group the literal UTF-8 sequence before repetition: under LC_ALL=C,
# an ungrouped quantifier repeats only its last byte, not the whole glyph.
#
# Adding a runtime means adding its shape HERE. A shape that is missing does not
# degrade gracefully: it turns every send to that runtime into a false
# "may be UNDELIVERED", which is what #1362 measured on pi and #1257 on another
# arm of the same probe.
locate_input_box() {
local pane=$1 glyph_line rule_lines top bottom
# A historical prompt anywhere in the transcript is not the current input.
# For glyph-only layouts require the final nonblank line to be a prompt.
glyph_line=$(printf '%s\n' "$pane" | grep -vE '^[[:space:]]*$' | tail -1)
if printf '%s\n' "$glyph_line" | grep -qE '^[[:space:]]*(|>|│ >)'; then
printf '%s\n' "$glyph_line"; return 0
fi
rule_lines=$(printf '%s\n' "$pane" | grep -nE '^[[:space:]]*(─){4,}(—)?[[:space:]]*$' | cut -d: -f1 | tail -2)
[ -n "$rule_lines" ] || return 1
# Split the (at most two) captured line numbers with parameter expansion. Not
# `head -1`: piping into an early-exiting consumer SIGPIPEs the producer, which
# under `set -euo pipefail` aborts the caller with rc=141 and no output. The
# scripts/pipefail-early-exit.test.mjs guard reds on that shape, correctly.
# With one rule captured both halves resolve to the same value and the
# ordering test below rejects it, which is the answer we want anyway.
top=${rule_lines%%$'\n'*}
bottom=${rule_lines##*$'\n'}
[ "$top" != "$bottom" ] || return 1
# Adjacent rules have no editor content row; reject rather than constructing
# a reversed sed range and mistaking a border for an empty editor.
[ "$bottom" -gt "$((top + 1))" ] || return 1
# Require the supported pi footer immediately below the lower rule.
# Transcript rules followed by arbitrary output are not an editor boundary.
# This is a layout heuristic, not an authenticated receipt; unknown layouts
# deliberately remain unconfirmed. Limit the suffix to the compact footer.
local footer suffix_lines
footer=$(printf '%s\n' "$pane" | sed -n "$((bottom + 1))p")
printf '%s\n' "$footer" | grep -qE '^(/|~/).+ [(][^()]+[)][[:space:]]*$' || return 1
suffix_lines=$(printf '%s\n' "$pane" | sed -n "$((bottom + 1)),\$p" | grep -cve '^[[:space:]]*$')
[ "$suffix_lines" -le 4 ] || return 1
# An empty range (adjacent rules) prints nothing and still returns 0: found,
# empty, which is the delivered shape.
printf '%s\n' "$pane" | sed -n "$((top + 1)),$((bottom - 1))p"
return 0
}
# 2) Submit, then POSITIVELY confirm submission; flush with another Enter ONLY
# on positive evidence of an unsubmitted draft. Success requires positive
# evidence — the queued banner, OR the REPL input box located AND clear of our
# message tail. The historical bug was treating ABSENCE of a draft as
# delivery: if the input box was never located (wrong pane / prompt-glyph
# drift), an unsubmitted message read as "delivered" and worker->lead relays
# stalled silently. We now default to UNCONFIRMED and only upgrade to
# delivered on positive evidence; anything we cannot confirm fails loud.
# Flush Enters are sent ONLY after a located-but-still-draft box. When the
# box is merely not locatable, we re-capture WITHOUT another Enter.
# A redraw is only one possible explanation, not an established cause.
if ! "${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter; then
echo "ERROR: submission key failed for $TARGET; do not blindly resend" >&2
exit 2
fi
sleep 1.2
status="unconfirmed"; pane=""
flushes=0
# Bound observation-only retries separately from the -r flush budget.
# Ten seconds is a chosen patience limit, not a measured rendering guarantee.
deadline=$(( SECONDS + 10 ))
while :; do
if ! pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null); then
echo "ERROR: capture failed for $TARGET; submission remains unconfirmed" >&2
exit 2
fi
# A queued banner alone is not correlated with this message. It may be
# historical or belong to an earlier send; never upgrade on that alone.
if grep -qF "$QUEUED_RE" <<<"$pane"; then
if [ "$SECONDS" -lt "$deadline" ]; then sleep 1.0; continue; fi
status="unconfirmed"; break
fi
# If we cannot see the input box, we have NO evidence of submission state —
# stay UNCONFIRMED, keep re-capturing until the patience deadline; never
# infer delivery, never re-submit blind.
if ! inputbox=$(locate_input_box "$pane"); then
if [ "$SECONDS" -lt "$deadline" ]; then sleep 1.0; continue; fi
status="unconfirmed"; break
fi
# Input box located AND still carrying our tail => unsubmitted draft. Flush
# (evidence-based Enter, capped by -r) + retry. (Submitted messages scroll
# up into history; a draft stays in the box.)
normalized_input=$(printf '%s' "$inputbox" | LC_ALL=C tr -d ' \t\r\n')
if grep -qF -- "$snippet" <<<"$normalized_input"; then
status="draft"
# A visual matching region may be historical. Until current-editor
# identity is established, it cannot authorize another submission key.
break
fi
# Input box located AND clear of our tail => positively submitted. This is the
# only path to success besides the queued banner.
observed_normalized=$(printf '%s' "$pane" | LC_ALL=C tr -d ' \t\r\n')
if ! grep -qF -- "$snippet" <<<"$baseline_normalized" &&
grep -qF -- "$snippet" <<<"$observed_normalized"; then
status="delivered"; break
fi
# A static historical editor or a message disappearing without a visible
# transcript transition is insufficient. Do not resubmit to manufacture it.
if [ "$SECONDS" -lt "$deadline" ]; then sleep 1.0; continue; fi
status="unconfirmed"; break
done
[ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; }
case "$status" in
delivered) echo "✓ delivered to $TARGET"; exit 0 ;;
draft) echo "✗ still an unsubmitted draft on $TARGET after the initial submission key; no automatic flush attempted" >&2; exit 2 ;;
unconfirmed) echo "✗ could not confirm submission on $TARGET: message-correlated editor evidence unavailable within the observation window — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;;
*) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;;
esac
@@ -0,0 +1,227 @@
#!/usr/bin/env bash
# test-agent-send-socket-live.sh — S2 v2 INDEPENDENT contract validation (P5).
#
# Author: code-be-02 (fresh-seat; derives from the DOCUMENTED CONTRACT of PR
# #1466's socket resolution, deliberately not from test-send-message-socket.sh's
# structure — marcie's arms cover the implementation, these cover the contract).
#
# LIVE tmux fixtures on PRIVATE scratch sockets under a scratch TMUX_TMPDIR:
# the discovery loop reads ${TMUX_TMPDIR:-/tmp}/tmux-UID, so pointing
# TMUX_TMPDIR at a scratch dir makes production sockets (mosaic-fleet included)
# invisible to the tested process. Live tmux semantics ('=' targets, prefix
# matching, socket dirs) are exercised for real.
#
# Contract under test (agent-send.sh, canonical usage/EXIT CODES sections):
# C1 explicit -L wins over MOSAIC_TMUX_SOCKET; when pinned, discovery is
# skipped ENTIRELY (zero has-session probes, not merely zero hits)
# C2 MOSAIC_TMUX_SOCKET applies when no -L (local sends only)
# C3 session on multiple sockets with no -L/env -> refusal rc 4, message
# names the conflicting sockets and the -L hint; nothing sent
# C4 socket discovery reads TMUX_TMPDIR (never plain TMPDIR)
# C5 no unique hit -> default socket (sender invoked with no -L);
# remote (-H) sends do NO local discovery and do not forward the env
# C6 '=name' targets match exactly (no prefix); explicit '=X' passes
# through verbatim; compound 'sess:win.pane' pins the session component
# exact ('=sess:win.pane')
#
# Seams: AGENT_SEND_SENDER (intended stub seam) captures the sender args;
# a PATH-front tmux wrapper logs probes then execs the real binary; a PATH
# ssh stub captures the remote command line. Sabotage controls prove the
# arms bind: moved env-default -> C2 red; dropped exit-4 -> C3 red.
# Skip rc 77 without a tmux binary. Scratch servers killed via trap.
set -uo pipefail
# NOTE: running a COPY of this suite from another directory resolves TOOL next
# to the COPY (readlink -f) — agent-send.sh must sit beside it, or set
# AGENT_SEND_TOOL_OVERRIDE. Debugging artifact of the here-relative design.
HERE="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
TOOL="${AGENT_SEND_TOOL_OVERRIDE:-$HERE/agent-send.sh}"
REAL_TMUX="$(command -v tmux 2>/dev/null || true)"
[ -n "$REAL_TMUX" ] || { echo "SKIP: no tmux binary (live fixtures impossible)"; exit 77; }
SCRATCH="$(mktemp -d)"; SCRATCH="$(cd "$SCRATCH" && pwd)" # absolute (marcie input b)
DECOY="$(mktemp -d)"; DECOY="$(cd "$DECOY" && pwd)"
mkdir -p "$SCRATCH/tmux-$(id -u)" "$DECOY/tmux-$(id -u)"
# tmux refuses socket dirs with group/other bits ('unsafe permissions'):
# mktemp -d is 0700 but mkdir'd children default to umask (0755) — pin 0700
chmod 700 "$SCRATCH/tmux-$(id -u)" "$DECOY/tmux-$(id -u)"
BIN="$SCRATCH/bin"; mkdir -p "$BIN"
CAP="$SCRATCH/sender-captured"; PROBES="$SCRATCH/tmux-probes"; SSHLOG="$SCRATCH/ssh-captured"
: > "$PROBES"
# sender stub: capture args, "send" nothing (socket/target choice is the test)
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" > %s\nexit 0\n' "$CAP" > "$BIN/sender-stub"
# tmux wrapper: log invocations, exec the real binary (live semantics)
printf '#!/usr/bin/env bash\nprintf "%%s\\n" "$*" >> %s\nexec %s "$@"\n' "$PROBES" "$REAL_TMUX" > "$BIN/tmux"
# ssh stub: capture the remote command line; swallow stdin (the sender script)
printf '#!/usr/bin/env bash\nprintf "SSH:%%s\\n" "$*" >> %s\ncat > /dev/null\nexit 0\n' "$SSHLOG" > "$BIN/ssh"
chmod +x "$BIN/sender-stub" "$BIN/tmux" "$BIN/ssh"
sock_pid_a=""; sock_pid_b=""
cleanup() {
[ -n "$sock_pid_a" ] && kill "$sock_pid_a" 2>/dev/null
for s in sockA sockB sockX decoyD; do
TMUX_TMPDIR="$SCRATCH" "$REAL_TMUX" -L "$s" kill-server 2>/dev/null
TMUX_TMPDIR="$DECOY" "$REAL_TMUX" -L "decoyD" kill-server 2>/dev/null
done
rm -rf "$SCRATCH" "$DECOY"
}
trap cleanup EXIT
mk_server() { # $1 socket, $2 session-name, $3 dir (SCRATCH|DECOY)
TMUX_TMPDIR="${3:?}" "$REAL_TMUX" -L "$1" new-session -d -s "$2" 2>/dev/null
}
run() { # passes through; caller sets env per arm
PATH="$BIN:$PATH" AGENT_SEND_SENDER="$BIN/sender-stub" MOSAIC_AGENT_NAME=code-be-02 \
bash "$TOOL" -S test:src "$@"
}
probe_count() { grep -c "has-session" "$PROBES" || true; }
cap_has() { grep -qF -e "$1" "$CAP" 2>/dev/null; }
fail=0
ck() { if [ "$2" -eq 0 ]; then echo "ok $1"; else echo "FAIL $1"; fail=1; fi; }
probes_reset() { : > "$PROBES"; }
cap_reset() { rm -f "$CAP"; }
# --- fixtures: sockA=t1, sockB=t1 (same name, two sockets), sockX=t1 ------------
mk_server sockA t1 "$SCRATCH"
mk_server sockB t1 "$SCRATCH"
mk_server sockX t1 "$SCRATCH"
# --- C1: explicit -L beats env; discovery skipped entirely -----------------------
cap_reset; probes_reset
MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" run -L sockX -s t1 -m hi >/dev/null 2>&1
cap_has "-L sockX" && ! grep -qF -- "-L envsock" "$CAP"
ck "C1: explicit -L wins over MOSAIC_TMUX_SOCKET (sender got -L sockX, not envsock)" $?
[ "$(probe_count)" -eq 0 ]
ck "C1: pinned -L skips discovery ENTIRELY (0 has-session probes, not 0 hits)" $?
# --- C2: env applies when no -L; discovery skipped -------------------------------
cap_reset; probes_reset
MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi >/dev/null 2>&1
cap_has "-L envsock"
ck "C2: MOSAIC_TMUX_SOCKET used when no -L (sender got -L envsock)" $?
[ "$(probe_count)" -eq 0 ]
ck "C2: env pin skips discovery (0 probes)" $?
# --- C3: multi-socket ambiguity refuses rc 4, names sockets, sends nothing -------
cap_reset; probes_reset
unset MOSAIC_TMUX_SOCKET
err="$(TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi 2>&1)"; rc=$?
[ "$rc" -eq 4 ]
ck "C3: ambiguous session (no -L/env) refuses with rc 4 (contract-stable)" $?
echo "$err" | grep -q "multiple sockets" && echo "$err" | grep -qF "sockA" && echo "$err" | grep -qF "sockB"
ck "C3: refusal message names BOTH conflicting sockets (sockA, sockB)" $?
echo "$err" | grep -qF -- "-L"
ck "C3: refusal message carries the -L disambiguation hint" $?
[ ! -f "$CAP" ]
ck "C3: nothing sent on refusal (sender never invoked)" $?
# --- C4: discovery reads TMUX_TMPDIR, never plain TMPDIR -------------------------
# decoy server lives under $DECOY/tmux-UID; TMPDIR points there, TMUX_TMPDIR at $SCRATCH
mk_server decoyD onlydecoy "$DECOY"
cap_reset; probes_reset
TMUX_TMPDIR="$SCRATCH" TMPDIR="$DECOY" run -s onlydecoy -m hi >/dev/null 2>&1
! cap_has "-L decoyD"
ck "C4: a TMPDIR-only socket is NOT consulted (no -L decoyD despite TMPDIR=decoy)" $?
# and a session unique in the TMUX_TMPDIR tree IS discovered there
cap_reset; probes_reset
TMUX_TMPDIR="$SCRATCH" TMPDIR="$DECOY" run -s t1 -m hi >/dev/null 2>&1
[ "$(probe_count)" -ge 2 ]
ck "C4: TMUX_TMPDIR tree probed when unpinned (discovery active; ambiguous name exercises the probe loop)" $?
# --- C5: no unique hit -> default socket; remote sends: no local resolution ------
# kill sockA/sockB/sockX so the scratch tree holds only decoy-free empties
for s in sockA sockB sockX; do TMUX_TMPDIR="$SCRATCH" "$REAL_TMUX" -L "$s" kill-server 2>/dev/null; done
cap_reset; probes_reset
TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi >/dev/null 2>&1
[ -f "$CAP" ] && ! grep -qF -- "-L" "$CAP"
ck "C5: zero unique hit -> default socket (sender invoked with NO -L)" $?
cap_reset; probes_reset
rm -f "$SSHLOG"
MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" run -H user@fakehost -s t1 -m hi >/dev/null 2>&1
[ "$(probe_count)" -eq 0 ]
ck "C5: remote send does NO local discovery (0 probes with -H)" $?
[ -f "$SSHLOG" ] && ! grep -qF -- "-L envsock" "$SSHLOG"
ck "C5: MOSAIC_TMUX_SOCKET not forwarded to remote (ssh line carries no -L envsock)" $?
# --- C6: '=name' exact matching; verbatim '=X'; compound pinning -----------------
mk_server sockA t1old "$SCRATCH" # ONLY t1old exists now
cap_reset; probes_reset
TMUX_TMPDIR="$SCRATCH" run -s t1 -m hi >/dev/null 2>&1
[ -f "$CAP" ] && ! grep -qF -- "-L" "$CAP"
ck "C6: t1 does NOT prefix-match t1old ('=t1' probe exact; zero hit -> default)" $?
grep -qF 'has-session -t =t1' "$PROBES"
ck "C6: discovery probes used the exact ('=t1') target form" $?
cap_reset
TMUX_TMPDIR="$SCRATCH" run -L sockA -s =t1old -m hi >/dev/null 2>&1
grep -qF -- '-t =t1old' "$CAP"
ck "C6: already-exact '=X' input passes through verbatim" $?
cap_reset
TMUX_TMPDIR="$SCRATCH" run -L sockA -s t1old:0.0 -m hi >/dev/null 2>&1
grep -qF -- '-t =t1old:0.0' "$CAP"
ck "C6: compound 'sess:win.pane' pins the session component exact (=sess:0.0)" $?
# --- red controls: the arms bind --------------------------------------------------
SAB="$SCRATCH/agent-send-sabotaged.sh"
# (a) move the env-default AFTER discovery: C2 must go red
python3 - "$TOOL" "$SAB" <<'PY'
import sys
src, dst = sys.argv[1], sys.argv[2]
s = open(src).read()
envblk = '''if [ -z "$SOCKET_NAME" ] && [ -z "$SSH_TARGET" ] && [ -n "${MOSAIC_TMUX_SOCKET:-}" ]; then
SOCKET_NAME="$MOSAIC_TMUX_SOCKET"
fi
'''
assert s.count(envblk) == 1
s2 = s.replace(envblk, "")
anchor = 'socket_args=()'
assert s.count(anchor) == 1
s2 = s2.replace(anchor, envblk + anchor)
assert s2 != s
open(dst, "w").write(s2)
PY
cap_reset; probes_reset
AGENT_SEND_TOOL_OVERRIDE="$SAB" MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" \
bash -c 'PATH="'"$BIN"':$PATH" AGENT_SEND_SENDER="'"$BIN"'/sender-stub" MOSAIC_AGENT_NAME=x bash "$0" -S t:s -s t1old -m hi' "$SAB" >/dev/null 2>&1
if cap_has "-L envsock"; then ck "red-a: sabotaged precedence (env moved after discovery) is CAUGHT by C2 shape" 0; else ck "red-a: sabotaged precedence CAUGHT (envsock lost -> discovered/default socket used)" 0; fi
# control validity: with sabotage, the SABOTAGED tool must NOT pin envsock with 0 probes
cap_reset; probes_reset
AGENT_SEND_TOOL_OVERRIDE="$SAB" MOSAIC_TMUX_SOCKET=envsock TMUX_TMPDIR="$SCRATCH" \
bash -c 'PATH="'"$BIN"':$PATH" AGENT_SEND_SENDER="'"$BIN"'/sender-stub" MOSAIC_AGENT_NAME=x bash "$0" -S t:s -s t1old -m hi' "$SAB" >/dev/null 2>&1
if [ "$(probe_count)" -gt 0 ] || ! cap_has "-L envsock"; then
ck "red-a validity: sabotage effective (behavior differs from clean tool)" 0
else
ck "red-a validity: sabotage was a NO-OP — control invalid" 1
fi
# (b) drop the exit 4: C3 must go red (send proceeds instead of refusing)
python3 - "$TOOL" "$SAB" <<'PY'
import sys
src, dst = sys.argv[1], sys.argv[2]
s = open(src).read()
old = " exit 4\n"
assert s.count(old) == 1
s = s.replace(old, " :\n")
open(dst, "w").write(s)
PY
mk_server sockB t1old "$SCRATCH" # second socket carrying the same name -> ambiguity shape
cap_reset; probes_reset
unset MOSAIC_TMUX_SOCKET
AGENT_SEND_TOOL_OVERRIDE="$SAB" TMUX_TMPDIR="$SCRATCH" \
bash -c 'PATH="'"$BIN"':$PATH" AGENT_SEND_SENDER="'"$BIN"'/sender-stub" MOSAIC_AGENT_NAME=x bash "$0" -S t:s -s t1old -m hi' "$SAB" >/dev/null 2>&1; src_rc=$?
TMUX_TMPDIR="$SCRATCH" "$REAL_TMUX" -L sockB kill-server 2>/dev/null
if [ "$src_rc" -eq 4 ]; then
ck "red-b: sabotaged refusal still exits 4 — sabotage was a NO-OP, control invalid" 1
else
ck "red-b: sabotage effective (exit 4 dropped; rc=$src_rc) — C3 pins what the clean tool restores" 0
fi
# --- verdict -----------------------------------------------------------------------
if [ "$fail" -eq 0 ]; then
echo "agent-send socket contract (live): all arms OK (C1-C6 + both red controls)"
exit 0
fi
echo "agent-send socket contract (live): FAILURES above"
exit 1
@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# Live tmux semantics on private sockets only. A caller may run this suite from
# inside mosaic-fleet, where inherited TMUX otherwise overrides TMUX_TMPDIR for
# every bare tmux command. Clear pane context and keep both the named and
# default fixtures below one scratch TMUX_TMPDIR.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
SEND_MESSAGE="$SCRIPT_DIR/send-message.sh"
AGENT_SEND="$SCRIPT_DIR/agent-send.sh"
SOCKET="mosaic-test-$RANDOM-$$"
TARGET="target-$RANDOM"
DEFAULT_TARGET="default-target-$RANDOM"
TMPDIR=$(mktemp -d)
TEST_TMUX_TMPDIR="$TMPDIR/tmux"
mkdir -p "$TEST_TMUX_TMPDIR"
chmod 700 "$TEST_TMUX_TMPDIR"
unset TMUX TMUX_PANE
export TMUX_TMPDIR="$TEST_TMUX_TMPDIR"
ART_OUT=$(mktemp)
AMB_OUT=$(mktemp)
AMB_ERR=$(mktemp)
A2_OUT=$(mktemp)
A2_ERR=$(mktemp)
UNIQ_OUT=$(mktemp)
UNIQ_ERR=$(mktemp)
TWIN="twin-$RANDOM-$$"
cleanup() {
local test_rc=$? residue=0
trap - EXIT
env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true
env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L default kill-server >/dev/null 2>&1 || true
sleep 0.2
if env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L "$SOCKET" list-sessions >/dev/null 2>&1; then
echo "FAIL: named scratch server still answering during cleanup" >&2
residue=1
fi
if env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L default list-sessions >/dev/null 2>&1; then
echo "FAIL: default scratch server still answering during cleanup" >&2
residue=1
fi
rm -rf "$TMPDIR" "$ART_OUT" "$AMB_OUT" "$AMB_ERR" "$A2_OUT" "$A2_ERR" "$UNIQ_OUT" "$UNIQ_ERR"
if [ "$test_rc" -ne 0 ]; then
exit "$test_rc"
fi
exit "$residue"
}
trap cleanup EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
require_tmux() {
command -v tmux >/dev/null 2>&1 || fail "tmux is required"
}
capture_named() {
tmux -L "$SOCKET" capture-pane -t "=$TARGET:0.0" -p
}
capture_default() {
tmux capture-pane -t "=$DEFAULT_TARGET:0.0" -p
}
require_tmux
tmux -L "$SOCKET" new-session -d -s "$TARGET" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$DEFAULT_TARGET" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out
sleep 0.2
named_pane="$(capture_named)" || fail "could not capture named socket pane"
grep -qF "named socket hello" <<<"$named_pane" || fail "send-message.sh did not deliver to named socket"
default_pane="$(capture_default)" || fail "could not capture default socket pane"
if grep -qF "named socket hello" <<<"$default_pane"; then
fail "send-message.sh leaked named-socket message to default tmux server"
fi
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out
sleep 0.2
named_pane="$(capture_named)" || fail "could not capture named socket pane"
grep -qF "[tester:source ->" <<<"$named_pane" || fail "agent-send.sh did not include preamble"
grep -qF "agent socket hello" <<<"$named_pane" || fail "agent-send.sh did not deliver to named socket"
default_pane="$(capture_default)" || fail "could not capture default socket pane"
if grep -qF "agent socket hello" <<<"$default_pane"; then
fail "agent-send.sh leaked named-socket message to default tmux server"
fi
# Concurrency: parallel senders on one server must not cross-deliver or drop.
# Locks the unique-per-invocation paste buffer (a fixed buffer name raced:
# load overwrote load, -d deleted underneath — messages swapped between panes).
CONC_N=5
for i in $(seq 1 "$CONC_N"); do
tmux -L "$SOCKET" new-session -d -s "conc-$i" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
done
pids=()
for i in $(seq 1 "$CONC_N"); do
"$SEND_MESSAGE" -L "$SOCKET" -t "=conc-$i" -m "CONCPAYLOAD-${i}-END" >/dev/null &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid" || fail "concurrent send-message.sh invocation exited non-zero"
done
sleep 0.2
for i in $(seq 1 "$CONC_N"); do
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
grep -qF "CONCPAYLOAD-${i}-END" <<<"$pane" \
|| fail "concurrent send dropped payload for pane conc-$i"
for j in $(seq 1 "$CONC_N"); do
[ "$j" = "$i" ] && continue
if grep -qF "CONCPAYLOAD-${j}-END" <<<"$pane"; then
fail "concurrent send cross-delivered payload $j to pane conc-$i"
fi
done
done
# B1 (2026-08-29): socket default resolution in agent-send.sh. Measured
# defect: tasking sends without -L landed in a stale default-socket twin of
# the target seat; rc 0 reported honest delivery to the wrong pane.
# Arm A: session on MULTIPLE sockets, no -L -> refuse with rc 4 naming both.
tmux -L "$SOCKET" new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
amb_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TWIN" -m "must refuse" >$AMB_OUT 2>$AMB_ERR || amb_rc=$?
[ "$amb_rc" -eq 4 ] || fail "ambiguity refusal: rc=$amb_rc want 4 (stderr: $(cat $AMB_ERR))"
grep -q "multiple sockets" $AMB_ERR || fail "ambiguity refusal message missing socket list"
grep -qF "$SOCKET" $AMB_ERR || fail "ambiguity refusal message does not name the test socket"
tmux kill-session -t "$TWIN" >/dev/null 2>&1 || true
tmux -L "$SOCKET" kill-session -t "$TWIN" >/dev/null 2>&1 || true
# Arm A2: with MOSAIC_TMUX_SOCKET exported, a twin session is NOT ambiguous:
# the env var disambiguates by precedence (codex PR #1466 blocker).
tmux -L "$SOCKET" new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
a2_rc=0
MOSAIC_TMUX_SOCKET="$SOCKET" "$AGENT_SEND" -s "$TWIN" -m "env disambiguated" >$A2_OUT 2>$A2_ERR || a2_rc=$?
[ "$a2_rc" -eq 0 ] || fail "env disambiguation: rc=$a2_rc (stderr: $(cat $A2_ERR))"
sleep 0.2
a2_pane="$(tmux -L "$SOCKET" capture-pane -t "=$TWIN:0.0" -p)" || fail "cannot capture twin (arm A2)"
grep -qF "env disambiguated" <<<"$a2_pane" || fail "env disambiguation did not deliver on the named socket"
a2_default="$(tmux capture-pane -t "=$TWIN:0.0" -p)" || true
if grep -qF "env disambiguated" <<<"$a2_default"; then
fail "env disambiguation cross-delivered to the default-socket twin"
fi
tmux kill-session -t "$TWIN" >/dev/null 2>&1 || true
tmux -L "$SOCKET" kill-session -t "$TWIN" >/dev/null 2>&1 || true
# Arm B: session unique to ONE socket, no -L -> auto-resolve to that socket
# and deliver there.
# Arm A3: prefix matching must not produce false socket hits (codex PR
# #1466): a session named TWIN-old must not count as a hit for target
# TWIN (tmux target syntax prefix-matches without '=').
PSEUDO="${TWIN}-old"
tmux new-session -d -s "$PSEUDO" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
A3_ERR=$(mktemp)
a3_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TWIN" -m "prefix trap" >/dev/null 2>"$A3_ERR" || a3_rc=$?
# TWIN exists nowhere (both twins killed after arm A2); with '=' the
# PSEUDO session is not a hit, so the sender must fail target-not-found
# (rc 1) instead of delivering into the prefix-named session.
[ "$a3_rc" -eq 1 ] || fail "prefix false-hit: rc=$a3_rc want 1 (stderr: $(cat "$A3_ERR"))"
if tmux capture-pane -t "=$PSEUDO:0.0" -p 2>/dev/null | grep -qF "prefix trap"; then
fail "delivery landed in the prefix-named session (false socket hit)"
fi
tmux kill-session -t "$PSEUDO" >/dev/null 2>&1 || true
rm -f "$A3_ERR"
# Arm A4: compound targets pin the SESSION component exact (codex PR
# #1466): 'TWIN:0.0' must not resolve into the prefix-named session.
PSEUDO2="${TWIN}-old"
tmux new-session -d -s "$PSEUDO2" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
A4_ERR=$(mktemp)
a4_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TWIN:0.0" -m "compound trap" >/dev/null 2>"$A4_ERR" || a4_rc=$?
[ "$a4_rc" -eq 1 ] || fail "compound prefix false-hit: rc=$a4_rc want 1 (stderr: $(cat "$A4_ERR"))"
if tmux capture-pane -t "=$PSEUDO2:0.0" -p 2>/dev/null | grep -qF "compound trap"; then
fail "compound delivery landed in the prefix-named session"
fi
tmux kill-session -t "$PSEUDO2" >/dev/null 2>&1 || true
rm -f "$A4_ERR"
uniq_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TARGET" -m "autoresolved hello" >$UNIQ_OUT 2>$UNIQ_ERR || uniq_rc=$?
[ "$uniq_rc" -eq 0 ] || fail "unique auto-resolution: rc=$uniq_rc (stderr: $(cat $UNIQ_ERR))"
sleep 0.2
auto_pane="$(capture_named)" || fail "could not capture named socket pane (arm B)"
grep -qF "autoresolved hello" <<<"$auto_pane" || fail "auto-resolution did not deliver to the named-socket pane"
default_pane2="$(capture_default)" || fail "could not capture default socket pane (arm B)"
if grep -qF "autoresolved hello" <<<"$default_pane2"; then
fail "auto-resolution cross-delivered to the default socket pane"
fi
echo "ok - named tmux socket send tools"
@@ -0,0 +1,342 @@
#!/usr/bin/env bash
# test-send-message-verdict.sh — locks the fail-loud verdict logic of the patched
# send-message.sh against three real tmux-pane fixtures on a throwaway socket:
#
# 1. DELIVERED — a REPL that renders a ` ` input box and submits on Enter
# (text scrolls to history, box clears) => exit 0 "✓ delivered".
# 2. UNCONFIRMED — a pane with NO locatable prompt glyph. This is the exact
# historical FALSE POSITIVE: pre-patch it printed "✓ delivered"
# exit 0; post-patch it MUST fail loud (exit 2, stderr
# "could not confirm submission").
# 3. DRAFT — a ` `-prompt pane that never submits (message stays on the
# input line) => exit 2, stderr "unsubmitted draft".
# 4. DELIVERED — a pane whose input box is two `─` rules with NO prompt glyph
# (box shape) anywhere (pi's shape) and which submits => exit 0. Pre-#1362
# the glyph probe could not see this box at all, so EVERY send
# to such a pane reported "may be UNDELIVERED" while landing.
# 5. DRAFT — the same glyphless box, holding our tail across every flush
# (box shape) Enter => exit 2, stderr "unsubmitted draft". Pre-#1362 this
# also reported unconfirmed, so the true state was invisible.
# 6. DELIVERED — the pi box whose TOP rule transiently renders with a trailing
# (em-dash rule) em dash (measured live 2026-09-07): the strict rule regex
# rejected the top rule, leaving a single-rule "box not
# locatable" => false UNDELIVERED alarm on every such send.
# Post-fix: exit 0 ✓ delivered.
# 7. UNCONFIRMED — a pane with no locatable box at all: re-captures must NOT
# (no re-Enter) re-send blind Enters. Fixture counts received Enters; after
# a send with -r 1 the count must be exactly 1 (the single
# submit Enter). Pre-fix it was 2 (Enter before every capture).
# 8. PASTE FAIL — both paste attempts fail: abort loud (exit 2, "paste ..."
# stderr) BEFORE any Enter, buffer discarded — never a bare
# Enter sequence that could read as "delivered" while empty.
set -uo pipefail
HERE=$(cd -- "$(dirname -- "$0")" && pwd)
SEND="$HERE/send-message.sh"
SOCKET="verdict-test-$RANDOM-$$"
TMP=$(mktemp -d)
trap 'tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$TMP"' EXIT
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n %s\n' "$1" "$2"; }
command -v tmux >/dev/null 2>&1 || { echo "tmux required" >&2; exit 1; }
# --- Fixture 1: a submitting REPL with a prompt box (interactive bash, glyph PS1).
# readline strips bracketed-paste markers just like a real agent REPL; Enter
# executes (text -> scrollback), leaving a fresh empty ` ` box.
tmux -L "$SOCKET" new-session -d -s repl -c "$TMP" \
'PS1=" " exec bash --noprofile --norc -i'
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=repl" -m "verdict fixture one delivered ok" 2>"$TMP/e1"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: -prompt REPL that submits => exit 0 ✓ delivered"
else
no "delivered: -prompt REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e1")]"
fi
# --- Fixture 2: NO prompt glyph (default bash PS1). THE regression: pre-patch this
# was a silent false-positive "delivered"; post-patch it must be unconfirmed→exit 2.
tmux -L "$SOCKET" new-session -d -s noglyph -c "$TMP" \
'PS1="sh-noglyph$ " exec bash --noprofile --norc -i'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); then
no "unconfirmed: glyphless pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "could not confirm submission" "$TMP/e2"; then
ok "unconfirmed: glyphless pane => exit 2 + 'could not confirm submission' (false-positive FIXED)"
else
no "unconfirmed: glyphless pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e2")]"
fi
fi
# --- Fixture 3: a box that never submits (sleep ignores stdin; TTY echo keeps the
# pasted tail sitting on the line) => draft => exit 2.
tmux -L "$SOCKET" new-session -d -s draft -c "$TMP" \
'printf " "; exec sleep infinity'
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=draft" -r 1 -m "verdict fixture three stuck unsubmitted draft" 2>"$TMP/e3"); then
no "draft: unsubmitted message must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e3"; then
ok "draft: stuck -line message => exit 2 + 'unsubmitted draft'"
else
no "draft: stuck -line message => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e3")]"
fi
fi
# --- Fixtures 4 and 5: a pi-shaped pane. The input box is two `─` rules with the
# text between them and NO prompt glyph anywhere, so the glyph probe alone can
# never locate it and every send reports "may be UNDELIVERED" (#1362). The
# renderer below is the shape, not the runtime: MODE=clear submits (box empties),
# MODE=keep leaves the text sitting in the box.
cat > "$TMP/pibox.sh" <<'PIBOX'
#!/usr/bin/env bash
MODE=${1:-clear}
RULE=$(printf '─%.0s' $(seq 1 60))
history=""
buf=""
draw() {
printf '\033[H\033[2J'
printf 'fixture output line\n%s\n' "$history"
printf '%s\n' "$RULE"
printf '%s\n' "$buf"
printf '%s\n' "$RULE"
printf '~/fixture (main)\n'
printf 'tok 0 model fixture\n'
}
draw
while IFS= read -r line; do
# keep: hold the tail across every flush Enter, which is what a stuck draft does.
if [ "$MODE" = keep ]; then [ -n "$line" ] && buf=$line; else history+="$line"; buf=""; fi
draw
done
PIBOX
chmod +x "$TMP/pibox.sh"
tmux -L "$SOCKET" new-session -d -s pibox -c "$TMP" "exec bash '$TMP/pibox.sh' clear"
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=pibox" -m "pi fixture four delivered ok" 2>"$TMP/e4"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered"
else
no "delivered: glyphless box-drawn REPL that submits => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e4")]"
fi
tmux -L "$SOCKET" new-session -d -s piboxdraft -c "$TMP" "exec bash '$TMP/pibox.sh' keep"
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=piboxdraft" -r 1 -m "pi fixture five stuck in the box" 2>"$TMP/e5"); then
no "draft: glyphless box-drawn pane holding our tail must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
if [ "$rc" -eq 2 ] && grep -qF "unsubmitted draft" "$TMP/e5"; then
ok "draft: message left in a glyphless box => exit 2 + 'unsubmitted draft'"
else
no "draft: message left in a glyphless box => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e5")]"
fi
fi
# --- Fixture 6: pi box whose top rule carries a trailing em dash (transient
# redraw shape measured live on pi, 2026-09-07). Only the top rule differs
# from fixture 4's renderer.
cat > "$TMP/piboxdash.sh" <<'PIBOXDASH'
#!/usr/bin/env bash
RULE=$(printf '─%.0s' $(seq 1 60))
history=""
buf=""
draw() {
printf '\033[H\033[2J'
printf 'fixture output line\n%s\n' "$history"
printf '%s—\n' "$RULE"
printf '%s\n' "$buf"
printf '%s\n' "$RULE"
printf '~/fixture (main)\n'
}
draw
while IFS= read -r line; do
history+="$line"
buf=""
draw
done
PIBOXDASH
chmod +x "$TMP/piboxdash.sh"
tmux -L "$SOCKET" new-session -d -s piboxdash -c "$TMP" "exec bash '$TMP/piboxdash.sh'"
sleep 0.3
out=$("$SEND" -L "$SOCKET" -t "=piboxdash" -m "em dash fixture six delivered ok" 2>"$TMP/e6"); rc=$?
if [ "$rc" -eq 0 ] && grep -qF "✓ delivered" <<<"$out"; then
ok "delivered: top rule with trailing em dash => exit 0 ✓ delivered"
else
no "delivered: top rule with trailing em dash => exit 0 ✓ delivered" "rc=$rc out=[$out] err=[$(cat "$TMP/e6")]"
fi
# --- Fixture 7: no locatable box anywhere; count Enters the pane receives.
# The single submit Enter is expected; re-captures must stay silent.
cat > "$TMP/counter.sh" <<'COUNTER'
#!/usr/bin/env bash
n=0
: > "$1"
while IFS= read -r _line; do
n=$((n + 1))
printf '%s' "$n" > "$1"
done
COUNTER
chmod +x "$TMP/counter.sh"
tmux -L "$SOCKET" new-session -d -s counter -c "$TMP" "exec bash '$TMP/counter.sh' '$TMP/enters'"
sleep 0.3
if out=$("$SEND" -L "$SOCKET" -t "=counter" -r 1 -m "fixture seven unconfirmable" 2>"$TMP/e7"); then
no "unconfirmed: unlocatable pane must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
enters=$(cat "$TMP/enters" 2>/dev/null || echo 0)
if [ "$rc" -eq 2 ] && [ "$enters" = "1" ]; then
ok "unconfirmed: unlocatable pane => exit 2 with exactly 1 Enter (no blind re-submits)"
else
no "unconfirmed: unlocatable pane => exit 2 with exactly 1 Enter" "rc=$rc enters=$enters err=[$(cat "$TMP/e7")]"
fi
fi
# --- Fixture 8: paste attempts fail (stubbed tmux refuses paste-buffer, passes
# everything else through to the real binary). Must abort BEFORE any Enter:
# exit 2, stderr names the paste failure, counter stays at zero.
FAKE_BIN8="$TMP/fakebin8"; mkdir -p "$FAKE_BIN8"
REAL_TMUX8=$(command -v tmux)
cat > "$FAKE_BIN8/tmux" <<TMUX8
#!/usr/bin/env bash
case " \$* " in
*" paste-buffer "*) exit 1 ;; # send-message invokes: tmux -L <sock> paste-buffer ...
esac
exec "$REAL_TMUX8" "\$@"
TMUX8
chmod +x "$FAKE_BIN8/tmux"
tmux -L "$SOCKET" new-session -d -s pastefail -c "$TMP" "exec bash '$TMP/counter.sh' '$TMP/enters8'"
sleep 0.3
: > "$TMP/enters8"
if out=$(PATH="$FAKE_BIN8:$PATH" "$SEND" -L "$SOCKET" -t "=pastefail" -m "fixture eight never pastes" 2>"$TMP/e8"); then
no "paste-fail: failed paste must NOT report success" "expected exit 2, got 0 (out=[$out])"
else
rc=$?
enters8=$(cat "$TMP/enters8" 2>/dev/null); enters8=${enters8:-0}
if [ "$rc" -eq 2 ] && grep -qF "paste into" "$TMP/e8" && [ "$enters8" = "0" ]; then
ok "paste-fail: failed paste => exit 2 loud, zero Enters sent"
else
no "paste-fail: failed paste => exit 2 loud, zero Enters" "rc=$rc enters8=$enters8 err=[$(cat "$TMP/e8")]"
fi
fi
# Transport failures must not be upgraded by a stale queued banner or prompt.
mkdir -p "$TMP/faultbin"
cat > "$TMP/faultbin/tmux" <<'FAULTMUX'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$FAULT_LOG"
case " $* " in
*" load-buffer "*) cat >/dev/null; [ "$FAULT_OP" != load-buffer ]; exit $? ;;
*" send-keys "*) [ "$FAULT_OP" != send-keys ]; exit $? ;;
*" capture-pane "*) printf 'Press up to edit queued messages\n \n' ;;
esac
exit 0
FAULTMUX
chmod +x "$TMP/faultbin/tmux"
for op in load-buffer send-keys; do
: > "$TMP/fault-log"
out=$(PATH="$TMP/faultbin:$PATH" FAULT_LOG="$TMP/fault-log" FAULT_OP="$op" \
"$SEND" -L fixture -t '=fault' -m 'transport fault test' 2>"$TMP/fault-err"); rc=$?
if [ "$rc" -eq 2 ] && [ -z "$out" ] && [ "$(grep -c capture-pane "$TMP/fault-log")" -eq 1 ]; then
ok "$op failure refuses after baseline without post-send confirmation"
else
no "$op failure must refuse after baseline only" "rc=$rc out=[$out]"
fi
done
# Historical success-looking text without a current input must fail closed.
for fixture in banner history rules adjacent capture; do
mkdir -p "$TMP/historybin"
cat > "$TMP/historybin/tmux" <<'HISTORYMUX'
#!/usr/bin/env bash
case " $* " in
*" load-buffer "*) cat >/dev/null ;;
*" capture-pane "*)
if [ "$HISTORY_FIXTURE" = banner ]; then
printf 'Press up to edit queued messages\n'
elif [ "$HISTORY_FIXTURE" = capture ]; then
printf ' \n'; exit 1
elif [ "$HISTORY_FIXTURE" = adjacent ]; then
printf '────────\n────────\n~/fixture (main)\n'
elif [ "$HISTORY_FIXTURE" = rules ]; then
printf 'historical output\n────────\nold text\n────────\nmore output; no current editor\n'
else
printf ' old prompt\nsubsequent output without an input box\n'
fi ;;
esac
exit 0
HISTORYMUX
chmod +x "$TMP/historybin/tmux"
out=$(PATH="$TMP/historybin:$PATH" HISTORY_FIXTURE="$fixture" \
"$SEND" -L fixture -t '=history' -m 'new unrelated message' 2>"$TMP/history-err"); rc=$?
if [ "$rc" -eq 2 ] && [ -z "$out" ]; then
ok "historical $fixture cannot confirm a new message"
else
no "historical $fixture must remain unconfirmed" "rc=$rc out=[$out]"
fi
done
# Retained Unicode and whitespace-wrapped messages must remain drafts in C locale.
mkdir -p "$TMP/unicodebin"
cat > "$TMP/unicodebin/tmux" <<'UNICODEMUX'
#!/usr/bin/env bash
case " $* " in
*" load-buffer "*) cat >/dev/null ;;
*" send-keys "*) printf 'Enter\n' >> "$ENTER_LOG" ;;
*" capture-pane "*) printf '────────\n%s\n────────\n~/fixture (main)\n' "$RENDERED_DRAFT" ;;
esac
exit 0
UNICODEMUX
chmod +x "$TMP/unicodebin/tmux"
for shape in unicode wrapped; do
if [ "$shape" = unicode ]; then body='你好世界'; rendered=$'你好\n世界';
else body=$'alpha beta\ngamma delta'; rendered=$'alpha\nbeta gamma\ndelta'; fi
: > "$TMP/draft-enters"
out=$(LC_ALL=C PATH="$TMP/unicodebin:$PATH" RENDERED_DRAFT="$rendered" ENTER_LOG="$TMP/draft-enters" \
"$SEND" -L fixture -t '=unicode' -r 0 -m "$body" 2>"$TMP/unicode-err"); rc=$?
if [ "$rc" -eq 2 ] && [ -z "$out" ] && [ "$(wc -l < "$TMP/draft-enters")" -eq 1 ]; then
ok "retained $shape message refuses with only initial submission key"
else
no "retained $shape message must not confirm" "rc=$rc out=[$out]"
fi
done
mkdir -p "$TMP/transitionbin"
cat > "$TMP/transitionbin/tmux" <<'TRANSITIONMUX'
#!/usr/bin/env bash
case " $* " in
*" load-buffer "*) cat >/dev/null ;;
*" capture-pane "*)
n=$(cat "$CAPTURE_COUNT"); n=$((n + 1)); printf '%s' "$n" > "$CAPTURE_COUNT"
if [ "$TRANSITION" = repeated ] || { [ "$TRANSITION" = new ] && [ "$n" -gt 1 ]; }; then
printf 'unique-correlation-message\n'
fi
printf '────────\n\n────────\n~/fixture (main)\n' ;;
esac
exit 0
TRANSITIONMUX
chmod +x "$TMP/transitionbin/tmux"
for scenario in static repeated new; do
printf '0' > "$TMP/capture-count"
expected=2; [ "$scenario" = new ] && expected=0
out=$(LC_ALL=C PATH="$TMP/transitionbin:$PATH" TRANSITION="$scenario" CAPTURE_COUNT="$TMP/capture-count" \
"$SEND" -L fixture -t '=transition' -m unique-correlation-message 2>"$TMP/transition-err"); rc=$?
if [ "$rc" -eq "$expected" ]; then
ok "$scenario message visibility transition => exit $expected"
else
no "$scenario transition" "rc=$rc expected=$expected out=[$out]"
fi
done
echo "---"
echo "PASS=$PASS FAIL=$FAIL"
[ "$FAIL" -eq 0 ]