Context
Commands ride the heartbeat reply and are executed by handle_commands() in agent/hub-agent.py with at-least-once delivery and cmdId de-dup. But the ack is binary — "executed" — with no outcome attached. Several refusal paths only write to the container log:
spawn() refuses at MAX_SESSIONS or for an unknown repo → log(...) and return; no session record is created.
start() refuses at MAX_SESSIONS → same.
- a poison command that throws is deliberately acked (correct — no retry loops) but its error goes only to the log.
The UI's optimistic pending state (pending map in index.html) covers this with a timeout: the spinner shows "Starting a new session…" for up to 45 seconds (PENDING_TTL_MS) and then silently disappears.
Problem
A refused spawn is indistinguishable from a slow one. The operator clicks, watches a ghost card spin for 45s, and ends up with nothing and no explanation — the actual reason ("at MAX_SESSIONS (6)") is buried in a container-log tail. Failures that do create a session record surface fine via errorMsg; it's precisely the refusals-without-a-record that vanish.
Proposal
- Extend the ack channel: instead of
ackedCommands: [cmdId], report ackedCommands: [{cmdId, ok, error?}] (keeping backward compat by accepting both shapes hub-side during transition).
- Hub: keep a short-lived
results map per host (cmdId → outcome, TTL a few minutes) in the agent record, exposed via /api/agents.
- UI: the pending-state reconciler (
reconcilePending()) checks results by cmdId — the spawn POST already returns the cmdId, it just isn't stored. On a failed result: clear the ghost immediately and show the error (toast or inline in the repo block).
- This also gives
restart a real completion signal — currently the UI notes it has "no observable signal — cleared by timeout" (RESTART_TTL_MS).
Touchpoints
agent/hub-agent.py — refusals become raised/returned errors captured into the ack; handle_commands(), spawn(), start()
agent-hub/server.js — result retention + API shape
agent-hub/public/index.html — store cmdIds from POST responses, reconcile against results, error display
Context
Commands ride the heartbeat reply and are executed by
handle_commands()inagent/hub-agent.pywith at-least-once delivery and cmdId de-dup. But the ack is binary — "executed" — with no outcome attached. Several refusal paths only write to the container log:spawn()refuses atMAX_SESSIONSor for an unknown repo →log(...)and return; no session record is created.start()refuses atMAX_SESSIONS→ same.The UI's optimistic pending state (
pendingmap inindex.html) covers this with a timeout: the spinner shows "Starting a new session…" for up to 45 seconds (PENDING_TTL_MS) and then silently disappears.Problem
A refused spawn is indistinguishable from a slow one. The operator clicks, watches a ghost card spin for 45s, and ends up with nothing and no explanation — the actual reason ("at MAX_SESSIONS (6)") is buried in a container-log tail. Failures that do create a session record surface fine via
errorMsg; it's precisely the refusals-without-a-record that vanish.Proposal
ackedCommands: [cmdId], reportackedCommands: [{cmdId, ok, error?}](keeping backward compat by accepting both shapes hub-side during transition).resultsmap per host (cmdId → outcome, TTL a few minutes) in the agent record, exposed via/api/agents.reconcilePending()) checks results by cmdId — the spawn POST already returns the cmdId, it just isn't stored. On a failed result: clear the ghost immediately and show the error (toast or inline in the repo block).restarta real completion signal — currently the UI notes it has "no observable signal — cleared by timeout" (RESTART_TTL_MS).Touchpoints
agent/hub-agent.py— refusals become raised/returned errors captured into the ack;handle_commands(),spawn(),start()agent-hub/server.js— result retention + API shapeagent-hub/public/index.html— store cmdIds from POST responses, reconcile against results, error display