fix(chat): cancel orphaned generation on client disconnect
Closing the QuickChat browser tab mid-response left the backend agent
running with no listener. The `activeGenerations` slot persisted and the
next message's freshly-opened CLI SessionManager raced against the
lingering agent on the same session file, so the model produced no
output for the new prompt.
- POST /chat/sessions/:id/messages now calls cancelGeneration on
req.on("close") if the response hasn't ended yet, so disconnects stop
the agent promptly.
- beginGeneration only aborts the prior controller; it no longer
disposes the prior agent. Disposing pre-emptively could yank the CLI
process out from under the new generation's session opened on the
same file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/fix-chat-stale-generation-after-disconnect.md
Normal file
5
.changeset/fix-chat-stale-generation-after-disconnect.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Quick Chat: messages would silently fail after closing the browser tab mid-response and reopening it. The backend agent kept running with no listener and left a stale `activeGenerations` slot; the next message's freshly-opened CLI session then raced against the lingering agent on the same session file. The `/messages` route now calls `chatManager.cancelGeneration` when the client disconnects before the response ended, and `beginGeneration` only aborts the previous generation's controller instead of pre-emptively disposing its agent (the previous agent's own `finally` handles dispose, so we don't tear down the CLI process under the new agent).
|
||||||
@@ -564,16 +564,18 @@ export class ChatManager {
|
|||||||
* first so subscription and broadcast generationIds are tied together.
|
* first so subscription and broadcast generationIds are tied together.
|
||||||
*/
|
*/
|
||||||
beginGeneration(sessionId: string): { generationId: number; abortController: AbortController } {
|
beginGeneration(sessionId: string): { generationId: number; abortController: AbortController } {
|
||||||
|
// If a previous generation is still tracked (e.g. its browser disconnected
|
||||||
|
// mid-stream and its agent loop hasn't reached `finally` yet), abort its
|
||||||
|
// controller so it stops issuing further prompts/tool calls that would
|
||||||
|
// race against the new generation for the same CLI session file.
|
||||||
|
//
|
||||||
|
// We deliberately do NOT dispose its agent here — the previous generation
|
||||||
|
// owns its own dispose in its `finally`. Calling dispose pre-emptively can
|
||||||
|
// yank the underlying CLI process out from under the new generation's
|
||||||
|
// freshly-opened SessionManager pointing at the same session file.
|
||||||
const existing = this.activeGenerations.get(sessionId);
|
const existing = this.activeGenerations.get(sessionId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.abortController.abort();
|
existing.abortController.abort();
|
||||||
if (existing.agentResult) {
|
|
||||||
try {
|
|
||||||
existing.agentResult.session.dispose?.();
|
|
||||||
} catch (err) {
|
|
||||||
diagnostics.error(`Error disposing previous agent session during pre-emption:`, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.generationCounter += 1;
|
this.generationCounter += 1;
|
||||||
const generationId = this.generationCounter;
|
const generationId = this.generationCounter;
|
||||||
|
|||||||
@@ -560,6 +560,15 @@ export function registerChatRoutes(ctx: ApiRoutesContext, deps: ChatRouteDeps):
|
|||||||
// Handle client disconnect
|
// Handle client disconnect
|
||||||
req.on("close", () => {
|
req.on("close", () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
|
// If the response hasn't been ended (i.e. the generation was still
|
||||||
|
// streaming when the browser disconnected — tab close, navigation,
|
||||||
|
// network drop), cancel the agent so it doesn't keep running with
|
||||||
|
// nobody listening. Otherwise the next request for this session sees a
|
||||||
|
// stale `activeGenerations` entry and races against the lingering
|
||||||
|
// agent for the same CLI session file.
|
||||||
|
if (!res.writableEnded) {
|
||||||
|
chatManager.cancelGeneration(sessionId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send heartbeat every 30s to keep connection alive
|
// Send heartbeat every 30s to keep connection alive
|
||||||
|
|||||||
Reference in New Issue
Block a user