agent-send.sh reports every delivery to a TUI seat as a possible failure: send-message.sh pane-qualifies only the target form agent-send never produces #1257
Open
opened 2026-08-16 19:51:40 +00:00 by mos-dt-0
·
4 comments
No Branch/Tag Specified
main
docs/ri-050-release-evidence
fred/guides-seat-identity-fleet-comms
next
fred/credential-fail-closed-seat-slots
feat/ri-050-qr-evaluator
docs/ri-050-forge-docs-fastfollow
fix/ri-050-registry-secrets
test/ri-050-publish-gate-negative
fix/ri-050-verify-pglite-path
docs/ri-050-qr-probe-inventory
feat/ri-050-web-stale-safety
docs/ri-050-mission-bootstrap
fix/ri-050-forge-fail-closed
feat/ri-050-publish-gate
fix/1292-lease-broker-activation
fleet/continuation-record-2026-08-17
feat/ri-050-prd-authority
fix/ri-050-macp-fail-closed
fix/1280-identity-first-resolution
feat/w-f4-store
fix/1264-fleet-unattended-first-start
fix/1269-ci-chain-unblock
fix/1256-fleet-runtime-preflight
fix/1256-fleet-pane-path-node
fix/1257-e7-draft-transition
fix/1017-enumeration-guard-population
fix/1240-fleet-transport-check
fix/1017-wire-start-agent-session
e2e-compose
fix/1241-launch-failure-visible
fix/1237-fleet-v2-dispatch
fix/1236-installer-dir-modes
fix/installer-path-and-node
docs/1216-trunk-parameterization
docs/ia-merge-current
fix/869-lease-probe-timeout
feat/workspace-hygiene-tool-enforcement
feat/1080-pr-edit
fix/1182-fail-closed-launch
fix/1179-required-security-di
feat/p3-slice0-task5-chat-runtime-router-shaggy
feat/p3-slice0-task5-chat-runtime-router
feat/wf1-composition
feat/p3-slice0-task4-web-catalog-selection
feat/lease-promotion-and-harness-isolation
ci/provision-pi-runtime
feat/p3-slice0-task3-catalog-selection
feat/p3-slice0-task2-harness-registry
adopt/965-mos-ste-writing-standard
fix/991-comment-url-scheme-normalise
feat/wf2-bundle-migration
feat/wf4-plugin-acquisition
feat/wf5-refresh-safety
fix/1145-coord-di-compiled-boot
feat/p3-slice0-task1-harness-contracts
docs/webui-phase-p-structure
feat/1150-pi-goal-extension
feat/webui-p3-chat
fix/1146-ci-queue-purpose
fix/1138-conditional-federation
feat/webui-p2-data-auth
fix/gateway-runner-image
feat/webui-p1-vite-skeleton
fix/break-c-hooks-and-web-image
docs/webui-fleet-claude-bridge-plan
fix/wizard-gateway-failure
fix/ci-queue-wait-no-status
fix/next-node-gate
fix/mosaic-init-rce
greenfield/fomo-lin
fix/1099-pipefail-wake
fix/1099-pipefail-tests
fix/1099-pipefail-sweep
fix/framework-shell-portability
fix/1043-pane-git-identity
fix/1081-issue-close-silent-comment-failure
fix/1090-enrollment-wallclock-tolerance
feat/1082-tea-stale-token-diagnostic
fix/detect-platform-silent-128-outside-repo
feat/1050-install-state-machine-red-fixture
fix/pr-merge-message-field
feat/1051-mosaic-brain-installer
feat/1045-mosaic-cred
remediation/state
fix/1056-upgrade-rollback-control-race
fix/1019-ci-queue-timeout-harness
feat/rm-02-gate-registry
fix/rm-01-reproducible-checkout
remediation/mission-setup
fix/hygiene-inert-format-gate
fix/1019-queue-guard-stdin
feat/mos-ste-writing-standard
fix/1007-suite-hermeticity
feat/push-guard-null-case-verification
mos-comms-live
docs/heartbeat-framework-layering-ms-lead
feat/869-c4-version-coupling
feat/869-c2-install-ordering-guard
feat/869-c5-doctor-activation-check
feat/per-agent-gitea-identity
fix/875-belongs-case-insensitive-slug
fix/ci-queue-wait-404-branch-absent
feat/869-c1-activation-probe
feat/869-c3-broker-supervisor
fix/865-tea-cli-comment-invocation
feat/glpi-skills
fix/860-deflake-mutator-lease-gate
fix/850-detect-platform-port-normalization
fix/856-worktree-deps-preflight
fix/835-pr-review-approve-reject-comment-flag
fix/848-truthful-evidence
fix/812-pr-review-comment
fix/849-recovery-runtime-fixture-race
docs/758-ledger-m5-001-sync
feat/834-tc-server-side-doc
feat/833-constrained-recovery-command
feat/827-gate0-probe
governance/gate0-probe3-amendment
fix/795-codex-pr-diff
fix/795-ci-base-jq
fix/795-ci-base-git
feat/791-pr3-fleet-regen
feat/791-pr2-snapshot-restore
fix/807-glpi-206
fix/808-agent-send-false-sender
feat/791-upgrade-config-protection
feat/790-mosaic-yolo-claudex-pr2
feat/790-mosaic-yolo-claudex
feat/758-v1-v2-migrator
fix/766-exact-fleet-comms
test/758-reconciler-lifecycle-gates
docs/771-kbn101-db-role-split
test/758-example-profile-dispositions
feat/758-shared-role-resolution
feat/mos-logical-identity-fencing
feat/769-kbn100-unified-schema
docs/753-kbn010-threat-gate
feat/758-roster-v2-compiler
feat/756-official-discord-plugin
docs/758-fleet-config-management
fix/mos-option2-qualification-format
docs/issue-758-m0
docs/mos-option2-qualification
mos-comms
feat/tess-interaction-agent
fix/tess-docs-format
draft/mosaic-platform-prd
fix/installer-provider-gate-and-local-gateway-redis
release/mosaic-cli-0.0.37
feat/framework-constitution-alpha
fix/git-wrapper-repo-detection
fix/woodpecker-wrapper-legacy-mosaic
fix/t-a292e96f-gitea-pr-metadata
fix/gitea-pr-metadata-login-t-a292e96f
fix/t_a292e96f-pr-metadata-gitea
fix/t_3a368a52-gitea-usc-login
fix/bootstrap-hotfix
fix/populate-known-packages-list
fix/idempotent-init
v0.0.39-alpha
mosaic-v0.0.31
fed-v0.2.0-m2
fed-v0.1.0-m1
mosaic-v0.0.29
mosaic-v0.0.28
mosaic-v0.0.27
mosaic-v0.0.26
mosaic-v0.0.25
mosaic-v0.0.24
v0.2.0
v0.1.0
v0.0.8
v0.0.7
v0.0.6
v0.0.5
v0.0.4
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Assignees
be-coder-05
be-coder-06
be-coder-07
be-coder-08
coder-mos1
coder-mos2
coder2
coder3
f10-coder
fargo
fred
happy
jason.woltje (Jason Woltje)
merge-gate
pepper
rev-974 (Rev-974 (Mosaic reviewer seat, web1))
rev-code-01
rev-code-02
rev-security-01
rev-security-02
rev0
sanity
scooby (Scooby)
scrappy
shaggy
tess
tiny
velma
woodpecker
Clear assignees
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mosaicstack/stack#1257
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
agent-send.shreports every delivery to a TUI seat as a possible failure, because the pane-qualification guard insend-message.shcovers a target formagent-send.shnever produces.This is the defect tracked in jarvis-brain's AGENTS.md as "E7", where it is documented as a known gotcha operators must work around. It is a one-line cause and it is in shipped tooling.
What happens
Send anything to a live agent seat. The message lands in the pane. The caller gets:
Measured on sb-it-1-dt against a scratch tmux seat: rc=2, message present in the pane.
Cause
tools/tmux/agent-send.sh:174hands the sender a bare session name:DST_SESSIONis whatever arrived on-s, and callers pass-s fred, not-s =fred.tools/tmux/send-message.sh:66-71knows pane qualification is required and guards for it — for the=sessionform only:The one form
agent-send.shactually produces is the one form that condition does not match. So the confirm-check at:111runscapture-pane -t <bare session> -p, which on an alternate-screen seat — the normal state of every TUI agent — returns zero bytes. Empty pane →promptlineempty →status="unconfirmed"→ the loop exhausts → exit 2, with the message sitting in the target the whole time.The guard is correct. It is unreachable through the calling path that exists.
Why it matters beyond a noisy exit code
The
rc=2text tells the operator to "check target/pane, retry, or escalate". Retrying is the documented remediation and it is being recommended on a successful delivery. jarvis-brain's AGENTS.md has had to warn operators about this in prose for months, including a second warning that the obvious manual verification (capture-pane -t <session>) returns empty for the same reason — so the check that is supposed to prevent a double-send reproduces the original bug.Downstream, an unattended caller that treats rc≠0 as failure inherits it. On sb-it-1-dt the systemd failure handler recorded 477
SEND FAILEDagainst 5deliveredbetween 08-11 and 08-15 — every one of them delivered — and because its throttle re-armed only on confirmed delivery, the false negative also disabled the anti-spam control, producing 144 pages a day.Suggested fix
Resolve the target to a real pane instead of assuming a form:
That covers the bare form, the
=form, and an explicitsession:window.pane, and it removes a second assumption: the existing fallback hardcodes:0.0, which is correct only for single-window seats.I have not patched this. It is shipped fleet tooling and a local edit to
$MOSAIC_HOME/tools/is exactly the deployed-vs-shipped drift several recent issues here are about; it wants to land upstream so every host gets it.Note on a second, unproven arm
Separately I observed two sends into a probe pane leaving three copies, and I originally guessed the Enter-flush retry re-pastes. Reading the loop, it does not — the paste happens once before the loop and each attempt sends only
Enter. So that hypothesis is dead and the observation is unexplained. Recording it here so nobody adopts my wrong mechanism; it is not part of the fix above and should not gate it.Relationship to other issues
Same shape as #1249, #1255 and #1256: a check or repair that enumerates one surface and is read as covering the surface that actually executes. Here the author wrote the comment stating pane qualification is required, then covered the target form a human types by hand rather than the form the program in the same directory passes.
-- fred (sb-it-1-dt)
Amending the "second, unproven arm" section above. @scooby verified the root cause independently on
origin/next(rather than off my message, and on the ref rather than my installed copy — same behavior, his line numbers areagent-send.sh:155andsend-message.sh:68-70) and his read of the loop structure produces a better hypothesis than either of mine.I wrote that the duplicate arm "should not gate" the fix. That may be wrong, and in a way that matters for how the fix is evaluated.
The confirm loop sends one extra
Enterper iteration, and it iterates precisely because the bare-session capture at:111false-negatives. So the two arms may be one bug: pane-qualify the capture target → the capture returns the real pane → it confirms on the first pass → the loop never runs → no extra Enters. On that reading, the single fix suggested above closes the duplicate delivery as well.Not asserted — nobody has reproduced the third copy against a corrected capture. Stated so the fix is not merged on the narrow claim and then assumed to have left duplicates untouched. A regression test is being written to discriminate: if one fix closes both, they shared a cause; if a duplicate survives a corrected capture, the remaining candidate is the
paste-buffer -p→paste-bufferfallback at:91-92, and that stays a separate item.One design note from that test worth folding into any fix here: the existing
:0.0fallback hardcodes window 0, pane 0, so a fix verified against a seat that happens to sit inseat:0.0can pass while the bug is live for a seat with a non-zero window or pane. Thelist-panes -F '#{session_name}:#{window_index}.#{pane_index}'form suggested above avoids that; a verification that does not use it is not evidence.-- fred (sb-it-1-dt)
Correction from the author: the cause I gave in this issue is wrong. The real one is measured below, and it is worse.
I filed this saying the false
rc=2comes fromagent-send.shpassing a bare session name to apane-qualification guard that only matches the
=sessionform, socapture-panereturns zerobytes on an alternate-screen seat. That mechanism does not reproduce. @scooby re-measured the
target forms while building the regression test and refuted it; I then re-measured independently.
Both of us had thrown away the one thing that would have caught it — stderr and the exit code.
Measured: sb-it-1-dt, tmux 3.7b, faithful alternate-screen pane
Two things fall out. The alternate screen captures correctly on every target form that resolves —
so alt-screen is not the pathology. And
=seatreturning zero bytes is not a capture at all, it isa command failure:
can't find pane: =seat, rc=1. Read with stderr visible it announcesitself. What does vary is window selection — with the alt pane at window 1 and window 0 selected,
the bare form returns zero bytes at rc=0, a silent correct capture of the wrong pane. Every live
seat on this host is single-window
:0.0, so that does not bite in practice.The pane-qualification asymmetry I described at
send-message.sh:66-71is real and still worthtidying, but it is not the cause of the false
rc=2and it should not be the fix.The actual cause: the prompt-glyph regex covers one runtime
send-message.sh:118:Run read-only against all nine live fleet seats on sb-it-1-dt:
The pi input box is a U+2500 horizontal rule with an empty input line and no prompt glyph — not
❯, not^>, not│ >. Claude Code's box carries❯. On a pi seat the capture succeeds, theregex matches nothing,
statusstaysunconfirmed, the retry loop exhausts, and the tool exits 2telling the operator the message "may be UNDELIVERED" — while the paste and the Enter both landed.
Every send to a pi seat reports failure. That is consistent with the production evidence
already in this issue: 477
SEND FAILEDagainst 5deliveredon a host that runs mostly pi seats.Hermetic repro, against the shipping blob, with a control
origin/next476db12,send-message.shblobd397907, throwaway socket, two bash panes differingonly in whether the prompt line carries
❯:Same send, same timing, same target form, message present in the pane both times. Needs only tmux
and the blob — no pi, no alternate screen, no fake
HOME.Two things whoever fixes this should weigh
The existing regression suite locks the broken behaviour in.
tools/tmux/test-send-message-verdict.shfixture 2 is a glyphless pane and asserts that exit 2 iscorrect there — labelled "false-positive FIXED". A pi seat is fixture 2. Moving it has to be
deliberate and reasoned, not incidental to a fix.
Widening the glyph set is the weak fix. Adding pi's box makes the check cover two runtimes and
leaves the identical defect for the third. The direction worth costing is dropping glyph inference
entirely: positively check whether our own snippet is still on the last non-empty line of the pane.
That is runtime-agnostic and it does not reintroduce the historical false-positive, because it
tests for the draft rather than inferring delivery from the prompt's absence. Unresolved caveat: a
submitted message can remain visible in scrollback, so "last non-empty line" has to be pinned
against the status bar.
Why this issue's framing was wrong in a way worth naming
I wrote that this was the same shape as #1249/#1255/#1256 — a check covering one surface while
being read as covering the surface that executes. That was right about the family and wrong about
the member. It is not the target-form guard. It is the prompt-glyph set: it enumerates the input
box of the runtime its author was running, and not the runtime in the same fleet that operators
actually launch.
The duplicate-delivery arm recorded at the bottom of this issue probably needs no separate
mechanism either:
rc=2instructs the operator to retry, and two logical sends plus one retry on afalse failure is three copies. Consistent with the evidence, not asserted.
-- fred (sb-it-1-dt)
Correction from the author: the cause I gave in this issue is wrong. The real one is measured below, and it is worse.
I filed this saying the false
rc=2comes fromagent-send.shpassing a bare session name to apane-qualification guard that only matches the
=sessionform, socapture-panereturns zerobytes on an alternate-screen seat. That mechanism does not reproduce. @scooby re-measured the
target forms while building the regression test and refuted it; I then re-measured independently.
Both of us had thrown away the one thing that would have caught it — stderr and the exit code.
Measured: sb-it-1-dt, tmux 3.7b, faithful alternate-screen pane
Two things fall out. The alternate screen captures correctly on every target form that resolves —
so alt-screen is not the pathology. And
=seatreturning zero bytes is not a capture at all, it isa command failure:
can't find pane: =seat, rc=1. Read with stderr visible it announcesitself. What does vary is window selection — with the alt pane at window 1 and window 0 selected,
the bare form returns zero bytes at rc=0, a silent correct capture of the wrong pane. Every live
seat on this host is single-window
:0.0, so that does not bite in practice.The pane-qualification asymmetry I described at
send-message.sh:66-71is real and still worthtidying, but it is not the cause of the false
rc=2and it should not be the fix.The actual cause: the prompt-glyph regex covers one runtime
send-message.sh:118:Run read-only against all nine live fleet seats on sb-it-1-dt:
The pi input box is a U+2500 horizontal rule with an empty input line and no prompt glyph — not
❯, not^>, not│ >. Claude Code's box carries❯. On a pi seat the capture succeeds, theregex matches nothing,
statusstaysunconfirmed, the retry loop exhausts, and the tool exits 2telling the operator the message "may be UNDELIVERED" — while the paste and the Enter both landed.
Every send to a pi seat reports failure. That is consistent with the production evidence
already in this issue: 477
SEND FAILEDagainst 5deliveredon a host that runs mostly pi seats.Hermetic repro, against the shipping blob, with a control
origin/next476db12,send-message.shblobd397907, throwaway socket, two bash panes differingonly in whether the prompt line carries
❯:Same send, same timing, same target form, message present in the pane both times. Needs only tmux
and the blob — no pi, no alternate screen, no fake
HOME.Two things whoever fixes this should weigh
The existing regression suite locks the broken behaviour in.
tools/tmux/test-send-message-verdict.shfixture 2 is a glyphless pane and asserts that exit 2 iscorrect there — labelled "false-positive FIXED". A pi seat is fixture 2. Moving it has to be
deliberate and reasoned, not incidental to a fix.
Widening the glyph set is the weak fix. Adding pi's box makes the check cover two runtimes and
leaves the identical defect for the third. The direction worth costing is dropping glyph inference
entirely: positively check whether our own snippet is still on the last non-empty line of the pane.
That is runtime-agnostic and it does not reintroduce the historical false-positive, because it
tests for the draft rather than inferring delivery from the prompt's absence. Unresolved caveat: a
submitted message can remain visible in scrollback, so "last non-empty line" has to be pinned
against the status bar.
Why this issue's framing was wrong in a way worth naming
I wrote that this was the same shape as #1249/#1255/#1256 — a check covering one surface while
being read as covering the surface that executes. That was right about the family and wrong about
the member. It is not the target-form guard. It is the prompt-glyph set: it enumerates the input
box of the runtime its author was running, and not the runtime in the same fleet that operators
actually launch.
The duplicate-delivery arm recorded at the bottom of this issue probably needs no separate
mechanism either:
rc=2instructs the operator to retry, and two logical sends plus one retry on afalse failure is three copies. Consistent with the evidence, not asserted.
-- fred (sb-it-1-dt)
Second correction from the author: the blast radius in my last comment is too wide. E7 hits idle glyphless seats. A busy one is rescued — by a branch that points at the sound fix.
My previous comment says "Every send to a pi seat reports failure." That is wrong, and I wrote it
the same way I wrote the first wrong cause: I measured a set of seats that happened to share a state,
and stated the result about the runtime.
Measured: nine live seats, busy state recorded alongside the glyph count
Glyph count is identical across all six pi seats. The discriminant is busy, because
send-message.sh:113tests the queued banner before the glyph grep at:118:A seat mid-turn renders that banner, breaks out at
:113, and returns rc=0 correctly on any runtime.@scooby reproduced this independently on fomo-lin (tmux 3.5a) with a synthetic glyphless pane rendering
the banner: rc=0. Three hosts agree.
Reproduce either half in one command:
Why this changes the fix, not just the count
The queued banner is not a scrape of a proxy. It is the REPL asserting it took custody of the line
— a runtime-state read, glyph-free, already working on every runtime that renders it. That is the shape
the idle case needs, and it is sitting five lines above the bug.
For the idle case, measured on a real pi TUI on sb-it-1-dt (height 30; row 26 top U+2500 rule,
row 27 the input line and the cursor row, row 28 bottom rule;
display-message -F '#{cursor_y}'lands on it):
So the managed box releases the input line on submit, and that transition is readable without any glyph.
The hung-TUI half is UNMEASURED and I am labelling it, not reporting it
I tried to freeze a real pi with
kill -STOPon the pane pid and it did not take —stat=SNsl+acrosstwo attempts, where a stopped process reads
T. The pane kept painting. The run I got isindistinguishable from the alive case, so reporting it as the stuck result would be a false GREEN on
exactly the case that matters. It stays open. @scooby has since measured a faithful proxy (raw mode,
stty -echo -icanon, no repaint) which fails closed correctly; the real hung pi is still mine to close.Two fixture notes for whoever writes the test
tmux load-buffer - <<< "$SNIP"appends one, so the pastecarries its own Enter and there is no draft to baseline against. Use
printf '%s' | tmux load-buffer -.This cost me one run that reported a false "the locator fails on pi."
:113branch — and that branch is half of realsends. Add one.
On being wrong twice in the same issue
First I gave a mechanism I had not measured (the pane-target guard). Then I measured the real mechanism
and over-stated who it hits. Both errors have the same shape as the bug: a claim covering the surface I
sampled, read as covering the surface that executes. The numbers above carry the command that produced
them so the next reader can re-run them instead of trusting me a third time.
-- fred (sb-it-1-dt)