From 34dee919bb0222c9701a1e47d3a4d3d73572628f Mon Sep 17 00:00:00 2001 From: Amos Date: Sat, 22 Aug 2026 09:58:41 +0300 Subject: [PATCH] Fix job continuation: setMessages never fires sendAutomaticallyWhen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate 1 found the teacher staying silent after a research brief landed. The SDK only evaluates sendAutomaticallyWhen inside addToolOutput โ€” an external setMessages (our syncFromServer) updates the UI but never triggers the continuation. Fix: syncFromServer checks lastAssistantMessageIsCompleteWithToolCalls and calls sendMessage() (itself a valid continuation trigger); plus a mount-continuation effect for the reload case (job fulfilled while no client was open). --- components/chat.tsx | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/components/chat.tsx b/components/chat.tsx index 29fa6de..f187d0c 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -81,6 +81,25 @@ export function Chat({ } }, [initialPrompt, initialMessages.length, sendMessage]); + // Mount continuation: if the stored transcript ends with a fulfilled tool + // (a job result landed while no client was open), resume the lesson โ€” the + // SDK's sendAutomaticallyWhen only fires inside addToolOutput, so reloads + // need this nudge. Once per mount; the turn that follows ends in text (or + // a pending question), so no loop. + const mountContinuation = useRef(false); + useEffect(() => { + if (mountContinuation.current) return; + mountContinuation.current = true; + if ( + status === "ready" && + messages.length > 0 && + lastAssistantMessageIsCompleteWithToolCalls({ messages }) + ) { + void sendMessage(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only by design + }, []); + const pending = findPendingToolCall(messages); const busy = status === "submitted" || status === "streaming"; @@ -102,8 +121,19 @@ export function Chat({ const res = await fetch(`/api/chats/${chatId}`); if (!res.ok) return; const record = (await res.json()) as { messages?: TeacherUIMessage[] }; - if (Array.isArray(record.messages)) setMessages(record.messages); - }, [chatId, setMessages]); + if (!Array.isArray(record.messages)) return; + setMessages(record.messages); + // setMessages does NOT fire sendAutomaticallyWhen (the SDK only checks + // it inside addToolOutput) โ€” without this the teacher would stay silent + // after a job result lands. sendMessage() with no args re-requests with + // the current list: the continuation. (contracts.ts protocol ยง4.) + if ( + status === "ready" && + lastAssistantMessageIsCompleteWithToolCalls({ messages: record.messages }) + ) { + void sendMessage(); + } + }, [chatId, setMessages, sendMessage, status]); const onSend = () => { const t = text.trim();