Files
stack/packages/control-board/README.md
T
jason.woltjeandClaude Fable 5.1 88d21defde Check pi liveness per tmux pane and add "Seen" marks to the control board (#1503)
First step-3 refinement from Jason's daily use. Liveness now lists the
panes of the agent's tmux session and counts it alive only if a pane runs
pi, so killed pi sessions whose tmux session still exists show offline
instead of waiting. A "Seen" button on waiting and error rows stores the
row's lastActivity in <dataRoot>/board/seen.json (clicks only, never
rewritten by a scan, fail closed if corrupt) and drops the row from
"Waiting on you" until the agent writes anything newer; "Unsee" reverses
it. New POST /api/seen route: JSON only, 4 KB limit, 400 on bad input.

Tests: control-board 63/63 (30 new), registry 69/69. Review APPROVED;
receipt docs/plans/reviews/2026-09-12_control-board-step3-seen-marks.md.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-12 08:24:49 -05:00

123 lines
5.2 KiB
Markdown

# control-board
Step 1 of the control board MVP (`docs/plans/2026-09-12_control-board-mvp.md`).
This package scans your running pi agent sessions and writes one small JSON
status file per agent, plus one summary file, so a later web page can show
them all in one place. It reads pi session logs and checks tmux liveness. It
does not launch, stop, or talk to any agent. Board files are derived and
rewritable; they are not run records and are not evidence.
## States
| 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, or its session exists but no longer runs `pi`. |
| 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. |
Liveness means a pane in the agent's tmux session is actually running `pi`
(`tmux list-panes -s -t '=<session>' -F '#{pane_current_command}'`). A tmux session that still
exists but only runs bash or some other program counts as offline, not
waiting.
## Commands
```
node src/cli.mjs scan [--config PATH] [--repo PATH] [--fleet PATH|none] [--liveness tmux|assume-alive] [--print]
node src/cli.mjs serve [same flags] [--port N] [--host 127.0.0.1]
```
`scan` runs once and writes the status files. `serve` starts a small local
web server: open `http://127.0.0.1:7331/` in a browser. The page fetches
`/api/board` every 10 seconds; each fetch re-runs the scan, so the page is
never staler than that timer. There is no login, so the server refuses to
bind to anything but a loopback address.
- `--config PATH` — path to the system config file. Defaults to
`~/.config/mosaic-dev/config.json`. This file must exist and name an
absolute `dataRoot`, or the scanner refuses to run.
- `--repo PATH` — path to a project checkout to scan for repo agents
(`.pi/state/<agent>/sessions`). Defaults to the current directory.
- `--fleet PATH|none` — path to the fleet agents directory
(`<fleet>/<agent>/.pi/agent/sessions`, tmux socket `mosaic-fleet`).
Defaults to `~/.mosaic/fleet/agents`. Pass `none` to skip fleet agents.
- `--liveness tmux|assume-alive` — how to decide if an agent is alive.
`tmux` (default) checks the real tmux session. `assume-alive` treats
every agent as alive, useful for tests or environments without tmux.
- `--print` — (`scan`) also print a one-line-per-agent table to stdout. The
first column is `*` for a row waiting on you, `s` for a seen row, or blank
otherwise. The final summary line reads
`board: <dir> (N sessions, N waiting on you, N seen)`.
- `--port N` — (`serve`) port to listen on. Default `7331`; `0` picks a free port.
- `--host ADDR` — (`serve`) loopback address to bind. Default `127.0.0.1`.
Any non-loopback address is refused.
Routes served:
- `GET /` — the page.
- `GET /api/board` — rescan, returns `index.json`.
- `POST /api/seen` — mark or unmark a row as seen, then rescan and return
`index.json`. Body must be JSON (`Content-Type: application/json`, no
more than 4096 bytes): `{"project", "agent", "lastActivity", "seen"}`.
`seen` defaults to `true`; pass `false` to unsee. Bad input, a missing
header, or an oversized body gets a `400`.
- `GET /healthz`.
## Exit codes
- `0` — scan completed and status files were written, or the server stopped cleanly.
- `2` — refused: bad config, missing/invalid `dataRoot`, or bad arguments.
The message on stderr says why.
## Tests
```
node --test packages/control-board/tests/
```
## Output layout
```
<dataRoot>/board/
index.json # summary: counts, waiting-on-you list, seen list, all records
seen.json # Jason's "seen" marks (see below)
sessions/
<project>/
<agent>.json # one status record per agent
```
`seen.json` holds `{ "<project>/<agent>": "<lastActivity>" }`. It is written
only when Jason clicks "Seen" or "Unsee" on the page (via `POST
/api/seen`); a scan reads it but never writes it. A mark applies only while
the agent's newest message still has that exact `lastActivity` timestamp —
as soon as the agent writes anything new, `lastActivity` changes, the mark
no longer matches, and the row falls back into "Waiting on you" on its own.
If `seen.json` exists but is not valid JSON (or not an object of string
values), the scan refuses rather than silently dropping every mark.
## Example status record
```json
{
"agent": "darkwing",
"project": "mosaic-stack",
"state": "waiting",
"waitingOnYou": true,
"seen": false,
"alive": true,
"tmux": { "socket": null, "session": "darkwing" },
"sessionFile": "/mnt/storage/src/mosaic-stack/.pi/state/darkwing/sessions/2026-09-12.jsonl",
"sessionId": "01a06e48-0718-71f2-a889-c263c4800fb9",
"cwd": "/mnt/storage/src/mosaic-stack",
"lastActivity": "2026-09-12T15:04:33.000Z",
"ageSeconds": 42,
"lastAssistantText": "Ready for the next step whenever you are.",
"lastError": null,
"skippedLines": 0,
"scannedAt": "2026-09-12T15:05:15.000Z"
}
```