From f76a5c96b60da01797debd83728115bba7cf53e8 Mon Sep 17 00:00:00 2001 From: fred Date: Sun, 16 Aug 2026 16:35:36 -0500 Subject: [PATCH] fix(tmux): confirm delivery by draft transition, not by prompt glyph (#1257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send-message.sh located the REPL input box with grep -E '❯|^>|│ >'. That set is Claude Code's box. A pi seat renders a bare U+2500 rule with no glyph, so on every idle pi seat the capture succeeded, the grep matched nothing, status stayed "unconfirmed", and the tool exited 2 "may be UNDELIVERED" with the paste and the Enter both landed. The stderr tells the operator to retry, and that retry is the duplicate delivery reported against the same tool. Confirmation is now runtime-agnostic: our message tail sits on the input line (located by cursor row, no glyph) before Enter and has left it after. That transition is positive proof of submission. Absence still proves nothing, which is the guard the 2026-08 fix was reaching for and got backwards. Two positive checks keep it: - a prompt box that IS locatable and still carries our tail => draft, exit 2. This covers the cursor-row blind spot: a cooked pane whose foreground process never reads stdin echoes the paste through the kernel line discipline and moves the cursor off it on Enter, which by cursor row alone is indistinguishable from a real submit. - no draft ever observed on the input line => unconfirmed, non-zero. Tests, both red-first against the shipping blob d397907: test-send-message-glyph-agnostic.sh (new, 6 fixtures) 4/6 -> 6/6 test-send-message-verdict.sh (fixture 2 reshaped, 2b added) 3/4 -> 4/4 Fixture 2 of the verdict suite asserted exit 2 for a glyphless pane that submits and was labelled "false-positive FIXED". A pi seat is that fixture, so the suite was locking the bug in. It is reshaped deliberately, and the guard it was credited with moves to new fixture 2b (glyphless AND non-submitting, raw/no-echo) so the "never infer delivered from absence" property is tested positively rather than as a side effect. Measured on tmux 3.7b (sb-it-1-dt), 3.5a (fomo-lin), and dragon-lin. Co-authored-by: scooby --- .../framework/tools/tmux/send-message.sh | 106 ++++++++---------- .../tmux/test-send-message-glyph-agnostic.sh | 97 ++++++++++++++++ .../tools/tmux/test-send-message-verdict.sh | 50 +++++++-- 3 files changed, 182 insertions(+), 71 deletions(-) create mode 100755 packages/mosaic/framework/tools/tmux/test-send-message-glyph-agnostic.sh diff --git a/packages/mosaic/framework/tools/tmux/send-message.sh b/packages/mosaic/framework/tools/tmux/send-message.sh index 56144d54..4e68a74e 100755 --- a/packages/mosaic/framework/tools/tmux/send-message.sh +++ b/packages/mosaic/framework/tools/tmux/send-message.sh @@ -32,9 +32,7 @@ # 0 delivered (submitted) or queued (agent busy; will process when free) # 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. +# input prompt could not be located to confirm the message actually landed. # 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. @@ -99,53 +97,34 @@ printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" - # would otherwise accumulate forever. 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. -# -# 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 - glyph_line=$(printf '%s\n' "$pane" | grep -E '❯|^>|│ >' | tail -1) - if [ -n "$glyph_line" ]; 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 - [ "$bottom" -gt "$top" ] || 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 by DRAFT TRANSITION, not by prompt +# glyph. The historical bug was treating ABSENCE of a draft as delivery; the +# 2026-08 fix over-corrected to glyph inference (grep '❯|^>|│ >'), which locates +# only Claude Code's box and false-NEGATIVES every glyphless REPL (pi renders a +# U+2500 rule, no glyph) — a delivered message reported "UNDELIVERED", driving a +# retry that duplicates it. Runtime-agnostic evidence: our message tail sits on +# the INPUT line (located by the cursor row, not a glyph) BEFORE Enter, and has +# LEFT it AFTER — that transition is positive proof of submission and needs no +# glyph. Absence alone still never means delivered: if we never saw our draft on +# the input line we stay UNCONFIRMED (wrong/dead pane), and a draft that never +# leaves the input line stays a DRAFT (exit 2), preserving both historical guards. +_cursor_line() { # echo the pane's current input (cursor) line, glyph-free + local cy line + cy=$("${tmux_cmd[@]}" display-message -p -t "$EFFECTIVE_TARGET" -F '#{cursor_y}' 2>/dev/null) || return 1 + [ -n "$cy" ] || return 1 + "${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null | sed -n "$((cy + 1))p" +} +_draft_on_input() { # true iff our message tail is sitting on the input line now + [ -n "$snippet" ] || return 1 + grep -qF "$snippet" <<<"$(_cursor_line)" } -# 2) Submit, then POSITIVELY confirm submission; flush with another Enter if it is -# still a 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. +# Baseline: after the paste, our draft must be on the input line. This is positive +# proof we are on the right pane and the paste landed — the anchor the transition +# check measures against. +saw_draft=0 +_draft_on_input && saw_draft=1 + status="unconfirmed" for attempt in $(seq 1 $((RETRIES + 1))); do "${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter @@ -155,19 +134,26 @@ for attempt in $(seq 1 $((RETRIES + 1))); do if grep -qF "$QUEUED_RE" <<<"$pane"; then status="queued"; break fi - # If we cannot see the input box, we have NO evidence of submission state — - # stay UNCONFIRMED and retry; never infer delivery. - if ! inputbox=$(locate_input_box "$pane"); then - status="unconfirmed"; continue - fi - # Input box located AND still carrying our tail => unsubmitted draft. Flush + retry. - # (Submitted messages scroll up into history; a draft stays in the box.) - if [ -n "$snippet" ] && grep -qF "$snippet" <<<"$inputbox"; then + # POSITIVE draft evidence from a located prompt box, when one exists. This is the + # cursor-row check's blind spot: a pane in COOKED mode (a plain shell whose + # foreground process never reads stdin) echoes our paste via the kernel line + # discipline and moves the cursor off it on Enter, which is indistinguishable from + # a real submit by cursor row alone. If a prompt box IS locatable and still carries + # our tail, that is affirmative proof the message was not consumed. Absence of a + # glyph is still never used for anything — that inference is the original E7 bug. + promptline=$(printf '%s' "$pane" | grep -E '❯|^>|│ >' | tail -1) + if [ -n "$promptline" ] && [ -n "$snippet" ] && grep -qF "$snippet" <<<"$promptline"; then status="draft"; continue fi - # Input box located AND clear of our tail => positively submitted. This is the - # only path to success besides the queued banner. - status="delivered"; break + if [ "$saw_draft" = 1 ]; then + if _draft_on_input; then + status="draft"; continue # still on the input line => not submitted; flush + retry + fi + status="delivered"; break # left the input line => positively submitted + fi + # No confirmed baseline yet: try to (re)acquire it; never infer delivery from absence. + if _draft_on_input; then saw_draft=1; status="draft"; continue; fi + status="unconfirmed"; continue done [ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; } @@ -176,6 +162,6 @@ case "$status" in delivered) echo "✓ delivered to $TARGET"; exit 0 ;; queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;; draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;; - unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input box not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;; + unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input prompt not locatable after $((RETRIES + 1)) attempts — 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 diff --git a/packages/mosaic/framework/tools/tmux/test-send-message-glyph-agnostic.sh b/packages/mosaic/framework/tools/tmux/test-send-message-glyph-agnostic.sh new file mode 100755 index 00000000..55d0ac13 --- /dev/null +++ b/packages/mosaic/framework/tools/tmux/test-send-message-glyph-agnostic.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Red-first regression test for E7 (#1017 task 2): the confirm-check must bind +# "delivered" to WHETHER THE MESSAGE WAS SUBMITTED, not to which runtime's prompt +# glyph is present. A pi seat renders a U+2500 rule input box with no ❯/^>/│ > +# glyph; send-message.sh:118 locates the box only by glyph, so a genuinely +# delivered message on a glyphless REPL falsely reports exit 2 "may be UNDELIVERED", +# and the operator's rc=2-driven retry duplicates it. +# +# Parameterized on $SEND: RED against the shipping blob (B and D fail), GREEN +# against a candidate patch. No pi; no fake HOME; hermetic throwaway socket. +# +# Submission counting is EXACT and terminal-echo-independent: the fixture message +# is `echo >>SINK`; each real submission appends one line. wc -l SINK == +# number of times the REPL actually executed the send. This does not depend on how +# many times the marker string is painted on screen. +set -u +SEND="${SEND:?set SEND=/path/to/send-message.sh}" +SOCKET="glyphagnostic-$$" +TMP="$(mktemp -d)" +tmux() { command tmux -L "$SOCKET" "$@"; } +cleanup() { command tmux -L "$SOCKET" kill-server 2>/dev/null; rm -rf "$TMP"; } +trap cleanup EXIT +pass=0; fail=0 +ok() { printf 'ok %s\n' "$1"; pass=$((pass+1)); } +no() { printf 'FAIL %s -- %s\n' "$1" "$2"; fail=$((fail+1)); } + +mk() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" "PS1='$2' exec bash --noprofile --norc -i"; sleep 0.5; } +subs() { [ -f "$1" ] && wc -l <"$1" | tr -d ' ' || echo 0; } # exact submission count + +echo "SEND=$SEND tmux $(command tmux -V | awk '{print $2}')" + +# --- A (control): glyph box (❯) that submits => exit 0, exactly one submission. +mk ctl '❯ ' +SINK="$TMP/sink.ctl" +out=$("$SEND" -L "$SOCKET" -t ctl -m "echo x >>'$SINK'" 2>"$TMP/e.ctl"); rc=$?; sleep 0.4 +if [ "$rc" = 0 ] && [ "$(subs "$SINK")" = 1 ]; then + ok "control: ❯-box submits => exit 0, exactly one submission" +else no "control: ❯-box submits => exit 0, one submission" "rc=$rc subs=$(subs "$SINK") err=[$(cat "$TMP/e.ctl")]"; fi + +# --- B (THE false-rc regression): glyphless U+2500 box that SUBMITS. Message lands +# (subs==1) yet shipping reports exit 2. Must be exit 0. +mk sub $'──────── \n' +SINK="$TMP/sink.sub" +out=$("$SEND" -L "$SOCKET" -t sub -m "echo x >>'$SINK'" 2>"$TMP/e.sub"); rc=$?; sleep 0.4 +if [ "$rc" = 0 ] && [ "$(subs "$SINK")" = 1 ]; then + ok "glyphless: U+2500 box that submits => exit 0 (delivered, not 'UNDELIVERED')" +else no "glyphless: U+2500 box that submits => exit 0" \ + "rc=$rc subs=$(subs "$SINK")(delivered=$([ "$(subs "$SINK")" -ge 1 ] && echo yes||echo no)) err=[$(cat "$TMP/e.sub")]"; fi + +# --- D (duplicate arm): operator follows the rc=2 stderr and retries once. On the +# glyphless box, shipping => two submissions (the reported duplicate). The +# property: one logical send => exactly one submission. Same fix closes it. +mk dup $'──────── \n' +SINK="$TMP/sink.dup" +tries=0 +for attempt in 1 2; do + tries=$((tries+1)) + out=$("$SEND" -L "$SOCKET" -t dup -m "echo x >>'$SINK'" 2>/dev/null); rc=$? + sleep 0.4 + [ "$rc" = 0 ] && break # operator stops retrying only when told delivered +done +if [ "$(subs "$SINK")" = 1 ]; then + ok "duplicate: one logical send (rc-driven retry) => exactly one submission (tries=$tries)" +else no "duplicate: one logical send => exactly one submission" "submissions=$(subs "$SINK") tries=$tries"; fi + +# --- E (faithful hung managed TUI, NOT a cooked shell): raw/no-echo, paints nothing. +# A cooked `sleep infinity` echoes the paste via the kernel line discipline and +# false-passes a cursor-row fix that is correct on real seats (measured). So: raw. +mk_rawstuck() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" \ + "bash --noprofile --norc -c 'stty -echo -icanon min 1 time 0 2>/dev/null; exec sleep infinity'"; sleep 0.5; } +mk_rawstuck estuck +SINK="$TMP/sink.estuck" +out=$("$SEND" -L "$SOCKET" -t estuck -r 1 -m "this stuck draft was never submitted" 2>/dev/null); rc=$? +sleep 0.3 +if [ "$rc" != 0 ] && [ "$(subs "$SINK")" = 0 ]; then + ok "raw/no-echo stuck TUI (not submitted) => non-zero (no false delivered)" +else no "raw stuck TUI must NOT report delivered" "rc=$rc subs=$(subs "$SINK")"; fi + +# --- F (busy/queued branch, your BUSY-not-runtime finding): glyphless pane rendering the +# queued banner, never consuming. QUEUED_RE :113 fires before the glyph grep => rc=0. +mk_busy() { tmux new-session -d -s "$1" -x 120 -y 40 -c "$TMP" \ + "bash --noprofile --norc -c 'printf \"Press up to edit queued messages\n\"; exec sleep infinity'"; sleep 0.5; } +mk_busy ebusy +SINK="$TMP/sink.ebusy" +out=$("$SEND" -L "$SOCKET" -t ebusy -m "echo x >>'$SINK'" 2>/dev/null); rc=$?; sleep 0.3 +if [ "$rc" = 0 ]; then + ok "busy/queued-banner glyphless => exit 0 (queued is delivery; runtime owns custody)" +else no "busy/queued-banner must report delivered" "rc=$rc"; fi + +# --- C (historical-bug guard): unresolvable target. No pane ever carried our draft +# => must fail, never infer delivered from absence of a glyph/snippet. +if out=$("$SEND" -L "$SOCKET" -t "nonexistent-$$" -m "echo x >>'$TMP/sink.wrong'" 2>/dev/null); then + no "wrong-pane: unresolvable target must NOT report success" "expected non-zero, got 0" +else ok "wrong-pane: unresolvable target => non-zero (no false delivered)"; fi + +echo "---"; echo "pass=$pass fail=$fail" +[ "$fail" = 0 ] diff --git a/packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh b/packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh index 1186c657..c0ea0b84 100755 --- a/packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh +++ b/packages/mosaic/framework/tools/tmux/test-send-message-verdict.sh @@ -4,10 +4,13 @@ # # 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"). +# 2. DELIVERED — a pane with NO prompt glyph that DOES submit => exit 0. A pi +# seat is this fixture (U+2500 rule, no glyph). Reshaped for +# #1257; see the note at the fixture for why the old exit-2 +# assertion was wrong. +# 2b. UNCONFIRMED— a glyphless pane that never submits (raw/no-echo hung TUI) +# => must fail loud. This carries the historical +# false-positive guard that fixture 2 used to be credited with. # 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 @@ -44,19 +47,44 @@ 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. +# --- Fixture 2: NO prompt glyph, and the pane DOES submit (interactive bash). +# RESHAPED 2026-08-16 (#1257), deliberately. This fixture previously asserted +# exit 2 here and was labelled "false-positive FIXED". That assertion was wrong, +# and locking it in is what kept E7 alive: the pane submits, so "delivered" is +# the truth, and a pi seat — whose input box is a bare U+2500 rule with no glyph +# — IS this fixture. Reporting exit 2 for it told operators a delivered message +# may be undelivered, and the retry that advice invites is the duplicate. +# +# The guard this fixture was reaching for is real and is NOT dropped: "never +# infer delivered from absence" is now enforced positively by fixture 2b below +# (glyphless AND not submitting => must fail) and by fixture 3 (locatable box +# still carrying our tail => draft). Absence alone decides nothing either way. 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])" +out=$("$SEND" -L "$SOCKET" -t "=noglyph" -m "verdict fixture two must fail loud" 2>"$TMP/e2"); rc=$? +if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -qF "✓ delivered"; then + ok "delivered: glyphless pane that submits => exit 0 (runtime-agnostic, E7 FIXED)" +else + no "delivered: glyphless pane that submits => exit 0" "rc=$rc out=[$out] err=[$(cat "$TMP/e2")]" +fi + +# --- Fixture 2b: NO prompt glyph AND never submits — a hung managed TUI holding the +# terminal in raw/no-echo, which is what a stuck agent seat actually is (measured +# on live pi: stty -echo -icanon). Nothing is echoed, nothing is consumed, so +# there is no positive evidence of submission and the tool MUST fail loud. This +# is the historical false-positive guard, kept as a positive test. +tmux -L "$SOCKET" new-session -d -s rawstuck -c "$TMP" \ + 'bash --noprofile --norc -c "stty -echo -icanon min 1 time 0 2>/dev/null; exec sleep infinity"' +sleep 0.3 +if out=$("$SEND" -L "$SOCKET" -t "=rawstuck" -r 1 -m "verdict fixture two-b never submitted" 2>"$TMP/e2b"); then + no "unconfirmed: glyphless hung TUI must NOT report success" "expected non-zero, 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)" + if [ "$rc" -ne 0 ] && grep -qF "could not confirm submission" "$TMP/e2b"; then + ok "unconfirmed: glyphless hung TUI (raw/no-echo) => non-zero + 'could not confirm submission'" else - no "unconfirmed: glyphless pane => exit 2 + stderr" "rc=$rc err=[$(cat "$TMP/e2")]" + no "unconfirmed: glyphless hung TUI => non-zero + stderr" "rc=$rc err=[$(cat "$TMP/e2b")]" fi fi