From a2ddf258b685a5284b43599da9944f3b36270315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 00:11:04 +0000 Subject: [PATCH] "started" is a latch, not a message count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-start pane keyed off `messages.length === 0`, which stands in for "not started yet" right up to the moment it matters. Lose the socket mid-turn and the messages come back empty, so the setup card and its Start button reappear on a commander that is already running — and pressing Start sends the opening brief a second time. That happened tonight, after a restart I ran while a commander was live: the turn died, the pane reset, Send re-sent the brief, and the commander was asked to introduce itself twice. The owner deleted it and started over, which was the right call and should not have been necessary. So it latches. Once anything says this conversation exists — a message, a transcript id, a turn in flight — it stays started. Deliberately not derived from live state, since surviving the moment that state goes empty is the entire job. Co-Authored-By: Claude Opus 5 --- .../src/apps/CommandHQ/CommandHqDetail.tsx | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx index d87488b9..bbf7dadc 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx @@ -230,6 +230,23 @@ function NewCommander({ // updates nothing and says nothing. Same root cause as the navigate below. const { setCategory } = useCommandHqCommanderActions(chat.claudeSessionId ?? ''); const [filed, setFiled] = useState(false); + + /* + Has this commander been started — which is NOT the same as "has it any messages". + + `messages.length === 0` was standing in for it, and it lies in exactly the situation that matters. + Lose the socket mid-turn and the messages come back empty, so the setup pane and its Start button + reappear on a commander that is already running; pressing Start then sends the opening brief a + SECOND time, and the agent answers a question nobody asked twice. Seen tonight, after a restart I + ran while a commander was live. + + A latch, not a derived value. Once started, always started — the point is to survive the moment the + live state goes empty, so anything recomputed from that state is the bug rather than the fix. + */ + const [started, setStarted] = useState(false); + useEffect(() => { + if (chat.messages.length > 0 || chat.claudeSessionId || chat.isGenerating) setStarted(true); + }, [chat.messages.length, chat.claudeSessionId, chat.isGenerating]); /* Refresh the grid when the first turn ends. @@ -313,7 +330,7 @@ function NewCommander({ So the scroller is here, where the height is actually constrained, and the composer below keeps `shrink-0` so it stays reachable at the bottom whatever the zoom. */} - {chat.messages.length === 0 && ( + {!started && (
setPickerOpen(true)} /> setPickerOpen(false)} onSelect={setCwd} />