a command dropped for want of a sidecar has to say so

`sendFire` looked up a sidecar, and if none was registered it did nothing.
No log, no error, no return value — the command simply stopped existing. That
window is real and entirely predictable: every `pm2 restart officer` has one
while the sidecars reconnect, and somebody clicking a button has no idea they
are in it.

The cost landed hardest on answering a question. `answerClaudeQuestion` in the
agent documents this exact failure in a careful comment and cannot report it,
because the command never arrives to be reported. So the question card kept
spinning, which from the person's side is indistinguishable from a slow agent,
and the only move left is to send another message — the worst possible thing to
do to a waiting tool call. Observed tonight, on a restart I ran myself while a
commander was mid-question.

So `sendFire` returns whether anyone took the command and warns when nobody did,
and the two paths where a person is actively waiting — a question answer and a
plan decision — tell them instead of leaving the card to spin.

The other five callers are unchanged and now leave a log line, which is the part
that was missing when this was diagnosed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 00:06:30 +00:00
co-authored by Claude Opus 5
parent 383df51a99
commit 5c802b07b5
2 changed files with 50 additions and 9 deletions
+24 -7
View File
@@ -224,11 +224,28 @@ function sendCommand(kind: string, cmd: SidecarCommand, timeoutMs = DEFAULT_TIME
});
}
function sendFire(kind: string, cmd: SidecarCommand): void {
/**
* Fire a command at whichever sidecar handles this kind. Returns whether one was there to take it.
*
* It used to return void and drop the command when no sidecar was registered — no log, no error, nothing.
* That window is real and predictable: every `pm2 restart officer` has one while the sidecars reconnect, and
* a person clicking a button has no idea they are in it.
*
* The cost landed on `claude:question-answer`, whose own handler documents this exact failure and cannot
* report it, because the message never arrives to be reported. From the person's side the question card just
* kept spinning — indistinguishable from a slow agent, so the only move left was to send another message.
*
* Callers that can tell somebody are now able to. The boolean is the whole mechanism; the warning is for
* whoever reads the log afterwards wondering why one click did nothing.
*/
function sendFire(kind: string, cmd: SidecarCommand): boolean {
const sc = findSidecarHandling(kind);
if (sc) {
sc.ws.send(JSON.stringify(cmd));
if (!sc) {
console.warn(`[sidecar] dropped ${cmd.type}: no sidecar registered for "${kind}"`);
return false;
}
sc.ws.send(JSON.stringify(cmd));
return true;
}
function sendCommandToSidecar(
@@ -368,8 +385,8 @@ export function decideClaudePlan(params: {
requestId: string;
approve: boolean;
feedback?: string;
}): void {
sendFire('claude', { type: 'claude:plan-decision', id: nextId(), ...params });
}): boolean {
return sendFire('claude', { type: 'claude:plan-decision', id: nextId(), ...params });
}
// The user's answer to an AskUserQuestion. Fire-and-forget for the same reason as the plan decision: the
@@ -380,8 +397,8 @@ export function answerClaudeQuestion(params: {
userId: number;
toolCallId: string;
answer: string;
}): void {
sendFire('claude', { type: 'claude:question-answer', id: nextId(), ...params });
}): boolean {
return sendFire('claude', { type: 'claude:question-answer', id: nextId(), ...params });
}
/**