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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
recordCommandHqTurn,
|
||||
} from 'officerdb';
|
||||
import { resolveHomeDir } from '@@/user-home';
|
||||
import { renameClaudeSession, type ChatIdentity } from './claude-sessions';
|
||||
import { claudeLoginState } from '@@/os-user-claude';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { logger } from './logger';
|
||||
@@ -539,6 +540,39 @@ function createMessageHandler(
|
||||
void armCommandHqWatcher(userId, transcriptId, taskId).catch((err: unknown) => {
|
||||
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 {
|
||||
// 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).
|
||||
|
||||
Reference in New Issue
Block a user