fix: isolate chat SSE broadcasts per generation
After stopping a streaming chat reply, the next message would appear sent but show no Stop button or "Connecting…" indicator. The cancel broadcast from the prior generation was leaking into the new SSE subscription and immediately marking it as errored. Each `chatManager.sendMessage` now allocates a per-generation id; `ChatStreamManager` only delivers tagged broadcasts to subscribers from the matching generation. `sendMessage`'s cleanup also stops deleting a newer generation's `activeGenerations` slot when an older one finally unwinds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -536,7 +536,13 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to session events
|
||||
// Allocate a generation up front so subscription and sendMessage broadcasts
|
||||
// share the same id. This filters out stragglers from a prior, just-cancelled
|
||||
// generation that would otherwise hit this fresh subscriber and falsely look
|
||||
// like an error/done for this request.
|
||||
const { generationId } = chatManager.beginGeneration(sessionId);
|
||||
|
||||
// Subscribe to session events for this generation only.
|
||||
const unsubscribe = chatStreamManager.subscribe(sessionId, (event, eventId) => {
|
||||
const data = (event as { data?: unknown }).data;
|
||||
if (!writeSSEEvent(res, event.type, JSON.stringify(data ?? {}), eventId)) {
|
||||
@@ -549,7 +555,7 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
||||
unsubscribe();
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
}, { generationId });
|
||||
|
||||
// Handle client disconnect
|
||||
req.on("close", () => {
|
||||
@@ -577,7 +583,7 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
||||
chatStreamManager.broadcast(sessionId, {
|
||||
type: "error",
|
||||
data: "modelProvider and modelId must both be provided or neither",
|
||||
});
|
||||
}, { generationId });
|
||||
unsubscribe();
|
||||
res.end();
|
||||
return;
|
||||
@@ -590,6 +596,7 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
||||
normalizedProvider,
|
||||
normalizedModelId,
|
||||
Array.isArray(attachments) ? attachments : undefined,
|
||||
{ generationId },
|
||||
).catch((err: Error) => {
|
||||
chatLogger.error("Error in sendMessage", {
|
||||
error: err.message,
|
||||
@@ -597,7 +604,7 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
||||
chatStreamManager.broadcast(sessionId, {
|
||||
type: "error",
|
||||
data: err.message || "Failed to process message",
|
||||
});
|
||||
}, { generationId });
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
|
||||
Reference in New Issue
Block a user