Jason asked for the running model (sonnet, opus, gpt-6-astra) on the board. readSession keeps model and provider from the log's latest model_change entry or assistant turn, whichever is later, so a /model switch shows on the next scan; scanAgent exposes both; the page shows the model under the agent name with the provider in the hover and a Model detail row. Blank when the log names none. Live check on a scratch board against the real data root: all 42 rows carried a model. Board 91/91. Sonnet review caught a double-escaped hover title; fixed. Co-Authored-By: Claude Fable 5.1 <[email protected]>
353 lines
19 KiB
Markdown
353 lines
19 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.
|
|
|
|
## 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.
|