Supervise several agents at once

One daemon already saw every session; the dashboard only ever showed one of
them well. Under four parallel agents it failed in specific ways, each fixed
here:

- Sessions were identified by workspace basename, so two agents in one repo
  were indistinguishable. The daemon now hands out a small ordinal badge in
  arrival order and the web UI colours each agent by it — rows, timeline
  lines, and approval cards, which previously asked you to approve `rm -rf`
  "in remote-grok" without saying which one.
- `currentTool` held a single call, so parallel tools overwrote each other.
  It is now a list; durations are matched FIFO per tool name, since hook
  payloads carry no call id.
- One 400-event ring, evicted oldest-first, let a chatty agent blank
  everyone else's history. Eviction now takes from whichever session holds
  the most of the ring.
- Sessions sorted by recency jumped under a moving thumb. They are ordered
  waiting -> error -> working -> idle -> ended, ties on badge, so an agent
  keeps its slot.
- Events carry no workspace root, so a restart came back with a full
  timeline and an empty roster. The session map is persisted to
  sessions.json (debounced, flushed on shutdown, 12h cutoff on load).
  In-flight tools are dropped on the way out: they belonged to a process
  that no longer exists.
- Snapshot pushes now back off to 1s once a snapshot exceeds 24KB, since
  full-snapshot SSE cost scales with agent count.
- Pending approvals are ordered by which expires first, not by arrival.

`glance status` and /local/status report the roster by state. e2e suite:
220 passed, 0 failed.
This commit is contained in:
iceBear67
2026-08-09 05:27:00 +00:00
parent 5eec1940be
commit 6d34a17d9d
18 changed files with 637 additions and 94 deletions
+17 -1
View File
@@ -59,6 +59,22 @@ async function api(pathname, { method = "GET", body, admin = false } = {}) {
return data;
}
/** "3 (2 working, 1 waiting on you)" — a raw count says nothing when you watch several agents. */
function sessionBreakdown(states) {
if (!states || typeof states !== "object") return "";
const order = [
["waiting", "waiting on you"],
["error", "error"],
["working", "working"],
["idle", "idle"],
["ended", "ended"],
];
const parts = order
.filter(([key]) => Number(states[key]) > 0)
.map(([key, label]) => `${states[key]} ${label}`);
return parts.length ? ` (${parts.join(", ")})` : "";
}
function requireBuild() {
if (!fs.existsSync(SERVER_ENTRY)) {
console.error(`grok-glance is not built yet.\n\n cd ${PLUGIN_ROOT}\n npm install && npm run build\n`);
@@ -122,7 +138,7 @@ switch (cmd) {
console.log(` devices : ${s.devices}`);
console.log(` approval mode : ${s.approval.mode}`);
console.log(` watchers : ${s.watchers}`);
console.log(` sessions : ${s.sessions}`);
console.log(` sessions : ${s.sessions}${sessionBreakdown(s.sessionStates)}`);
console.log(` events kept : ${s.events}`);
if (s.hookAuthOk === false) {
console.log(