Pin the mid-tool-call rule in the control board scanner (#1503)

Acceptance rule (plan page, bf641e22): a seat mid-tool-call is working,
never waiting. The scanner already met it through pi's stopReason values;
deriveState now also checks the content for a toolCall block (working),
after the error stop reasons and before "stop" (waiting). Thinking blocks
do not keep a text turn from being waiting. Three JSONL fixture tests and
three state-table cases pin the rule. Live check on the real board:
orch-01 and rev-code-01 mid-tool-call are working, velma's finished
text-only turn is waiting. Sonnet review: APPROVED.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
2026-09-12 09:29:19 -05:00
co-authored by Claude Fable 5.1
parent bf641e220a
commit 6ec253de20
6 changed files with 95 additions and 4 deletions
+11 -1
View File
@@ -95,16 +95,26 @@ export function readSession(file) {
return { file, sessionId, cwd, lastTimestamp, lastMessage, lastAssistantText, lastError, skippedLines };
}
// True when an assistant message carries a tool call in its content.
export function hasToolCall(message) {
return Array.isArray(message?.content) && message.content.some((c) => c && c.type === "toolCall");
}
// Pure state rule. alive: true/false, or null when liveness could not be checked.
// A null check is reported as "unknown" rather than assumed alive (fail closed).
// Acceptance rule (plan page, 2026-09-12): a seat mid-tool-call is working,
// never waiting. The newest entry being an assistant message with a tool call,
// or a tool result, means working even if the last text looked like a question.
// waiting needs a text-only assistant message whose turn ended (stopReason stop).
export function deriveState({ alive, session }) {
if (alive === false) return "offline";
if (alive !== true) return "unknown";
if (!session || !session.lastMessage) return "idle";
const m = session.lastMessage;
if (m.role === "assistant") {
if (m.stopReason === "stop") return "waiting";
if (m.stopReason === "error" || m.stopReason === "aborted" || m.stopReason === "length") return "error";
if (hasToolCall(m)) return "working";
if (m.stopReason === "stop") return "waiting";
return "working";
}
return "working";