Skip to content

Commit 5550c3b

Browse files
TKassisclaude
andcommitted
fix(web): send() claims the tab synchronously so loadSession can't clobber a live run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0ce7ad9 commit 5550c3b

1 file changed

Lines changed: 12 additions & 4 deletions

File tree

web/src/lib/use-agent.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ export function useAgent() {
235235
const [pendingSteers, setPendingSteers] = useState<string[]>([]);
236236
const sessionIdRef = useRef<string | null>(null);
237237
const abortRef = useRef<AbortController | null>(null);
238+
// send() claims the tab synchronously BEFORE its first await: a loadSession
239+
// resolving mid-run must not replace the transcript, because the run's
240+
// plain-value setMessages(transcript) writes would clobber it.
241+
const sendClaimRef = useRef(false);
238242
const messageCounter = useRef(0);
239243

240244
const nextId = () => String(++messageCounter.current);
@@ -246,17 +250,19 @@ export function useAgent() {
246250
* emits, so the restored transcript renders identically.
247251
*/
248252
const loadSession = useCallback(async (sessionId: string): Promise<boolean> => {
249-
// Never swap the session out from under a tab that already has one.
250-
if (sessionIdRef.current) return false;
253+
// Never swap the session out from under a tab that already has one, or
254+
// one where a send has already claimed the transcript.
255+
if (sessionIdRef.current || sendClaimRef.current) return false;
251256
try {
252257
const res = await apiFetch(
253258
`/sessions/${encodeURIComponent(sessionId)}/history`,
254259
);
255260
if (!res.ok) return false;
256261
const data = (await res.json()) as { messages?: HistoryItem[] };
257262
// Re-check after the awaits: a message sent while the history fetch was
258-
// in flight binds the tab to a fresh session, which must win.
259-
if (sessionIdRef.current) return false;
263+
// in flight claims the tab (and will bind a fresh session), which must
264+
// win over hydration.
265+
if (sessionIdRef.current || sendClaimRef.current) return false;
260266
const restored: ChatMessage[] = [];
261267
const fallbackTs = Date.now();
262268
for (const item of data.messages ?? []) {
@@ -347,6 +353,7 @@ export function useAgent() {
347353
computeTarget?: string,
348354
): Promise<string | undefined> => {
349355
if (!text.trim() || status === "submitted" || status === "streaming") return;
356+
sendClaimRef.current = true;
350357

351358
const userMsgId = nextId();
352359
const assistantId = nextId();
@@ -453,6 +460,7 @@ export function useAgent() {
453460
setPendingSteers([]);
454461
setStatus(aborted ? "ready" : "error");
455462
} finally {
463+
sendClaimRef.current = false;
456464
abortRef.current = null;
457465
}
458466

0 commit comments

Comments
 (0)