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:
@@ -351,8 +351,12 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
|
||||
'broken on its first day.',
|
||||
'',
|
||||
'Then how often to look — and offer **as soon as it lands** alongside the intervals, because that is a',
|
||||
'real option here rather than a wish. Categories and dispatching come later still, one at a time, and',
|
||||
'most commanders never need either.',
|
||||
'real option here rather than a wish.',
|
||||
'',
|
||||
'Once it is armed, the conversation is not over: ask what should happen next. Filing what it produces',
|
||||
'into categories, doing something with a result beyond keeping it, or nothing at all — leaving it as a',
|
||||
'watcher that quietly does one job is a complete answer and often the right one. Ask, one at a time,',
|
||||
'rather than deciding for them that there is nothing more to discuss.',
|
||||
'',
|
||||
'**Never arm a watcher before the owner has agreed what it does.** Arming ahead of that is arming a',
|
||||
'guess, and a wrong condition either never fires or fires constantly — either way it is found out days',
|
||||
@@ -367,6 +371,14 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
|
||||
'knows how to handle items it has not seen — that is the whole of what a watcher is — so handing it the',
|
||||
'backlog costs nothing and hands the owner a running commander in the time one item takes.',
|
||||
'',
|
||||
'**Then keep talking. Do not wait for the backlog to finish.** You proved the pipeline on one item, so',
|
||||
'the rest working is expected rather than news — say it is armed and move straight to the next',
|
||||
'question. No sleeping, no polling a log, no "I will confirm once it is done". The owner does not need a',
|
||||
'progress report on work they already know succeeds.',
|
||||
'',
|
||||
'If something DOES go wrong in the backlog, that is worth saying — in the conversation if it is still',
|
||||
'open, or as a notification if it is not. A failure is news. A success is not.',
|
||||
'',
|
||||
'Once the job is settled, write it into this file, replacing this section: what you watch for, what you',
|
||||
'do about it, and which numbers were guesses. The next session should read the answer, not the',
|
||||
'questions.',
|
||||
|
||||
@@ -274,6 +274,9 @@ commandHqRouter.get('/commanders', async (ctx) => {
|
||||
// on `dirName`, not the transcript id: the pid file lives in this commander's OWN `state/`, not
|
||||
// beside the transcript.
|
||||
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
|
||||
// Whether the LOOP is alive right now, separate from `running` (an agent turn in progress) and from
|
||||
// `watcherArmed` (which is also true for a row that says armed but whose loop has gone).
|
||||
watcherRunning: liveLoopPid(who.email, row.dirName) !== null,
|
||||
watcherArmedAt: row.watcherArmedAt,
|
||||
watcherStoppedAt: row.watcherStoppedAt,
|
||||
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
|
||||
@@ -457,6 +460,7 @@ commandHqRouter.get('/commanders/:sessionId', async (ctx) => {
|
||||
tagIds: tagsByRow.get(row.id) ?? [],
|
||||
running,
|
||||
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
|
||||
watcherRunning: liveLoopPid(who.email, row.dirName) !== null,
|
||||
watcherArmedAt: row.watcherArmedAt,
|
||||
watcherStoppedAt: row.watcherStoppedAt,
|
||||
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user