485 lines
26 KiB
Markdown
485 lines
26 KiB
Markdown
# Control board MVP — plan
|
|
|
|
Tracking: Gitea issue #1503 (https://git.mosaicstack.dev/mosaicstack/stack/issues/1503)
|
|
|
|
## Why
|
|
|
|
Jason is building blind. He has many agent sessions running across projects
|
|
and no single place to see what they are doing or who is waiting on him.
|
|
This plan replaces the registry line as the next thing to build. Registry
|
|
increment 3 stays parked; it does not resume by inertia. Decision record:
|
|
`MOSAIC-STACK-D-001` (Jason, 2026-09-12).
|
|
|
|
## What "done" looks like for the MVP
|
|
|
|
One web page lists my running agent sessions across projects, shows each
|
|
one's current status, and flags which ones are waiting on me.
|
|
|
|
Acceptance:
|
|
|
|
- Jason opens one page and sees every live pi agent, grouped by project.
|
|
- Each agent shows a state in plain words (working, waiting, error,
|
|
offline, idle, unknown).
|
|
- A "waiting on you" section sits at the top of the page.
|
|
- The page refreshes itself. Jason does not have to reload it by hand.
|
|
- Every state on the page comes from a status file written by the scanner,
|
|
not from a guess made in the page itself.
|
|
|
|
## Three steps
|
|
|
|
### Step 1 (today): plan, ticket, scanner
|
|
|
|
- Write this plan and open the Gitea ticket.
|
|
- Build the status scanner script with tests. Running it writes one status
|
|
file per agent under `<dataRoot>/board/`.
|
|
- This step does not touch any page or launcher. It only produces files.
|
|
|
|
### Step 2: the web page
|
|
|
|
- Build a page that reads the status files the scanner writes and shows
|
|
them grouped by project, with the waiting-on-you section on top.
|
|
- The page refreshes itself on a short timer. The small local server behind
|
|
it may re-run the scanner on each refresh so the files stay fresh. No
|
|
separate daemon is needed for this step.
|
|
|
|
### Step 3: daily use and fixes
|
|
|
|
- Jason uses the page every day for real work.
|
|
- He says what is wrong or missing.
|
|
- Fix that one thing, then repeat. No new scope beyond what he actually hits.
|
|
|
|
### Step 3 log
|
|
|
|
**2026-09-12 — Gate A fix: task, active project and workspace per row.**
|
|
Asked by the professor session on Jason's behalf. Each row now shows
|
|
Task (the session's first user message), Active project (basename of the
|
|
nearest git checkout above the workspace) and, in the detail row,
|
|
Workspace (the live `#{pane_current_path}` of the pane running `pi`, else
|
|
the session log's `cwd`). Anything that cannot be derived shows the word
|
|
"unknown". Missing signals, for launchers: (1) pi session logs carry no
|
|
task envelope entry, so Task is the free-text first user message, which
|
|
for fleet seats is a fleet-comms envelope and for hand-started seats is
|
|
whatever was typed ("resume", "who are you?"); a launcher that wants a
|
|
clean task label should emit one as the first line of the first message
|
|
or write a per-seat status file the scanner can read. (2) Neither
|
|
`roster.yaml` nor a seat's `profile.json` names a project, and every fleet
|
|
seat runs in `~/.mosaic`, which is itself a git checkout, so Active
|
|
project reads `.mosaic` for all of them; a `project` field in the roster
|
|
would fix that. (3) Workspace is only live while a `pi` pane exists; for
|
|
offline seats it is history from the log. Fixture test per field, a
|
|
tmux-inspect parse test, and a page test (79/79). Sonnet review: APPROVED, no findings.
|
|
|
|
**2026-09-12 — liveness follows the tmux pane, not just the session.**
|
|
Jason found agents marked "waiting" that were actually dead: their tmux
|
|
session still existed but no longer ran `pi` (it had exited to a shell or
|
|
something else). The scanner now checks which program each tmux pane is
|
|
running (`tmux list-panes -s -t '=<session>' -F '#{pane_current_command}'`) and only counts an
|
|
agent alive if a pane is running `pi`. A tmux session with no `pi` pane is
|
|
now "offline" instead of "waiting".
|
|
|
|
**2026-09-12 — a "Seen" mark for rows that don't need a reply.** Jason
|
|
noticed most "waiting" rows were completion reports, not real asks, and
|
|
they kept cluttering "Waiting on you". The page now has a "Seen" button on
|
|
waiting/error rows; clicking it stores the row's `lastActivity` in
|
|
`<dataRoot>/board/seen.json` (via `POST /api/seen`) and drops the row out
|
|
of "Waiting on you" while keeping its state visible. The mark clears
|
|
itself the moment the agent writes anything new, and "Unsee" puts a row
|
|
back by hand. `seen.json` is only ever written by that click, never by a
|
|
scan.
|
|
|
|
**2026-09-12 — Gate B passed: seat registration on the board.**
|
|
Jason restarted the board on 17153fe1 and relaunched darkwing through
|
|
`agents/darkwing/launch.sh`. The row shows the registration (pid alive, not
|
|
stale), workspace and active project from the record, and the last message
|
|
updating as the session runs. Task still comes from the first user message
|
|
because the launch gave no `--task`; the next real use is
|
|
`scripts/mosaic seat task darkwing "<text>"` so the row shows the assigned
|
|
task instead. Fleet seats stay on the old launchers; only `agents/` seats
|
|
register. Next piece in Jason's order: reply-from-board.
|
|
|
|
**2026-09-12 — Gate C passed: reply-from-board (#1505).**
|
|
Board restarted on 867619dc. Jason sent "pizza?" from the board to filbert
|
|
(Pi, `openai-codex/gpt-6-astra`). The message arrived with the trailer, the
|
|
seat answered in its own session ("Yes. Pepperoni?") and made no send
|
|
attempt; the row showed the answer on the next scan. Before the trailer the
|
|
same model followed ms-communications to the letter and tried
|
|
`agent-send.sh -s control-board`, which the tool refused (no such pane). That
|
|
was correct behaviour on both sides, not a model defect; a weaker model that
|
|
skipped the send was ignoring the skill, not doing better. Sequence next:
|
|
the ledger script, then the WebUI on the Console design absorbs the board.
|
|
|
|
## Piece 2: reply-from-board
|
|
|
|
Approved by Jason 2026-09-12 after Gate B ("seat task update works. I want to
|
|
keep things building"). Same shape as #1503/#1504: one piece, one yes/no gate,
|
|
small commits with tests on `refactor`. Darkwing opens the issue and updates
|
|
`CURRENT.md` when he picks it up.
|
|
|
|
### What it is
|
|
|
|
A reply box on the board. Jason types a message on a seat's row and the board
|
|
delivers it to that seat's tmux pane through the repository's own channel.
|
|
No new transport, no message store, no WebSocket.
|
|
|
|
- **Page**: a one-line text box and a Send button in each row's detail
|
|
(registered rows only, see below). After Send the row shows one of two
|
|
receipts: `delivered` with the time, or `failed` with the tool's stderr.
|
|
Nothing else changes on the page; the next scan shows the effect (the
|
|
seat's newest user message is the text just sent, and a `waiting` row
|
|
turns `working`).
|
|
- **Server**: `POST /api/reply` with `{ "agent": "<row id>", "text": "..." }`.
|
|
The handler resolves the row's tmux session, runs
|
|
`tools/tmux/agent-send.sh -s <session> -S "<host>:control-board" -m <text>`
|
|
and returns the exit code, stdout and stderr. The exit code is the receipt;
|
|
a non-zero code is a failure the page shows verbatim. `-S` labels the
|
|
sender so the seat's preamble reads `[<host>:control-board -> …]`. Fleet
|
|
sockets pass `-L` from the registration's `tmux.socket` when set.
|
|
- **Which rows**: only rows whose registration (#1504) names a tmux session,
|
|
and whose registration is not stale. Everything else shows "reply needs a
|
|
registered seat" instead of the box. Seat name alone is not an address; the
|
|
registration's session is.
|
|
- **Never**: raw `tmux send-keys`, a queue, retries, a history panel, a
|
|
broadcast, sending to more than one seat per request. The durable record of
|
|
what was said is the seat's own session log, which the scanner already reads.
|
|
|
|
### Gate C (yes/no)
|
|
|
|
Jason answers a seat that is `waiting` from the board, without opening a
|
|
terminal, and within one scan the row is `working` and its Task or last
|
|
message reflects the reply. One real exchange is the pass.
|
|
|
|
### Tests
|
|
|
|
- Server: a fake `agent-send.sh` on PATH captures its argv; assert `-s`, `-S`,
|
|
`-L` (when the registration has a socket) and the message text.
|
|
- Refusals with exit codes surfaced: unknown row, empty text, row without
|
|
registration, stale registration, tool exit non-zero (stderr returned).
|
|
- Page: the box appears only on registered live rows; a failure receipt shows
|
|
the stderr text.
|
|
- Loopback only stays as is; no auth is added for this piece.
|
|
|
|
### Boundaries
|
|
|
|
- No change to `packages/seat` or the registration record.
|
|
- No change to `tools/tmux/agent-send.sh`; if it refuses, the refusal is the
|
|
finding (ms-communications rule), reported on the page and in the issue.
|
|
- Fleet seats are out: they are not registered (Jason, 2026-09-12).
|
|
|
|
## How the scanner decides state
|
|
|
|
The scanner reads the newest pi session log for each agent, plus whether
|
|
the agent's tmux session is still alive. It does not guess; it only reports
|
|
what the log and tmux say.
|
|
|
|
| State | Plain-words meaning |
|
|
|---------|----------------------|
|
|
| working | The agent is in the middle of a turn: thinking or running a tool. |
|
|
| waiting | The agent finished its turn. It is your move now. |
|
|
| error | The agent's last turn ended in an error, was aborted, or was cut off. Go look at it. |
|
|
| offline | There is no live tmux session for this agent right now. |
|
|
| idle | The agent is live but has not had a conversation yet. |
|
|
| unknown | The scanner could not ask tmux (missing or not answering). It does not assume the agent is alive. |
|
|
|
|
Two kinds of agents are scanned today:
|
|
|
|
- Repo agents: session logs live under `.pi/state/<agent>/sessions` in a
|
|
project checkout, on the default tmux socket.
|
|
- Fleet agents: session logs live under
|
|
`~/.mosaic/fleet/agents/<agent>/.pi/agent/sessions`, on the tmux socket
|
|
named `mosaic-fleet`.
|
|
|
|
## Where files go
|
|
|
|
- One file per agent: `<dataRoot>/board/sessions/<project>/<agent>.json`
|
|
- One summary file for the whole board: `<dataRoot>/board/index.json`
|
|
- One registration per seat started through `mosaic launch`:
|
|
`<dataRoot>/seats/<layout>/<seat>/registration.json` (#1504). Written by the
|
|
launcher, read by the board, rewritten on relaunch. Not under `board/`
|
|
because the board never writes it.
|
|
- `dataRoot` comes from `~/.config/mosaic-dev/config.json`. If that config
|
|
is missing or broken, the scanner refuses to run. It does not guess a
|
|
fallback location.
|
|
|
|
Board files are derived. They can be deleted and rebuilt at any time by
|
|
running the scanner again. They are NOT run records and they are not
|
|
evidence under the repository's write-once rules.
|
|
|
|
## Commands
|
|
|
|
Scan once and print the table:
|
|
|
|
```
|
|
node packages/control-board/src/cli.mjs scan --print
|
|
```
|
|
|
|
Start the page (step 2), then open http://127.0.0.1:7331/ in a browser:
|
|
|
|
```
|
|
node packages/control-board/src/cli.mjs serve
|
|
```
|
|
|
|
Tests:
|
|
|
|
```
|
|
node --test packages/control-board/tests/
|
|
```
|
|
|
|
## Boundaries
|
|
|
|
- No changes to any launcher script. Corrected 2026-09-12 by Jason's go on
|
|
the `mosaic launch` brief (#1504): the four repository launch scripts
|
|
(`agents/{darkwing,dewey,filbert,rocko}/launch.sh`) gained a guard that
|
|
re-enters them through `scripts/mosaic launch` for registration. The
|
|
fleet launchers under `~/.mosaic` are still untouched; their one-line
|
|
change is written in the Step 3 log for orch-01 to apply.
|
|
- No new files at the repository root.
|
|
- No secrets read, stored, or printed.
|
|
- No daemon yet. In step 1 the scanner is run by hand. In step 2 the page
|
|
may trigger it on refresh; nothing runs it on a schedule.
|
|
- No changes to `packages/mosaic`. Still true: seat registration went into
|
|
a new package, `packages/seat`, not the hardened registry.
|
|
- This plan does not authorize push or merge beyond whatever the existing
|
|
refactor-branch plan already allows.
|
|
|
|
**2026-09-12 — a place to find what you marked Seen.** After using the
|
|
button, Jason noted the board had no way to show seen rows again. The page
|
|
now has a collapsed "Seen (N)" section between "Waiting on you" and "By
|
|
project" that lists every marked row with an Unsee button. It stays open or
|
|
closed across refreshes.
|
|
|
|
**2026-09-12 — "Hide seen" per project.** Jason suggested a checkbox like
|
|
"Hide offline" so seen rows stop cluttering the fleet table now that the
|
|
Seen section exists. Each project header has both boxes, on by default,
|
|
and the note under the table reads "N offline hidden · N seen hidden".
|
|
|
|
**2026-09-12 — Mid-tool-call rule checked and pinned.** Requested by the
|
|
professor session on Jason's behalf. Finding: the scanner already honoured
|
|
the rule, but only through pi's `stopReason` (`toolUse` and tool results
|
|
were `working`, `stop` was `waiting`; `deriveState` in `scan.mjs`). It now
|
|
also looks at the content: an assistant message with a `toolCall` block is
|
|
`working` whatever its stop reason; an error stop reason still wins; a
|
|
`thinking` block does not stop a text turn from being `waiting`. Three
|
|
fixture tests (tool call after a question-looking text, tool result last,
|
|
finished text-only turn) plus three state-table cases. Live check at
|
|
14:27Z: orch-01 mid-tool-call → working, rev-code-01 tool result last →
|
|
working, velma finished text-only turn → waiting; all 13 waiting seats had
|
|
text-only (or thinking+text) last messages, none had a tool call.
|
|
|
|
**2026-09-12 — Header count "N of N".** Jason pointed out that "fleet (38)"
|
|
sat above a table showing 13 rows. The header now reads "fleet (13 of 38)"
|
|
while a checkbox hides something and falls back to "fleet (38)" when nothing
|
|
is hidden. The note under the table stays: the header says how many, the
|
|
note says why. One static test.
|
|
|
|
**2026-09-12 — Parked until the board passes its gate.** Jason's ruling:
|
|
nothing else starts until he has used the board in place of cycling tmux.
|
|
Parked, not cancelled, and not to be resumed by inertia:
|
|
|
|
- Dewey's dashboard mockup selection (task D05, `agents/dewey/work/wui/`).
|
|
It gets chosen against a board Jason has used, not before.
|
|
- Registry increment 3 (already parked above; restated so it stays parked).
|
|
- New seat directories. The roster is frozen at its current seats until the
|
|
rails pass condition is written (phase C of the 48-hour plan).
|
|
|
|
**2026-09-12 — Rule: a seat mid-tool-call is `working`, never `waiting`.**
|
|
Added as an acceptance rule, not a fix log entry, because it is the second
|
|
way the top section can lie (the first was the dead-session case above).
|
|
If the newest session-log entry is an assistant message that contains a
|
|
tool call, or a tool result with no assistant text after it, the state is
|
|
`working` even if the last text the agent printed looks like a question.
|
|
`waiting` requires the last entry to be an assistant message whose content
|
|
is text only and whose turn has ended. Verify with one live seat caught
|
|
mid-tool-call and one that has finished a turn; add a fixture test for
|
|
each so the rule cannot regress silently.
|
|
|
|
**2026-09-12 — Jason chose Dewey design 3 (Console) as the WebUI starting
|
|
point.** This answers task D05 and lifts the "chosen against a board Jason has
|
|
used" hold above: the selection is made, on evidence from today's use. It does
|
|
not start WebUI build work. Sequence stays: seat registration via
|
|
`mosaic launch` (in progress), then reply-from-board, then the WebUI on the
|
|
Console design absorbs the board as its first screen.
|
|
|
|
**2026-09-12 — Seat registration: `mosaic launch <seat>` (#1504).** Gate A
|
|
passed on Jason's go, relayed by the professor session with a brief. New
|
|
package `packages/seat` and wrapper `scripts/mosaic`:
|
|
|
|
- `scripts/mosaic launch <seat|seat-dir> [--task ..] [-- args]` resolves
|
|
`agents/<seat>/launch.sh` (repo layout) or a seat directory (fleet layout,
|
|
`<seatDir>/.pi`), writes `<dataRoot>/seats/<layout>/<seat>/registration.json`, then
|
|
replaces itself with the launch script (`process.execve`), so the pid, the
|
|
terminal, the exit code and the tmux foreground command are the script's
|
|
own. The record: seat, project, task, workspace, tmux, harness, startedAt,
|
|
pid, plus sessionsDir (the board matches on it, not on the name, because
|
|
"darkwing" exists in both layouts), seatDir, launchScript, layout,
|
|
updatedAt, version. Rewritten on every launch.
|
|
- `scripts/mosaic seat task <seat> <text>` changes the task only; refuses
|
|
when the seat was never launched this way.
|
|
- Scanner: registrations are read on every scan; a registered task, project
|
|
or workspace wins and `taskSource`, `activeProjectSource`,
|
|
`workspaceSource` read `registration`; empty or null registered fields
|
|
leave the derived value; rows without a registration are unchanged. A
|
|
malformed record is listed in `registrationErrors` and skipped. Page: a
|
|
small "registered" tag next to the value and a Registered line in the
|
|
detail (start, harness, pid, tmux, layout).
|
|
- The four repository launch scripts (`agents/{darkwing,dewey,filbert,
|
|
rocko}/launch.sh`) gained a five-line guard: unless
|
|
`MOSAIC_LAUNCH_REGISTERED` is set or `--check` is among the arguments,
|
|
re-enter through `scripts/mosaic launch --harness pi|claude-code`. So the
|
|
plain `agents/darkwing/launch.sh` registers too. rocko registers but is
|
|
still not on the board: it has no pi session log to scan. Gap, not a bug.
|
|
- Fleet launchers under `~/.mosaic` are not edited from this repository.
|
|
Not planned (Jason's ruling, 2026-09-12, relayed by the professor
|
|
session): fleet seats stay on the old launchers; only `agents/` seats
|
|
use `scripts/mosaic`. The one-liner below is kept for the record only.
|
|
One-line change that would do it, in
|
|
`~/.mosaic/fleet/bin/launch-seat.sh`, after `SEAT_DIR=` is set (the
|
|
per-seat `launch.sh` files are shims and need nothing):
|
|
`[[ -n "${MOSAIC_LAUNCH_REGISTERED:-}" || "$CHECK" -eq 1 ]] || exec /mnt/storage/src/mosaic-stack/scripts/mosaic launch --harness pi "$SEAT_DIR" -- "$@"`
|
|
Fleet records carry project and workspace null (roster has no project
|
|
field; all fleet seats run in `~/.mosaic`), so those rows keep their
|
|
derived values until `--project`/`--workspace` are passed or the roster
|
|
gains a field.
|
|
- Sonnet review found one defect before commit: the record path was keyed
|
|
by seat name alone, so the repo and fleet "darkwing" would have overwritten
|
|
each other. Fixed by keying on layout and seat
|
|
(`seats/<layout>/<seat>/`); `seat task` refuses a bare name registered in
|
|
more than one layout unless `--layout` is given. Second review pass
|
|
approved. The reviewer also noted `process.execve` is marked experimental
|
|
in Node; recorded, accepted (Node 24+ is the floor and the API is the only
|
|
way to keep the seat's pid).
|
|
- Name clash noted: the npm-global `mosaic` CLI (estate tooling) has no
|
|
`launch` or `seat` command; the repository wrapper is run by path.
|
|
|
|
Tests: seat package 15 (record write via a fake launch.sh, args and exit
|
|
passthrough, relaunch rewrite, `--task`, tmux null, tmuxContext with a fake
|
|
exec, task update and its refusals, malformed records, samePath through a
|
|
symlink, recursion guard); control board 79 → 89 (override precedence,
|
|
empty registration, no registration parity, loadRegistrations errors,
|
|
matchRegistration by realpath and the same-name-different-layout case, scan
|
|
on disk, server round trip, page source checks); the two launch-script
|
|
suites now run each real `launch.sh` in a fixture that is its own data root
|
|
and assert the record and the `MOSAIC_LAUNCH_REGISTERED` value Pi/Claude
|
|
saw (5/5); registry 69/69; config 24/24. Live check: the real
|
|
`agents/darkwing/launch.sh` run through the launcher against a temporary
|
|
data root, then a scratch board on 7332 reading that root: the darkwing
|
|
row shows the registered task, "registered" on task and project, and the
|
|
Registered detail line.
|
|
|
|
Found on the way: Jason's own launch refused with "local Pi must match
|
|
package.json (0.85.1)". Commit 557aba0f (#1499) moved the pin without an
|
|
`npm ci` in this checkout (node_modules held 0.84.4; the global pi was
|
|
already 0.85.1, which hid it). Fixed by `npm ci`; the refusal now names
|
|
both versions. Jason questioned the pin itself; it is canon (AGENTS.md
|
|
"Version pin") and stays unless he decides otherwise.
|
|
|
|
**2026-09-12 — Two defects in 69f99323, reported by the professor session
|
|
and verified here.** (1) The darkwing launch test's flock-contention spawn
|
|
inherited the plain environment, so the launch script re-entered
|
|
`scripts/mosaic` with the default config and wrote fixture records for
|
|
darkwing, dewey and filbert into the real data root (dead pids, workspaces
|
|
under `/tmp`), overwriting the record of Jason's live darkwing. Fix: that
|
|
spawn names the fixture config, and both launch test files now set
|
|
`MOSAIC_CONFIG` to a nonexistent path and clear `MOSAIC_LAUNCH_REGISTERED`
|
|
for the whole process, so any spawn that forgets fails instead of
|
|
polluting. The three fixture records were deleted by hand; Jason's next
|
|
darkwing launch writes the real one. (2) A registration is written before
|
|
the launch script's own checks, so a refused launch (Jason's Pi pin refusal)
|
|
left a record with a dead pid that the board showed as registered. The
|
|
scanner now probes the recorded pid on every pass (`pidAlive`, signal 0);
|
|
a gone pid makes the record stale: still on the Registered line, marked
|
|
stale with `registered.alive` false, but derived task, project and
|
|
workspace win. Index gains `registrationStale`; the CLI summary a stale
|
|
count. Tests: board 90/90 (was 89), seat 15/15, launch scripts 5/5; the
|
|
launch tests run against the real data root untouched (checked by
|
|
timestamp). Also from Jason: fleet seats stay on the old launchers; the
|
|
orch-01 one-liner above is marked not planned.
|
|
|
|
Observation from Jason, same day: after relaunching darkwing, the row's
|
|
"Last message" was still from the earlier session. Cause, not a scanner
|
|
bug: Pi writes the session log on the first message, and `--continue`
|
|
resumes the newest existing log, so until the relaunched seat sends or
|
|
receives a message there is nothing newer to read. The registration's
|
|
`startedAt` and pid are the only trace of the relaunch, which is what the
|
|
Registered line is for. A later refinement could say "relaunched at X, no
|
|
messages since" when `startedAt` is newer than the last activity.
|
|
|
|
**2026-09-12 — Model per row (#1503).** Jason: "The running agent (sonnet,
|
|
opus, gpt-6-astra) should surface in the interface as well." Pi logs name
|
|
the model twice: a `model_change` entry (`provider`, `modelId`) at launch
|
|
and on every `/model` switch, and `model`/`provider` on each assistant
|
|
turn. `readSession` now keeps the latest of either; the record gains
|
|
`model` and `provider`; the page shows the model in small type under the
|
|
agent name (provider in the hover) and a Model detail row. A `/model`
|
|
switch shows on the next scan even before the seat answers. Blank when the
|
|
log names no model; rocko has no pi log and stays off the board. Checked
|
|
on a scratch board against the real data root: all 42 rows carried a
|
|
model (darkwing and sage `gpt-6-astra`, filbert `glm-5.3`, fleet rows a mix
|
|
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.
|