From e12e26095ff6158e1572855b61d7fe049aeebe33 Mon Sep 17 00:00:00 2001 From: AG Werschky Date: Mon, 3 Aug 2026 07:32:38 -0600 Subject: [PATCH 1/3] fix(send): fail loud when a steer stays queued-unsubmitted Parked lanes (especially grok on herdr) can accept typed text into a numbered composer queue without submitting it. The composer row then reads empty, so fm-send exited 0 while the steer never landed. After each Enter that would report delivery, scan the pane for a numbered `#N [fm-from-firstmate]` item. Retry Enter only within the existing budget; if the item remains, report queued-unsubmitted so fm-send exits non-zero. Preserve the opencode busy-queue exception. --- bin/backends/herdr.sh | 56 +++++++++++++++---- bin/fm-composer-lib.sh | 21 ++++++++ bin/fm-send.sh | 14 ++++- bin/fm-tmux-lib.sh | 53 +++++++++++++++++- tests/fm-backend-herdr.test.sh | 89 +++++++++++++++++++++++++++++-- tests/fm-composer-lib.test.sh | 13 +++++ tests/fm-send-strict.test.sh | 47 ++++++++++++++++ tests/fm-tmux-submit-busy.test.sh | 69 ++++++++++++++++++++++++ 8 files changed, 344 insertions(+), 18 deletions(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index cb677be0cf..4f0955f349 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -2681,10 +2681,25 @@ EOF # re-invokes this function from scratch with the same text after seeing # an error, which is a human/escalation decision, not an automatic # retry). -# Echoes empty|pending|unknown|send-failed, a subset of the proof-carrying -# submit vocabulary. Empty means confirmed submitted for every backend; how -# each backend confirms it is an internal decision, and herdr's is no longer -# literally "the composer read empty". +# Echoes empty|pending|queued-unsubmitted|unknown|send-failed, a subset of the +# proof-carrying submit vocabulary. Empty means confirmed submitted for every +# backend; how each backend confirms it is an internal decision, and herdr's is +# no longer literally "the composer read empty". +# +# Queued-unsubmitted (parked lane): when a harness accepts typed text into a +# numbered composer queue (`#N [fm-from-firstmate] ...`) without starting a turn, +# the composer row can read empty (and a pre-existing working agent_status can +# never prove this Enter landed). After any candidate success (idle-baseline +# busy, or non-idle composer empty), a pane scan for that positive queue marker +# keeps retrying Enter only; exhaustion reports `queued-unsubmitted` so +# fm-send exits non-zero instead of claiming delivery. +fm_backend_herdr_has_queued_unsubmitted() { # + local cap + # Generous enough to cover queue rows sitting just above the composer. + cap=$(fm_backend_herdr_capture "$1" "${FM_BACKEND_HERDR_COMPOSER_LINES:-20}" 2>/dev/null) || return 1 + printf '%s\n' "$cap" | fm_composer_has_queued_unsubmitted +} + fm_backend_herdr_send_text_submit() { # local target=$1 text=$2 retries=$3 sleep_s=$4 settle=$5 i=0 verdict baseline confirm_sleep fm_backend_herdr_parse_target "$target" || { printf 'unknown'; return 0; } @@ -2698,17 +2713,38 @@ fm_backend_herdr_send_text_submit() { # if [ "$baseline" = idle ]; then verdict=$(fm_backend_herdr_wait_for_working "$FM_BACKEND_HERDR_SESSION" "$FM_BACKEND_HERDR_PANE" \ "$confirm_sleep" "$FM_BACKEND_HERDR_SUBMIT_POLLS") + case "$verdict" in + busy) verdict=empty ;; + idle) verdict=pending ;; + unknown) printf 'unknown'; return 0 ;; + esac else sleep "$sleep_s" verdict=$(fm_backend_herdr_composer_state "$target") + case "$verdict" in + empty) verdict=empty ;; + unknown) printf 'unknown'; return 0 ;; + *) verdict=pending ;; + esac + fi + if [ "$verdict" = empty ]; then + # Positive queue proof overrides empty: the steer is still undelivered. + if fm_backend_herdr_has_queued_unsubmitted "$target"; then + verdict=pending + else + printf 'empty' + return 0 + fi fi - case "$verdict" in - busy) printf 'empty'; return 0 ;; - empty) printf 'empty'; return 0 ;; - unknown) printf 'unknown'; return 0 ;; - esac i=$((i + 1)) - [ "$i" -lt "$retries" ] || { printf 'pending'; return 0; } + [ "$i" -lt "$retries" ] || { + if fm_backend_herdr_has_queued_unsubmitted "$target"; then + printf 'queued-unsubmitted' + else + printf 'pending' + fi + return 0 + } done } diff --git a/bin/fm-composer-lib.sh b/bin/fm-composer-lib.sh index 6e2509ec2c..94bd4c556a 100644 --- a/bin/fm-composer-lib.sh +++ b/bin/fm-composer-lib.sh @@ -180,6 +180,27 @@ fm_composer_idle_matches() { esac } +# fm_composer_has_queued_unsubmitted: positive proof that a typed firstmate +# steer is still sitting in a harness's numbered composer queue rather than +# having been submitted. Reads plain pane text on stdin. +# +# Observed shape (grok on herdr, parked lane holding a foregrounded wait): +# messages land as numbered pending items above an empty composer, e.g. +# #1 [fm-from-firstmate]corr=... +# and stay there until a later Enter actually submits them. The composer row +# itself can read empty while the queue still holds the instruction, so a +# submit path that only checks the composer (or a pre-existing busy agent +# status) would falsely report delivery. Matching this shape is fail-closed +# only on positive proof: absence of the pattern is never treated as delivery +# by this helper alone; callers own the empty/pending/unknown contract. +# Returns 0 when at least one matching item is present, 1 otherwise. +fm_composer_has_queued_unsubmitted() { + # Looser than a full-line anchor so a bordered queue row (│ #1 [fm-from-...] │) + # still matches. The [fm-from-firstmate] label is the live charter marker and + # is distinctive enough not to collide with ordinary transcript text. + grep -qE '#[0-9]+[[:space:]]+\[fm-from-firstmate\]' +} + fm_composer_classify_content() { # [idle_re] [idle_case] [plain_content] local bordered=$1 content=$2 idle_re=${3:-} idle_case=${4:-sensitive} plain_content plain_content=${5:-$content} diff --git a/bin/fm-send.sh b/bin/fm-send.sh index 6755e56c7f..256fd000bf 100755 --- a/bin/fm-send.sh +++ b/bin/fm-send.sh @@ -14,7 +14,12 @@ # retried (Enter only, never retyped) until the target backend confirms a # submit or reports an inconclusive send. If a swallowed Enter is positively # confirmed, fm-send exits NON-ZERO so the caller knows the steer did not land -# instead of silently leaving an unsubmitted instruction. +# instead of silently leaving an unsubmitted instruction. The same loud refusal +# applies when the steer lands as a numbered parked-lane queue item +# (`#N [fm-from-firstmate] ...`) that never leaves the composer queue: the +# submit path retries Enter within the budget and exits non-zero with a +# `queued-unsubmitted` verdict if the item is still present - never exit 0 for +# a positively queued-unsubmitted message. # Submission dispatches through the target's recorded backend; the tmux adapter # shares its composer/submit core with the away-mode daemon via bin/fm-tmux-lib.sh. # Tune with FM_SEND_RETRIES (default 3) / FM_SEND_SLEEP (0.4). @@ -308,6 +313,13 @@ else echo "error: text not sent to $T ($TARGET_BACKEND send failed; tried $RESOLUTION_TRIED)" >&2 exit 1 ;; + queued-unsubmitted) + if [ "$PENDING_REPLY_CREATED" = 1 ] && [ -n "$PENDING_REPLY_CORR" ]; then + fm_pending_reply_discard_undelivered "$STATE" "$PENDING_REPLY_CORR" || true + fi + echo "error: text not submitted to $T (queued-unsubmitted: numbered pending item still in the composer queue; tried $RESOLUTION_TRIED)" >&2 + exit 1 + ;; *) if [ "$PENDING_REPLY_CREATED" = 1 ] && [ -n "$PENDING_REPLY_CORR" ]; then fm_pending_reply_discard_undelivered "$STATE" "$PENDING_REPLY_CORR" || true diff --git a/bin/fm-tmux-lib.sh b/bin/fm-tmux-lib.sh index 577a922669..7d0953c1ce 100755 --- a/bin/fm-tmux-lib.sh +++ b/bin/fm-tmux-lib.sh @@ -45,6 +45,17 @@ # as a known gap in `docs/herdr-backend.md` rather than patched here, so the # tmux adapter does not paper over a herdr-specific shape. # +# Queued-unsubmitted (parked lane / numbered composer queue): a different +# failure shape from the opencode busy-queue exception. Some harnesses (observed +# on grok) accept typed text into a numbered pending queue (`#N [fm-from-firstmate] +# ...`) above an empty composer without submitting it. The composer row can +# therefore read empty while the steer is still undelivered. After each Enter +# that would otherwise report empty, the submit core scans the pane for that +# positive queue marker via fm_composer_has_queued_unsubmitted; a match keeps +# retrying Enter only, and exhaustion reports `queued-unsubmitted` rather than +# empty. This never converts the opencode busy-queue case (text still pending +# inside the composer box, no numbered queue marker). +# # Overrides: FM_COMPOSER_IDLE_RE matches an empty composer after ghost and # structural border stripping. FM_BUSY_REGEX overrides the rendered busy-footer # matching used here. @@ -388,6 +399,15 @@ fm_pane_is_busy() { # [harness] | fm_busy_lines_match "$harness" } +# fm_tmux_pane_has_queued_unsubmitted: 0 when the pane positively shows a +# numbered firstmate queue item that has not been submitted yet. Capture +# failure is not positive proof (returns 1). See fm_composer_has_queued_unsubmitted. +fm_tmux_pane_has_queued_unsubmitted() { # + local pane + pane=$(tmux capture-pane -p -t "$1" -S -40 2>/dev/null) || return 1 + printf '%s\n' "$pane" | fm_composer_has_queued_unsubmitted +} + # fm_tmux_submit_core: type into ONCE, then submit with Enter, # verifying the composer cleared. Retries Enter ONLY — never retypes, because a # swallowed Enter leaves our text in the composer and retyping would duplicate @@ -401,6 +421,10 @@ fm_pane_is_busy() { # [harness] # `empty` so the caller does not re-send), while an idle pane keeps `pending` as # a genuine swallow. Pending-unproven receives the same Enter retry budget but # never reaches this exception. +# Queued-unsubmitted: a numbered `#N [fm-from-firstmate]` item still visible +# after Enter means the steer landed in a parked queue, not that it was +# delivered. That positive proof overrides an empty composer row, keeps +# retrying Enter only, and exhausts as `queued-unsubmitted` (never as empty). fm_tmux_submit_enter_core() { # local target=$1 retries=$2 sleep_s=$3 i=0 state while :; do @@ -409,16 +433,41 @@ fm_tmux_submit_enter_core() { # state=$(fm_tmux_composer_state "$target") case "$state" in pending|pending-unproven) ;; - *) printf '%s' "$state"; return 0 ;; + empty) + # An empty composer is not delivery if the steer is still a numbered + # queue item above it (parked-lane shape). Keep retrying Enter only. + if fm_tmux_pane_has_queued_unsubmitted "$target"; then + state=pending + else + printf 'empty' + return 0 + fi + ;; + *) + # unknown / future states: still refuse empty-style success when the + # numbered queue positively holds an undelivered firstmate steer. + if fm_tmux_pane_has_queued_unsubmitted "$target"; then + state=pending + else + printf '%s' "$state" + return 0 + fi + ;; esac i=$((i + 1)) [ "$i" -lt "$retries" ] || break done + # Retries exhausted. A still-visible numbered queue is a loud undelivered + # state and must never be converted to empty by the busy-queue exception. + if fm_tmux_pane_has_queued_unsubmitted "$target"; then + printf 'queued-unsubmitted' + return 0 + fi if [ "$state" != pending ]; then printf '%s' "$state" return 0 fi - # Retries exhausted, composer still shows proven pending. + # Retries exhausted, composer still shows proven pending (no numbered queue). # If the pane is busy (agent mid-turn), the harness accepted the Enter # and queued the message for processing when the current turn ends. # Treat it as submitted so the caller does not re-send. diff --git a/tests/fm-backend-herdr.test.sh b/tests/fm-backend-herdr.test.sh index 16166ac283..f869b1c69d 100755 --- a/tests/fm-backend-herdr.test.sh +++ b/tests/fm-backend-herdr.test.sh @@ -3080,8 +3080,11 @@ test_send_text_submit_detects_landed_send() { assert_contains "$(cat "$log")" $'\x1f''pane'$'\x1f''send-text'$'\x1f''w1:p2'$'\x1f''hello captain' "send_text_submit did not type the literal text first" enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 1 ] || fail "send_text_submit should not need a second Enter for a plain message with no popup, sent $enter_count Enter(s)" - [ "$(grep -c $'\x1f''pane'$'\x1f''read' "$log")" -eq 0 ] || fail "send_text_submit must never read the composer/pane content for confirmation anymore" - pass "fm_backend_herdr_send_text_submit: reports 'empty' once agent_status reports working after one Enter, without ever reading the composer" + # Idle-baseline delivery still confirms via agent-state; the single pane read + # is only the post-success queued-unsubmitted scan (empty pane = no queue). + [ "$(grep -c $'\x1f''pane'$'\x1f''read' "$log")" -eq 1 ] \ + || fail "send_text_submit should make exactly one post-success queue scan pane read, got $(grep -c $'\x1f''pane'$'\x1f''read' "$log")" + pass "fm_backend_herdr_send_text_submit: reports 'empty' once agent_status reports working after one Enter" } test_send_text_submit_detects_swallowed_enter() { @@ -3155,7 +3158,8 @@ test_send_text_submit_preexisting_working_does_not_false_confirm_swallowed_enter enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") [ "$enter_count" -eq 2 ] || fail "preexisting-working swallowed Enter should retry Enter up to the configured count, sent $enter_count Enter(s)" read_count=$(grep -c $'\x1f''pane'$'\x1f''read' "$log") - [ "$read_count" -eq 2 ] || fail "preexisting-working confirmation should fall back to composer reads, made $read_count read(s)" + # Two composer-state reads during retries plus one final queued-unsubmitted scan. + [ "$read_count" -eq 3 ] || fail "preexisting-working confirmation should fall back to composer reads plus a final queue scan, made $read_count read(s)" pass "fm_backend_herdr_send_text_submit: preexisting working is not accepted as submit proof when the composer still holds the message" } @@ -3172,8 +3176,12 @@ test_send_text_submit_confirms_despite_codex_idle_tip_composer() { out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "reply with just OK" 3 0.01 0.01' "$ROOT" ) [ "$out" = empty ] || fail "send_text_submit should confirm via agent_status alone even for a harness whose idle composer shows dynamic tip text, got '$out'" - [ "$(grep -c $'\x1f''pane'$'\x1f''read' "$log")" -eq 0 ] || fail "send_text_submit must never call 'pane read' - a codex-style dynamic idle-tip composer can never mislead a confirmation path that does not read it" - pass "fm_backend_herdr_send_text_submit: confirms submission via native agent-state alone, immune to a codex-style dynamic idle-tip composer that would have misread as 'pending' under the old composer-based confirmation" + # Delivery still keys off agent-state; the one pane read is only the + # post-success queued-unsubmitted scan and must not reintroduce composer-based + # confirmation (an empty/missing capture never turns a landed turn into pending). + [ "$(grep -c $'\x1f''pane'$'\x1f''read' "$log")" -eq 1 ] \ + || fail "send_text_submit should make exactly one post-success queue scan pane read, got $(grep -c $'\x1f''pane'$'\x1f''read' "$log")" + pass "fm_backend_herdr_send_text_submit: confirms submission via native agent-state, immune to a codex-style dynamic idle-tip composer" } # Companion regression for the pre-injection empty-box guard itself @@ -3231,6 +3239,75 @@ test_send_text_submit_slow_transition_within_one_enter_needs_no_extra_enter() { pass "fm_backend_herdr_send_text_submit: a slow transition landing on a later sample within one Enter's budget is confirmed WITHOUT sending a needless extra Enter" } +# Parked-lane regression: a pre-existing working agent (foregrounded wait) plus +# an empty composer that still shows a numbered `#N [fm-from-firstmate]` queue +# item must NOT report empty. Exhausted Enter retries report queued-unsubmitted. +# +# Counted herdr calls (status --json from target_ready is uncounted by the fake): +# 1 send-text 2 agent-get baseline +# per attempt: send-keys Enter, pane-read (composer_state), pane-read (queue scan) +# final exhaust: one more queue-scan pane-read +test_send_text_submit_queued_unsubmitted_fails_loud() { + local dir log resp fb out enter_count queue_pane + dir="$TMP_ROOT/submit-queued-unsubmitted"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" + queue_pane='⠧ Responding… 1h18m +#1 [fm-from-firstmate]corr=deadbeef steer me +╭────────╮ +│ > │ +╰────────╯ +' + printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/2.out" + # attempt 1: composer + queue scan + printf '%s' "$queue_pane" > "$resp/4.out" + printf '%s' "$queue_pane" > "$resp/5.out" + # attempt 2: composer + queue scan + printf '%s' "$queue_pane" > "$resp/7.out" + printf '%s' "$queue_pane" > "$resp/8.out" + # exhaust queue scan + printf '%s' "$queue_pane" > "$resp/9.out" + fb=$(make_herdr_fakebin "$dir") + out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "[fm-from-firstmate]corr=deadbeef steer me" 2 0.01 0.01' "$ROOT" ) + [ "$out" = queued-unsubmitted ] \ + || fail "send_text_submit must report queued-unsubmitted when a numbered queue item survives Enter retries, got '$out'" + enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") + [ "$enter_count" -eq 2 ] || fail "queued-unsubmitted should consume the Enter retry budget, sent $enter_count Enter(s)" + pass "fm_backend_herdr_send_text_submit: parked numbered queue exhausts as queued-unsubmitted (never empty)" +} + +# Same parked-lane shape, but a later Enter actually submits the queue item +# (queue marker gone, empty composer) -> empty after bounded retry. +test_send_text_submit_queued_unsubmitted_retry_succeeds() { + local dir log resp fb out enter_count queue_pane clear_pane + dir="$TMP_ROOT/submit-queued-retry-ok"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" + queue_pane='⠧ Responding… +#1 [fm-from-firstmate]corr=abcd do the work +╭────────╮ +│ > │ +╰────────╯ +' + clear_pane='⠧ Responding… +╭────────╮ +│ > │ +╰────────╯ +' + printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/2.out" + # attempt 1 still queued + printf '%s' "$queue_pane" > "$resp/4.out" + printf '%s' "$queue_pane" > "$resp/5.out" + # attempt 2: queue cleared -> empty success after composer + queue scan + printf '%s' "$clear_pane" > "$resp/7.out" + printf '%s' "$clear_pane" > "$resp/8.out" + fb=$(make_herdr_fakebin "$dir") + out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "[fm-from-firstmate]corr=abcd do the work" 3 0.01 0.01' "$ROOT" ) + [ "$out" = empty ] \ + || fail "send_text_submit should report empty once a later Enter clears the numbered queue, got '$out'" + enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") + [ "$enter_count" -eq 2 ] || fail "queued-unsubmitted retry-success should use a second Enter, sent $enter_count Enter(s)" + pass "fm_backend_herdr_send_text_submit: bounded Enter retry submits a parked numbered queue item" +} + test_send_text_submit_send_failed() { local dir log resp fb out dir="$TMP_ROOT/submit-fail"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" @@ -3993,6 +4070,8 @@ test_send_text_submit_confirms_despite_codex_idle_tip_composer test_composer_state_codex_dynamic_idle_tip_reads_empty_when_faint test_composer_state_guard_still_refuses_real_pending_text_after_submit_confirmation_change test_send_text_submit_slow_transition_within_one_enter_needs_no_extra_enter +test_send_text_submit_queued_unsubmitted_fails_loud +test_send_text_submit_queued_unsubmitted_retry_succeeds test_send_text_submit_send_failed test_send_text_submit_unknown_on_capture_failure test_dispatch_routes_herdr_backend diff --git a/tests/fm-composer-lib.test.sh b/tests/fm-composer-lib.test.sh index cf93cf36a0..5ba6f461aa 100755 --- a/tests/fm-composer-lib.test.sh +++ b/tests/fm-composer-lib.test.sh @@ -125,6 +125,18 @@ test_real_text_is_pending() { pass "fm_composer_classify_content: real unsubmitted text reads pending (including a popup argument-hint fill)" } +test_queued_unsubmitted_detector() { + printf '#1 [fm-from-firstmate]corr=abc do the work\n' | fm_composer_has_queued_unsubmitted \ + || fail "plain numbered firstmate queue item should match" + printf '│ #2 [fm-from-firstmate] more\n' | fm_composer_has_queued_unsubmitted \ + || fail "bordered numbered firstmate queue item should match" + printf 'just a transcript line about #1 something\n' | fm_composer_has_queued_unsubmitted \ + && fail "unrelated #N text without the firstmate marker must not match" + printf '╭─────╮\n│ > │\n╰─────╯\n' | fm_composer_has_queued_unsubmitted \ + && fail "an empty composer without a queue item must not match" + pass "fm_composer_has_queued_unsubmitted: matches only numbered firstmate queue items" +} + test_bare_shell_glyphs_are_unknown test_stripped_unbordered_content_uses_plain_content test_bare_shell_prompt_with_command_is_not_empty @@ -134,3 +146,4 @@ test_empty_content_is_empty test_idle_placeholder_is_empty test_idle_placeholder_case_mode_is_explicit test_real_text_is_pending +test_queued_unsubmitted_detector diff --git a/tests/fm-send-strict.test.sh b/tests/fm-send-strict.test.sh index 1faf98a0ce..b965167214 100755 --- a/tests/fm-send-strict.test.sh +++ b/tests/fm-send-strict.test.sh @@ -163,9 +163,56 @@ test_healthy_fm_id_send_still_works() { pass "fm-send strict: healthy fm- sends still type once and submit" } +# When the submit path reports queued-unsubmitted (numbered parked queue still +# holding the steer), fm-send must exit non-zero and name that condition. A +# still-queued message must never produce exit 0. +test_queued_unsubmitted_exits_nonzero_with_diagnostic() { + local dir fb home err log rc + dir="$TMP_ROOT/queued-unsubmitted"; mkdir -p "$dir" + fb="$dir/fakebin"; mkdir -p "$fb" + home=$(setup_home queued); err="$dir/send.err"; log="$dir/tmux.log"; : > "$log" + fm_write_meta "$home/state/parked-lane.meta" "window=sess:fm-parked-lane" "kind=ship" "harness=grok" + # capture-pane always shows a numbered firstmate queue item above an empty + # composer so submit exhausts as queued-unsubmitted. + cat > "$fb/tmux" <<'SH' +#!/usr/bin/env bash +set -u +case "${1:-}" in + send-keys) + printf 'send-keys %s\n' "$*" >> "$FM_TMUX_LOG" + exit 0 ;; + display-message) + for a in "$@"; do case "$a" in *cursor_y*) printf '1\n'; exit 0 ;; esac; done + printf '%%1\n'; exit 0 ;; + capture-pane) + printf '#1 [fm-from-firstmate]corr=abc urgent steer\n╭────╮\n│ > │\n╰────╯\n' + exit 0 ;; + list-windows) exit 0 ;; +esac +exit 0 +SH + chmod +x "$fb/tmux" + cat > "$fb/sleep" <<'SH' +#!/usr/bin/env bash +exit 0 +SH + chmod +x "$fb/sleep" + + PATH="$fb:$PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$home" FM_TMUX_LOG="$log" \ + FM_SEND_SETTLE=0 FM_SEND_RETRIES=2 FM_SEND_SLEEP=0.01 \ + "$SEND" parked-lane "urgent steer" >/dev/null 2>"$err"; rc=$? + [ "$rc" -ne 0 ] || fail "queued-unsubmitted send must exit non-zero, got $rc" + assert_contains "$(cat "$err")" "queued-unsubmitted" \ + "fm-send diagnostic must name the queued-unsubmitted condition" + assert_contains "$(cat "$err")" "numbered pending item" \ + "fm-send diagnostic must describe the still-queued state" + pass "fm-send: queued-unsubmitted exits non-zero and names the condition" +} + test_exact_lane_id_send_still_works test_unset_fm_home_fails test_unresolvable_target_does_not_tmux_fallback test_prefixless_herdr_pane_id_fails test_unmatched_single_colon_target_must_exist test_healthy_fm_id_send_still_works +test_queued_unsubmitted_exits_nonzero_with_diagnostic diff --git a/tests/fm-tmux-submit-busy.test.sh b/tests/fm-tmux-submit-busy.test.sh index 2d217ad88b..1963238323 100755 --- a/tests/fm-tmux-submit-busy.test.sh +++ b/tests/fm-tmux-submit-busy.test.sh @@ -265,6 +265,72 @@ test_claude_busy_signature_uses_real_capture_shapes() { pass "fm_pane_is_busy: Claude spinner is scoped, multi-frame, and backward-compatible" } +# Parked-lane numbered queue: empty composer + `#N [fm-from-firstmate]` must +# never report empty. Exhaust reports queued-unsubmitted; a later clear succeeds. +test_queued_unsubmitted_exhausted_fails_loud() { + local dir fakebin composer sent vfile + dir="$TMP_ROOT/queued-unsubmitted-fail" + fakebin=$(make_submit_mock "$dir") + composer="$dir/composer" + sent="$dir/sent.log" + vfile="$dir/verdict" + # Empty composer box with a numbered firstmate queue item still present. + printf '#1 [fm-from-firstmate]corr=abc do the work\n╭─────╮\n│ > │\n╰─────╯\n' > "$composer" + : > "$sent" + # Persist: Enter never clears the queue in this scenario. + touch "$dir/.swallow" + PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$composer" FM_FAKE_SENT="$sent" \ + FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=1 FM_FAKE_PANE_BUSY=0 \ + fm_tmux_submit_enter_core "win" 3 0.05 > "$vfile" 2>/dev/null + [ "$(cat "$vfile")" = queued-unsubmitted ] \ + || fail "numbered queue surviving Enter retries should be queued-unsubmitted, got '$(cat "$vfile")'" + [ "$(grep -c '^Enter$' "$sent" 2>/dev/null || true)" -eq 3 ] \ + || fail "queued-unsubmitted should consume the Enter retry budget" + pass "fm_tmux_submit_enter_core: parked numbered queue exhausts as queued-unsubmitted" +} + +test_queued_unsubmitted_retry_succeeds() { + local dir fakebin composer sent vfile + dir="$TMP_ROOT/queued-unsubmitted-retry" + fakebin=$(make_submit_mock "$dir") + composer="$dir/composer" + sent="$dir/sent.log" + vfile="$dir/verdict" + printf '#1 [fm-from-firstmate]corr=abc do the work\n╭─────╮\n│ > │\n╰─────╯\n' > "$composer" + : > "$sent" + # Swallow only the first Enter; later Enters clear the composer file to empty + # box without the queue marker (mock's default clear path). + touch "$dir/.swallow" + PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$composer" FM_FAKE_SENT="$sent" \ + FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=0 FM_FAKE_PANE_BUSY=0 \ + fm_tmux_submit_enter_core "win" 3 0.05 > "$vfile" 2>/dev/null + [ "$(cat "$vfile")" = empty ] \ + || fail "a later Enter that clears the numbered queue should return empty, got '$(cat "$vfile")'" + [ "$(grep -c '^Enter$' "$sent" 2>/dev/null || true)" -ge 2 ] \ + || fail "queued-unsubmitted retry-success should send more than one Enter" + pass "fm_tmux_submit_enter_core: bounded Enter retry clears a parked numbered queue" +} + +test_queued_unsubmitted_blocks_busy_queue_conversion() { + local dir fakebin composer sent vfile + dir="$TMP_ROOT/queued-blocks-busy" + fakebin=$(make_submit_mock "$dir") + composer="$dir/composer" + sent="$dir/sent.log" + vfile="$dir/verdict" + # Proven pending in composer is the opencode busy-queue shape; a numbered + # queue marker must still win and fail loud rather than convert to empty. + printf '#1 [fm-from-firstmate]corr=abc fix it\n╭────────────╮\n│ > fix it │\n╰────────────╯\n' > "$composer" + : > "$sent" + touch "$dir/.swallow" + PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$composer" FM_FAKE_SENT="$sent" \ + FM_FAKE_SWALLOW="$dir/.swallow" FM_FAKE_PERSIST_SWALLOW=1 FM_FAKE_PANE_BUSY=1 \ + fm_tmux_submit_enter_core "win" 3 0.05 > "$vfile" 2>/dev/null + [ "$(cat "$vfile")" = queued-unsubmitted ] \ + || fail "busy conversion must not hide a numbered queue item, got '$(cat "$vfile")'" + pass "fm_tmux_submit_enter_core: numbered queue overrides opencode busy-queue empty conversion" +} + test_busy_pane_pending_returns_empty test_idle_pane_pending_returns_pending test_busy_pane_composer_clears_first_try @@ -273,3 +339,6 @@ test_busy_pane_unknown_stays_unknown test_busy_pane_ambiguous_pending_retries_without_conversion test_unrecognized_state_skips_busy_conversion test_claude_busy_signature_uses_real_capture_shapes +test_queued_unsubmitted_exhausted_fails_loud +test_queued_unsubmitted_retry_succeeds +test_queued_unsubmitted_blocks_busy_queue_conversion From 5c42df5fa573c42e8e1e41c7d33de9d8f42d7c9a Mon Sep 17 00:00:00 2001 From: AG Werschky Date: Mon, 3 Aug 2026 07:47:55 -0600 Subject: [PATCH 2/3] no-mistakes(review): Captain, fail closed on unreadable queue verification --- bin/backends/herdr.sh | 44 +++++++++++++++--------- bin/fm-tmux-lib.sh | 56 +++++++++++++++++-------------- tests/fm-backend-herdr.test.sh | 42 +++++++++++++++++++++++ tests/fm-tmux-submit-busy.test.sh | 53 ++++++++++++++++++++++++++++- 4 files changed, 154 insertions(+), 41 deletions(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 4f0955f349..77c48a84a4 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -2693,15 +2693,21 @@ EOF # busy, or non-idle composer empty), a pane scan for that positive queue marker # keeps retrying Enter only; exhaustion reports `queued-unsubmitted` so # fm-send exits non-zero instead of claiming delivery. -fm_backend_herdr_has_queued_unsubmitted() { # +fm_backend_herdr_queued_unsubmitted_state() { # -> queued|clear|unknown local cap # Generous enough to cover queue rows sitting just above the composer. - cap=$(fm_backend_herdr_capture "$1" "${FM_BACKEND_HERDR_COMPOSER_LINES:-20}" 2>/dev/null) || return 1 - printf '%s\n' "$cap" | fm_composer_has_queued_unsubmitted + cap=$(fm_backend_herdr_capture "$1" "${FM_BACKEND_HERDR_COMPOSER_LINES:-20}" 2>/dev/null) \ + || { printf 'unknown'; return 0; } + if printf '%s\n' "$cap" | fm_composer_has_queued_unsubmitted; then + printf 'queued' + else + printf 'clear' + fi } fm_backend_herdr_send_text_submit() { # local target=$1 text=$2 retries=$3 sleep_s=$4 settle=$5 i=0 verdict baseline confirm_sleep + local queue_state candidate_empty=0 fm_backend_herdr_parse_target "$target" || { printf 'unknown'; return 0; } fm_backend_herdr_send_literal "$target" "$text" || { printf 'send-failed'; return 0; } sleep "$settle" @@ -2727,22 +2733,30 @@ fm_backend_herdr_send_text_submit() { # *) verdict=pending ;; esac fi + candidate_empty=0 if [ "$verdict" = empty ]; then - # Positive queue proof overrides empty: the steer is still undelivered. - if fm_backend_herdr_has_queued_unsubmitted "$target"; then - verdict=pending - else - printf 'empty' - return 0 - fi + candidate_empty=1 + queue_state=$(fm_backend_herdr_queued_unsubmitted_state "$target") + case "$queue_state" in + queued) verdict=pending ;; + clear) printf 'empty'; return 0 ;; + *) printf 'unknown'; return 0 ;; + esac fi i=$((i + 1)) [ "$i" -lt "$retries" ] || { - if fm_backend_herdr_has_queued_unsubmitted "$target"; then - printf 'queued-unsubmitted' - else - printf 'pending' - fi + queue_state=$(fm_backend_herdr_queued_unsubmitted_state "$target") + case "$queue_state" in + queued) printf 'queued-unsubmitted' ;; + clear) + if [ "$candidate_empty" = 1 ]; then + printf 'empty' + else + printf 'pending' + fi + ;; + *) printf 'unknown' ;; + esac return 0 } done diff --git a/bin/fm-tmux-lib.sh b/bin/fm-tmux-lib.sh index 7d0953c1ce..6edcbd0a96 100755 --- a/bin/fm-tmux-lib.sh +++ b/bin/fm-tmux-lib.sh @@ -399,13 +399,16 @@ fm_pane_is_busy() { # [harness] | fm_busy_lines_match "$harness" } -# fm_tmux_pane_has_queued_unsubmitted: 0 when the pane positively shows a -# numbered firstmate queue item that has not been submitted yet. Capture -# failure is not positive proof (returns 1). See fm_composer_has_queued_unsubmitted. -fm_tmux_pane_has_queued_unsubmitted() { # +# fm_tmux_queued_unsubmitted_state: classify a numbered firstmate queue scan. +fm_tmux_queued_unsubmitted_state() { # -> queued|clear|unknown local pane - pane=$(tmux capture-pane -p -t "$1" -S -40 2>/dev/null) || return 1 - printf '%s\n' "$pane" | fm_composer_has_queued_unsubmitted + pane=$(tmux capture-pane -p -t "$1" -S -40 2>/dev/null) \ + || { printf 'unknown'; return 0; } + if printf '%s\n' "$pane" | fm_composer_has_queued_unsubmitted; then + printf 'queued' + else + printf 'clear' + fi } # fm_tmux_submit_core: type into ONCE, then submit with Enter, @@ -426,32 +429,30 @@ fm_tmux_pane_has_queued_unsubmitted() { # # delivered. That positive proof overrides an empty composer row, keeps # retrying Enter only, and exhausts as `queued-unsubmitted` (never as empty). fm_tmux_submit_enter_core() { # - local target=$1 retries=$2 sleep_s=$3 i=0 state + local target=$1 retries=$2 sleep_s=$3 i=0 state queue_state candidate_empty=0 while :; do tmux send-keys -t "$target" Enter 2>/dev/null || true sleep "$sleep_s" state=$(fm_tmux_composer_state "$target") + candidate_empty=0 case "$state" in pending|pending-unproven) ;; empty) - # An empty composer is not delivery if the steer is still a numbered - # queue item above it (parked-lane shape). Keep retrying Enter only. - if fm_tmux_pane_has_queued_unsubmitted "$target"; then - state=pending - else - printf 'empty' - return 0 - fi + candidate_empty=1 + queue_state=$(fm_tmux_queued_unsubmitted_state "$target") + case "$queue_state" in + queued) state=pending ;; + clear) printf 'empty'; return 0 ;; + *) printf 'unknown'; return 0 ;; + esac ;; *) - # unknown / future states: still refuse empty-style success when the - # numbered queue positively holds an undelivered firstmate steer. - if fm_tmux_pane_has_queued_unsubmitted "$target"; then - state=pending - else - printf '%s' "$state" - return 0 - fi + queue_state=$(fm_tmux_queued_unsubmitted_state "$target") + case "$queue_state" in + queued) state=pending ;; + clear) printf '%s' "$state"; return 0 ;; + *) printf 'unknown'; return 0 ;; + esac ;; esac i=$((i + 1)) @@ -459,8 +460,13 @@ fm_tmux_submit_enter_core() { # done # Retries exhausted. A still-visible numbered queue is a loud undelivered # state and must never be converted to empty by the busy-queue exception. - if fm_tmux_pane_has_queued_unsubmitted "$target"; then - printf 'queued-unsubmitted' + queue_state=$(fm_tmux_queued_unsubmitted_state "$target") + case "$queue_state" in + queued) printf 'queued-unsubmitted'; return 0 ;; + unknown) printf 'unknown'; return 0 ;; + esac + if [ "$candidate_empty" = 1 ]; then + printf 'empty' return 0 fi if [ "$state" != pending ]; then diff --git a/tests/fm-backend-herdr.test.sh b/tests/fm-backend-herdr.test.sh index f869b1c69d..975718aa1e 100755 --- a/tests/fm-backend-herdr.test.sh +++ b/tests/fm-backend-herdr.test.sh @@ -3308,6 +3308,46 @@ test_send_text_submit_queued_unsubmitted_retry_succeeds() { pass "fm_backend_herdr_send_text_submit: bounded Enter retry submits a parked numbered queue item" } +test_send_text_submit_unreadable_queue_probe_never_reports_delivered() { + local dir log resp fb out enter_count + dir="$TMP_ROOT/submit-queue-probe-unreadable"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" + printf '{"result":{"agent":{"agent_status":"idle"}}}\n' > "$resp/2.out" + printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/4.out" + printf '1\n' > "$resp/5.exit" + fb=$(make_herdr_fakebin "$dir") + out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" FM_BACKEND_HERDR_SUBMIT_POLLS=1 \ + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "hello captain" 3 0.01 0.01' "$ROOT" ) + [ "$out" = unknown ] \ + || fail "an unreadable post-success queue probe must return unknown, got '$out'" + enter_count=$(grep -c $'\x1f''pane'$'\x1f''send-keys'$'\x1f''w1:p2'$'\x1f''enter' "$log") + [ "$enter_count" -eq 1 ] || fail "an unreadable queue probe must not send another Enter, sent $enter_count" + pass "fm_backend_herdr_send_text_submit: unreadable queue verification never reports delivered" +} + +test_send_text_submit_queue_clear_on_exhaustion_rescan_reports_empty() { + local dir log resp fb out queue_pane clear_pane + dir="$TMP_ROOT/submit-queue-clear-on-rescan"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" + queue_pane='#1 [fm-from-firstmate]corr=abcd do the work +╭────────╮ +│ > │ +╰────────╯ +' + clear_pane='╭────────╮ +│ > │ +╰────────╯ +' + printf '{"result":{"agent":{"agent_status":"working"}}}\n' > "$resp/2.out" + printf '%s' "$queue_pane" > "$resp/4.out" + printf '%s' "$queue_pane" > "$resp/5.out" + printf '%s' "$clear_pane" > "$resp/6.out" + fb=$(make_herdr_fakebin "$dir") + out=$( PATH="$fb:$PATH" FM_HERDR_LOG="$log" FM_HERDR_RESPONSES="$resp" \ + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_send_text_submit default:w1:p2 "[fm-from-firstmate]corr=abcd do the work" 1 0.01 0.01' "$ROOT" ) + [ "$out" = empty ] \ + || fail "a queue cleared on the exhaustion rescan must return empty, got '$out'" + pass "fm_backend_herdr_send_text_submit: exhaustion rescan preserves candidate-empty delivery proof" +} + test_send_text_submit_send_failed() { local dir log resp fb out dir="$TMP_ROOT/submit-fail"; mkdir -p "$dir/responses"; log="$dir/log"; resp="$dir/responses"; : > "$log" @@ -4072,6 +4112,8 @@ test_composer_state_guard_still_refuses_real_pending_text_after_submit_confirmat test_send_text_submit_slow_transition_within_one_enter_needs_no_extra_enter test_send_text_submit_queued_unsubmitted_fails_loud test_send_text_submit_queued_unsubmitted_retry_succeeds +test_send_text_submit_unreadable_queue_probe_never_reports_delivered +test_send_text_submit_queue_clear_on_exhaustion_rescan_reports_empty test_send_text_submit_send_failed test_send_text_submit_unknown_on_capture_failure test_dispatch_routes_herdr_backend diff --git a/tests/fm-tmux-submit-busy.test.sh b/tests/fm-tmux-submit-busy.test.sh index 1963238323..5713954029 100755 --- a/tests/fm-tmux-submit-busy.test.sh +++ b/tests/fm-tmux-submit-busy.test.sh @@ -30,7 +30,21 @@ case "${1:-}" in case "$a" in *cursor_y*) printf '1\n'; exit 0 ;; esac done exit 0 ;; - capture-pane) cat "$COMPOSER" 2>/dev/null; exit 0 ;; + capture-pane) + queue_probe=1 + for a in "$@"; do [ "$a" = -e ] && queue_probe=0; done + if [ "$queue_probe" = 1 ] && [ "${FM_FAKE_QUEUE_PROBE_FAIL:-0}" = 1 ]; then + exit 1 + fi + if [ "$queue_probe" = 1 ] && [ -n "${FM_FAKE_QUEUE_CLEAR_ON_RESCAN:-}" ]; then + count=$(( $(cat "$FM_FAKE_QUEUE_CLEAR_ON_RESCAN" 2>/dev/null || echo 0) + 1 )) + printf '%s\n' "$count" > "$FM_FAKE_QUEUE_CLEAR_ON_RESCAN" + if [ "$count" -gt 1 ]; then + printf '╭─────╮\n│ > │\n╰─────╯\n' + exit 0 + fi + fi + cat "$COMPOSER" 2>/dev/null; exit 0 ;; send-keys) shift; is_enter=0 while [ "$#" -gt 0 ]; do @@ -331,6 +345,41 @@ test_queued_unsubmitted_blocks_busy_queue_conversion() { pass "fm_tmux_submit_enter_core: numbered queue overrides opencode busy-queue empty conversion" } +test_unreadable_queue_probe_never_reports_delivered() { + local dir fakebin composer sent vfile + dir="$TMP_ROOT/queued-probe-unreadable" + fakebin=$(make_submit_mock "$dir") + composer="$dir/composer" + sent="$dir/sent.log" + vfile="$dir/verdict" + printf '╭─────╮\n│ > │\n╰─────╯\n' > "$composer" + : > "$sent" + PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$composer" FM_FAKE_SENT="$sent" \ + FM_FAKE_QUEUE_PROBE_FAIL=1 FM_FAKE_PANE_BUSY=0 \ + fm_tmux_submit_enter_core "win" 3 0.05 > "$vfile" 2>/dev/null + [ "$(cat "$vfile")" = unknown ] \ + || fail "an unreadable queue probe must return unknown, got '$(cat "$vfile")'" + pass "fm_tmux_submit_enter_core: unreadable queue verification never reports delivered" +} + +test_queue_clear_on_exhaustion_rescan_reports_empty() { + local dir fakebin composer sent vfile probe_count + dir="$TMP_ROOT/queued-clear-on-rescan" + fakebin=$(make_submit_mock "$dir") + composer="$dir/composer" + sent="$dir/sent.log" + vfile="$dir/verdict" + probe_count="$dir/probe-count" + printf '#1 [fm-from-firstmate]corr=abc do the work\n╭─────╮\n│ > │\n╰─────╯\n' > "$composer" + : > "$sent" + PATH="$fakebin:$PATH" FM_FAKE_COMPOSER="$composer" FM_FAKE_SENT="$sent" \ + FM_FAKE_QUEUE_CLEAR_ON_RESCAN="$probe_count" FM_FAKE_PANE_BUSY=0 \ + fm_tmux_submit_enter_core "win" 1 0.05 > "$vfile" 2>/dev/null + [ "$(cat "$vfile")" = empty ] \ + || fail "a queue cleared on the exhaustion rescan must return empty, got '$(cat "$vfile")'" + pass "fm_tmux_submit_enter_core: exhaustion rescan preserves candidate-empty delivery proof" +} + test_busy_pane_pending_returns_empty test_idle_pane_pending_returns_pending test_busy_pane_composer_clears_first_try @@ -342,3 +391,5 @@ test_claude_busy_signature_uses_real_capture_shapes test_queued_unsubmitted_exhausted_fails_loud test_queued_unsubmitted_retry_succeeds test_queued_unsubmitted_blocks_busy_queue_conversion +test_unreadable_queue_probe_never_reports_delivered +test_queue_clear_on_exhaustion_rescan_reports_empty From 7da15135787fa195501cb7dd60d618fb7a46dea7 Mon Sep 17 00:00:00 2001 From: AG Werschky Date: Mon, 3 Aug 2026 07:58:23 -0600 Subject: [PATCH 3/3] no-mistakes(document): Document queued-unsubmitted delivery failures --- docs/herdr-backend.md | 3 +++ docs/tmux-backend.md | 9 +++++---- docs/verification/runtime-backends.md | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/herdr-backend.md b/docs/herdr-backend.md index 83d527c588..5b7a1f9061 100644 --- a/docs/herdr-backend.md +++ b/docs/herdr-backend.md @@ -195,6 +195,9 @@ Text is typed once; only Enter is retried. On an idle or done native baseline, submit confirmation waits for `working` or `blocked` across a bounded polling window. On an already active or unreadable baseline, it falls back to conservative composer clearance. +A would-be success on either path is also checked for a numbered `#N [fm-from-firstmate]` item still in the pane's composer queue. +That queued-unsubmitted proof keeps the Enter-only retry active and, on exhaustion, makes `fm-send.sh` exit non-zero with a named `queued-unsubmitted` diagnostic. +A failed queue read reports unknown rather than assuming delivery. A fully unreadable target stops retrying and reports unknown. The poll density bounds the residual possibility of an extremely fast complete turn; a missed transition can cause only a redundant Enter on an empty composer, never duplicate message text. diff --git a/docs/tmux-backend.md b/docs/tmux-backend.md index 936e0e7b47..cde525fe26 100644 --- a/docs/tmux-backend.md +++ b/docs/tmux-backend.md @@ -66,16 +66,17 @@ The submit acknowledgement and away-mode supervisor-pane busy guard below still The supervisor guard selects only the detected primary harness's signature rather than a global union of vendor patterns. `bin/fm-tmux-lib.sh` owns exact type-and-submit mechanics. -It types a message once and retries Enter only until the composer clears. -Only a proven empty composer is a positive delivery acknowledgement. +It types a message once and retries Enter only until the backend has a positive delivery acknowledgement. +A proven empty composer is not enough while a numbered `#N [fm-from-firstmate]` item remains in the pane's composer queue. +That queued-unsubmitted proof keeps the Enter-only retry active and, on exhaustion, makes `fm-send.sh` exit non-zero with a named `queued-unsubmitted` diagnostic. Text left in established structure remains `pending`, text in ambiguous structure remains unproven, and unreadable or unsafe state remains unknown. `fm-send.sh` reports every unconfirmed verdict as a failure instead of retyping or assuming delivery. OpenCode 1.18.4 has one busy-queue exception. While OpenCode is mid-turn, Enter queues the message but leaves its text visible until the turn completes. After the normal retry budget, only structurally proven pending text in a provably busy pane is accepted as queued, while an idle pane remains `pending` as a genuine swallowed Enter. -Ambiguous pending text never receives the busy-queue conversion. -`tests/fm-tmux-submit-busy.test.sh` covers busy and idle panes with proven, ambiguous, and cleared composers. +Ambiguous pending text never receives the busy-queue conversion, and a numbered queued-unsubmitted item always overrides it. +`tests/fm-tmux-submit-busy.test.sh` covers busy and idle panes with proven, ambiguous, cleared, and numbered queued-unsubmitted states. ## Limits and regression entry points diff --git a/docs/verification/runtime-backends.md b/docs/verification/runtime-backends.md index 84cc500746..b6d9d2beb8 100644 --- a/docs/verification/runtime-backends.md +++ b/docs/verification/runtime-backends.md @@ -86,8 +86,8 @@ tests/fm-kimi-harness.test.sh tests/fm-tmux-submit-busy.test.sh ``` -Expected structural matrix: real text on any content row is pending; all-empty complete boxes are empty; unreadable, incomplete, or unsafe boxes are unknown; and non-bordered panes retain cursor-row compatibility. -Expected submit matrix: proven pending plus busy is accepted as queued; proven pending plus idle remains pending; ambiguous pending is never converted by the busy exception; and only a proven empty composer succeeds directly. +The [tmux backend guide](../tmux-backend.md#current-behavior-and-safety) owns the current structural and submit semantics. +The suite exercises empty, pending, ambiguous, busy, idle, numbered queue retry, queue-clear, retry-exhaustion, and unreadable queue cases against that contract. ### Cleanup endpoint identity @@ -350,6 +350,8 @@ Real captures verified these active distinctions: - A bare shell prompt has no safe agent-composer container and is unknown. `tests/fm-composer-ghost.test.sh`, `tests/fm-composer-lib.test.sh`, and the Herdr composer cases pin the exact captured ANSI bytes. +The [Herdr backend guide](../herdr-backend.md#current-transport-behavior) owns the current submit semantics. +`tests/fm-backend-herdr.test.sh` and `tests/fm-send-strict.test.sh` exercise numbered queue retry, queue-clear, retry-exhaustion, unreadable queue, and the caller-facing failure diagnostic against that contract. The U+2063 operational and routed-request separators were exercised through a real Pi-on-Herdr path; the byte-exact active regression is: ```sh