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.',
|
'broken on its first day.',
|
||||||
'',
|
'',
|
||||||
'Then how often to look — and offer **as soon as it lands** alongside the intervals, because that is a',
|
'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',
|
'real option here rather than a wish.',
|
||||||
'most commanders never need either.',
|
'',
|
||||||
|
'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',
|
'**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',
|
'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',
|
'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.',
|
'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',
|
'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',
|
'do about it, and which numbers were guesses. The next session should read the answer, not the',
|
||||||
'questions.',
|
'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
|
// on `dirName`, not the transcript id: the pid file lives in this commander's OWN `state/`, not
|
||||||
// beside the transcript.
|
// beside the transcript.
|
||||||
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
|
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,
|
watcherArmedAt: row.watcherArmedAt,
|
||||||
watcherStoppedAt: row.watcherStoppedAt,
|
watcherStoppedAt: row.watcherStoppedAt,
|
||||||
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
|
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
|
||||||
@@ -457,6 +460,7 @@ commandHqRouter.get('/commanders/:sessionId', async (ctx) => {
|
|||||||
tagIds: tagsByRow.get(row.id) ?? [],
|
tagIds: tagsByRow.get(row.id) ?? [],
|
||||||
running,
|
running,
|
||||||
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
|
watcherArmed: row.watcherArmed || liveLoopPid(who.email, row.dirName) !== null,
|
||||||
|
watcherRunning: liveLoopPid(who.email, row.dirName) !== null,
|
||||||
watcherArmedAt: row.watcherArmedAt,
|
watcherArmedAt: row.watcherArmedAt,
|
||||||
watcherStoppedAt: row.watcherStoppedAt,
|
watcherStoppedAt: row.watcherStoppedAt,
|
||||||
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
|
watcherLastFiredAt: lastFiredByRow.get(row.id) ?? null,
|
||||||
|
|||||||
@@ -608,8 +608,10 @@ function MetaBar({ sessionId, title, isConnected, isGenerating, onDisconnect }:
|
|||||||
</span>
|
</span>
|
||||||
{commander.watcherArmed ? (
|
{commander.watcherArmed ? (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<span className={`h-1.5 w-1.5 rounded-full ${commander.running ? 'bg-success' : 'bg-warning'}`} />
|
<span
|
||||||
watcher armed
|
className={`h-1.5 w-1.5 rounded-full ${commander.watcherRunning ? 'bg-success' : 'bg-warning'}`}
|
||||||
|
/>
|
||||||
|
{commander.watcherRunning ? 'watching' : 'armed, loop not running'}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => disarmWatcher.mutate()}
|
onClick={() => disarmWatcher.mutate()}
|
||||||
|
|||||||
@@ -15,14 +15,21 @@ import { useTranslation } from '../../locales';
|
|||||||
// `docs/navigation-audit.md`.
|
// `docs/navigation-audit.md`.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Three states, not two — the pair `COMMS/command-hq/mobile-4.md` proposed and the design record adopted:
|
* Three states, on the right axis.
|
||||||
* 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`
|
* Green is the LOOP being alive — a commander watching its folder, which is the whole point of one and its
|
||||||
* alone cannot tell "briefly between fire and re-arm" (normal) from "silently dead". Only the pair says it.
|
* 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 }) {
|
function StatusDot({ watcherRunning, watcherArmed }: { watcherRunning: boolean; watcherArmed: boolean }) {
|
||||||
const color = running ? 'bg-success' : watcherArmed ? 'bg-warning' : 'bg-muted-foreground/40';
|
const color = watcherRunning ? 'bg-success' : watcherArmed ? 'bg-warning' : 'bg-muted-foreground/40';
|
||||||
const title = running ? 'Running' : watcherArmed ? 'Armed, not currently running' : 'Not running';
|
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} />;
|
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} />
|
<Link to={to} className="absolute inset-0" aria-label={commander.title} />
|
||||||
|
|
||||||
<div className="flex items-center gap-2 pr-6">
|
<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>
|
<span className="truncate text-sm font-medium">{commander.title}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export type CommandHqCommander = {
|
|||||||
tagIds: number[];
|
tagIds: number[];
|
||||||
running: boolean;
|
running: boolean;
|
||||||
watcherArmed: boolean;
|
watcherArmed: boolean;
|
||||||
|
/** The loop process is alive right now. Distinct from `running`, which is an agent TURN. */
|
||||||
|
watcherRunning?: boolean;
|
||||||
watcherArmedAt: string | null;
|
watcherArmedAt: string | null;
|
||||||
watcherStoppedAt: string | null;
|
watcherStoppedAt: string | null;
|
||||||
watcherLastFiredAt: string | null;
|
watcherLastFiredAt: string | null;
|
||||||
@@ -58,6 +60,8 @@ export type CommandHqCommanderDetail = {
|
|||||||
tagIds: number[];
|
tagIds: number[];
|
||||||
running: boolean;
|
running: boolean;
|
||||||
watcherArmed: boolean;
|
watcherArmed: boolean;
|
||||||
|
/** The loop process is alive right now. Distinct from `running`, which is an agent TURN. */
|
||||||
|
watcherRunning?: boolean;
|
||||||
watcherArmedAt: string | null;
|
watcherArmedAt: string | null;
|
||||||
watcherStoppedAt: string | null;
|
watcherStoppedAt: string | null;
|
||||||
watcherLastFiredAt: string | null;
|
watcherLastFiredAt: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user