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:
2026-08-18 10:18:17 +00:00
co-authored by Claude Opus 5
parent eed906f45b
commit 963c965056
2 changed files with 51 additions and 0 deletions
+34
View File
@@ -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).