command hq: auto-name a commander from its watcher's own task description
The initial title (the mechanics preamble, truncated, when a background job is
the very first message) is expected — that's just Claude Code naming the
session from its first message, same as always. But by the time the watcher's
task actually starts, the harness has already handed it a real description
("Watch google.com reachability" for a ping watcher), and that reads far
better than the preamble ever will.
Hooked into the exact moment pendingArmWatcher is consumed — the FIRST arm,
never a self-re-arm, which the watcher-loop code already had to distinguish
for its own reasons. Resolves a fresh ChatIdentity via resolveHomeDir and
calls renameClaudeSession with msg.description. Best-effort: a failure is
logged and does not touch watcherArmed.
Known gap, noted in the doc rather than fixed: the detail panel's title bar
is fetched once on mount and won't live-refresh when this fires while it's
open. The grid's cards don't have this problem since GET /commanders already
polls every 15s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -477,6 +477,23 @@ computes `running` from the same fold `GET /commanders` uses for every card, a c
|
|||||||
ago, nothing has run since" without any new mechanism, purely by reading what already exists. Not built
|
ago, nothing has run since" without any new mechanism, purely by reading what already exists. Not built
|
||||||
tonight; noted here so it is not lost, not because it was rejected.
|
tonight; noted here so it is not lost, not because it was rejected.
|
||||||
|
|
||||||
|
**Auto-naming from the watcher's own description, added 2026-08-18 after the web frontend surfaced why it
|
||||||
|
matters.** A fresh commander's title is whatever the first message was — the mechanics preamble, truncated,
|
||||||
|
when that first message is a `handoff` background job. Expected, not a bug: that IS the first message, and
|
||||||
|
Claude Code names sessions from it the same way it always has (see "Naming" above). But by the time the
|
||||||
|
watcher's task actually STARTS, the harness has already given it a real description — `msg.description` on
|
||||||
|
`task:started`, "Watch google.com reachability" for a ping watcher — and that reads far better than the
|
||||||
|
preamble ever will. So the exact moment `pendingArmWatcher` is consumed (the FIRST arm, never a self-re-arm
|
||||||
|
— see "known, accepted imprecision" above for why that distinction is already load-bearing here) now also
|
||||||
|
calls `renameClaudeSession` with that description, resolving a fresh `ChatIdentity` via `resolveHomeDir`
|
||||||
|
for the filesystem call. Best-effort, like everything else in this block: a failure is logged and does not
|
||||||
|
touch `watcherArmed`.
|
||||||
|
|
||||||
|
**Known gap, not closed:** the detail panel's title bar is fetched once on mount and does not live-refresh,
|
||||||
|
so a commander open in the panel at the moment this fires keeps showing the old title until the next
|
||||||
|
navigation or reload. The grid's cards do not have this problem — `GET /commanders` is already polled every
|
||||||
|
15s. Flagged rather than fixed; worth a `refetch()` hook if it turns out to matter in practice.
|
||||||
|
|
||||||
**A real bug, caught by tracing rather than by the compiler, worth stating plainly.** When the cost-tracking
|
**A real bug, caught by tracing rather than by the compiler, worth stating plainly.** When the cost-tracking
|
||||||
schema changed `recordCommandHqWatcherFired`'s signature from `(userId, sessionId)` to `(sessionRowId,
|
schema changed `recordCommandHqWatcherFired`'s signature from `(userId, sessionId)` to `(sessionRowId,
|
||||||
taskId)`, the ONE existing call site was not updated in the same pass — and `bunx tsgo` stayed clean, because
|
taskId)`, the ONE existing call site was not updated in the same pass — and `bunx tsgo` stayed clean, because
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
recordCommandHqTurn,
|
recordCommandHqTurn,
|
||||||
} from 'officerdb';
|
} from 'officerdb';
|
||||||
import { resolveHomeDir } from '@@/user-home';
|
import { resolveHomeDir } from '@@/user-home';
|
||||||
|
import { renameClaudeSession, type ChatIdentity } from './claude-sessions';
|
||||||
import { claudeLoginState } from '@@/os-user-claude';
|
import { claudeLoginState } from '@@/os-user-claude';
|
||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
@@ -539,6 +540,39 @@ function createMessageHandler(
|
|||||||
void armCommandHqWatcher(userId, transcriptId, taskId).catch((err: unknown) => {
|
void armCommandHqWatcher(userId, transcriptId, taskId).catch((err: unknown) => {
|
||||||
logger.warn('Could not arm commander watcher', { sessionId, transcriptId, error: String(err) });
|
logger.warn('Could not arm commander watcher', { sessionId, transcriptId, error: String(err) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Auto-name the commander from the agent's own description of what it just started —
|
||||||
|
"Watch google.com reachability" reads better than the mechanics-preamble text every fresh
|
||||||
|
commander is titled with initially (expected, not a bug: that IS the first message, and
|
||||||
|
Claude Code names sessions from it the same way it always has). This is the one moment we
|
||||||
|
know for certain this is the FIRST task the watcher produced, not a self-re-arm — the
|
||||||
|
`else` branch below is exactly what handles every later one, and never reaches here.
|
||||||
|
*/
|
||||||
|
const description = msg.description?.trim();
|
||||||
|
if (description) {
|
||||||
|
const capturedTranscriptId = transcriptId;
|
||||||
|
const capturedCwd = session.cwd;
|
||||||
|
const capturedEmail = session.email;
|
||||||
|
void resolveHomeDir(userId)
|
||||||
|
.then((resolved) => {
|
||||||
|
if (!resolved.ok) return;
|
||||||
|
const who: ChatIdentity = {
|
||||||
|
email: capturedEmail,
|
||||||
|
home: resolved.home,
|
||||||
|
osUser: resolved.osUser,
|
||||||
|
isOwner: resolved.isOwner,
|
||||||
|
};
|
||||||
|
renameClaudeSession(who, capturedCwd, capturedTranscriptId, description);
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
logger.warn('Could not auto-name commander from watcher description', {
|
||||||
|
sessionId,
|
||||||
|
transcriptId: capturedTranscriptId,
|
||||||
|
error: String(err),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not a fresh arm — but if this commander is ALREADY armed, a new task starting is exactly what
|
// Not a fresh arm — but if this commander is ALREADY armed, a new task starting is exactly what
|
||||||
// the agent's own self-re-arm produces (`handoffContract`: same turn, same command, new task id).
|
// the agent's own self-re-arm produces (`handoffContract`: same turn, same command, new task id).
|
||||||
|
|||||||
Reference in New Issue
Block a user