control board: reply from the board through agent-send.sh (#1505)

Piece 2 of the MVP (#1503). A one-line reply box and Send in the detail
of rows with a live registration; POST /api/reply runs
tools/tmux/agent-send.sh -s <session> -S <host>:control-board
[-L <socket>] -m <text> once for one seat and returns the exit code,
stdout and stderr. The page shows delivered or failed with the tool's
stderr; other rows say "reply needs a registered seat". No send-keys,
queue, retries, history or broadcast; packages/seat and agent-send.sh
untouched. Every message ends with a fixed trailer telling the seat to
answer in its own session (Jason's refinement after the first Gate C
exchange; the board has no pane). Board suite 98/98.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 11:30:30 -05:00
co-authored by Claude Fable 5.1
parent a62ca1904f
commit 867619dca2
8 changed files with 508 additions and 19 deletions
@@ -407,3 +407,67 @@ of `gpt-5.6-*` and `glm-5.3`). Board suite 91/91. Jason's board on 7331
needs a restart to pick this up. Sonnet review found the model hover title
escaped an already escaped string (double encoding for names with `&` or
`<`); fixed before commit, then APPROVED on that basis.
**2026-09-12 — Piece 2: reply-from-board (#1505).** Built to the brief
above. `POST /api/reply {agent, text}` rescans, finds the row, and runs
`tools/tmux/agent-send.sh -s <session> -S <host>:control-board -m <text>`,
plus `-L <socket>` when the registration names one, with
`MOSAIC_TMUX_SOCKET` stripped from the tool's environment so the
registration is the only source of the socket. The answer carries
`delivered`, the exit code, stdout and stderr; a non-zero exit is
reported as is. Refusals before the tool runs: bad or empty or long text
400, unknown row 404, no registration or stale registration or no tmux
session 409, tool not startable 500. The page puts a one-line box and
Send in the detail of rows whose registration is live and has a tmux
session; other rows say "reply needs a registered seat". The receipt
reads `delivered <time> to tmux <session>` or `failed (exit N): <stderr>`;
drafts and receipts survive the periodic refresh and the caret is given
back to the box; a delivered reply clears the box and rescans at once.
Not built, per the brief: raw send-keys, queue, retries, history,
broadcast, more than one seat per request; no change to `packages/seat`,
the registration record or `agent-send.sh`.
Live check on a scratch server (port 7332) with a scratch tmux session
`cb-reply-test` running `cat` and a temp data root: `agent-send.sh`
accepted `dragon-lin:control-board` as sender, the text arrived in the
pane as `[dragon-lin:control-board -> dragon-lin:cb-reply-test] ...`, and
the page showed `delivered`. Killing the session also killed the
registered pid, so the row went stale and the box gave way to "reply
needs a registered seat", as intended. With a live pid pointed at a
missing session the receipt read `failed (exit 1): ERROR: invalid
resolved pane identity` and the draft stayed in the box. Tests: a fake
`agent-send.sh` on the server's configured path captures argv and the
environment; cases for `-s`/`-S`/`-m` without `-L`, with `-L`, exit 4 with
stderr, each refusal with the tool never called, a missing tool, and
static checks on the page (box only when `alive !== false` and a tmux
session, JSON POST, escaped receipts). Board suite 98/98 (was 91). Gate C
is Jason's to run against a real waiting seat; his board on 7331 needs a
restart first.
Sonnet review of the piece: one finding, the route comment at the top of
`serve.mjs` had the `/api/reply` line spliced into the middle of the
`/api/seen` description, so it read as if a reply rescanned and returned
the index; the comment was split back into two entries before commit.
Noted but not changed: the body limit is bytes and the text limit is
characters, so a non-ASCII text near 2000 characters is refused as an
oversized body rather than as long text; and a row open in both "Waiting
on you" and its project group has two forms with the same id, so the
refocus after a refresh may land on the other copy.
**2026-09-12 — Gate C, first exchange, and the trailer.** Jason answered
darkwing from his restarted board with "pizza?". The delivery worked
(`[dragon-lin:control-board -> dragon-lin:darkwing] pizza?` in darkwing's
session log) and the row showed the answer as its last message within one
scan, so the return path exists: the board reads the transcript. What
failed was the seat's side: the communications skill told the seat a reply
only exists once `agent-send.sh` runs, the seat aimed at `-s
control-board`, and the tool refused because `control-board` is not a
tmux session ("invalid resolved pane identity"). That refusal is correct;
the board is not made a pane. Fix in the board, relayed as Jason's ruling
by the professor session: every message the board sends ends with a fixed
line, `(control-board: answer in your own session as usual; the board
reads your transcript. Do not agent-send to control-board.)`, exported as
`REPLY_TRAILER`; `-S` stays `<host>:control-board` so the preamble stays
honest about the origin. The fake agent-send test now checks the message
text ends with the trailer; the receipt is unchanged. Committed with the
piece.