the status dot was on the wrong axis

a commander watching its folder perfectly showed AMBER, titled "Armed, not
currently running", because green was keyed on `running` — which is an
agent TURN in progress, not the loop. so the healthy steady state, which is
what a commander is in for weeks at a time, was painted as the failure
colour and the owner reasonably concluded nothing was watching.

the failure amber was designed for could not happen either: watcherArmed is
computed from the live pid, so a dead loop turns it off rather than leaving
the pair mismatched.

the server now reports `watcherRunning` separately — the loop pid is alive,
right now — which is what makes the distinction available at all. green is
watching, amber is a row that says armed while its loop is gone, grey is a
commander that never watched.

and the flow: after arming, do not wait for the backlog. the pipeline was
proved on one item, so the rest working is expected rather than news — say
it is armed and move to the next question. a failure in the backlog is
worth saying; a success is not. then ask what comes next, because a watcher
that quietly does one job is a complete answer and the conversation should
offer it rather than assume it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 06:23:44 +00:00
co-authored by Claude Opus 5
parent ee1cd006af
commit f4e983693e
5 changed files with 41 additions and 12 deletions
@@ -608,8 +608,10 @@ function MetaBar({ sessionId, title, isConnected, isGenerating, onDisconnect }:
</span>
{commander.watcherArmed ? (
<span className="flex items-center gap-1">
<span className={`h-1.5 w-1.5 rounded-full ${commander.running ? 'bg-success' : 'bg-warning'}`} />
watcher armed
<span
className={`h-1.5 w-1.5 rounded-full ${commander.watcherRunning ? 'bg-success' : 'bg-warning'}`}
/>
{commander.watcherRunning ? 'watching' : 'armed, loop not running'}
<button
type="button"
onClick={() => disarmWatcher.mutate()}
@@ -15,14 +15,21 @@ import { useTranslation } from '../../locales';
// `docs/navigation-audit.md`.
/**
* Three states, not two — the pair `COMMS/command-hq/mobile-4.md` proposed and the design record adopted:
* green running, amber armed-but-not (the failure `watcherArmed && !running` exists to make visible), grey
* neither. `running` alone cannot tell "never meant to watch" from "was watching and stopped"; `watcherArmed`
* alone cannot tell "briefly between fire and re-arm" (normal) from "silently dead". Only the pair says it.
* Three states, on the right axis.
*
* Green is the LOOP being alive — a commander watching its folder, which is the whole point of one and its
* normal condition for weeks at a time. Amber is a row that says armed while the loop is gone: the genuine
* failure, and the only thing worth a warning colour. Grey is a commander that was never watching.
*
* This used to key green on `running`, which is an agent TURN in progress. So a perfectly healthy watcher
* with nobody chatting to it showed amber, titled "Armed, not currently running", and the owner reasonably
* concluded nothing was running. The failure amber was designed for could not happen either: `watcherArmed`
* is computed from the live pid, so a dead loop turns it off rather than leaving the pair mismatched. The
* server reports `watcherRunning` separately now, which is what makes the distinction available at all.
*/
function StatusDot({ running, watcherArmed }: { running: boolean; watcherArmed: boolean }) {
const color = running ? 'bg-success' : watcherArmed ? 'bg-warning' : 'bg-muted-foreground/40';
const title = running ? 'Running' : watcherArmed ? 'Armed, not currently running' : 'Not running';
function StatusDot({ watcherRunning, watcherArmed }: { watcherRunning: boolean; watcherArmed: boolean }) {
const color = watcherRunning ? 'bg-success' : watcherArmed ? 'bg-warning' : 'bg-muted-foreground/40';
const title = watcherRunning ? 'Watching' : watcherArmed ? 'Armed, but its loop is not running' : 'Not watching';
return <span className={`inline-block h-2 w-2 shrink-0 rounded-full ${color}`} title={title} />;
}
@@ -63,7 +70,7 @@ const Card = ({ commander, to, selected, onDeleted }: CardProps) => {
<Link to={to} className="absolute inset-0" aria-label={commander.title} />
<div className="flex items-center gap-2 pr-6">
<StatusDot running={commander.running} watcherArmed={commander.watcherArmed} />
<StatusDot watcherRunning={commander.watcherRunning === true} watcherArmed={commander.watcherArmed} />
<span className="truncate text-sm font-medium">{commander.title}</span>
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
@@ -41,6 +41,8 @@ export type CommandHqCommander = {
tagIds: number[];
running: boolean;
watcherArmed: boolean;
/** The loop process is alive right now. Distinct from `running`, which is an agent TURN. */
watcherRunning?: boolean;
watcherArmedAt: string | null;
watcherStoppedAt: string | null;
watcherLastFiredAt: string | null;
@@ -58,6 +60,8 @@ export type CommandHqCommanderDetail = {
tagIds: number[];
running: boolean;
watcherArmed: boolean;
/** The loop process is alive right now. Distinct from `running`, which is an agent TURN. */
watcherRunning?: boolean;
watcherArmedAt: string | null;
watcherStoppedAt: string | null;
watcherLastFiredAt: string | null;