fix(ai-chat): stop title generation racing the chat stream (provider stall)
A new-chat turn fired the chat stream (streamText) and title generation (generateText) concurrently to the same z.ai coding endpoint. That plan stalls one of two concurrent requests, so the chat stream black-holed for ~300s (undici headers timeout) and the turn hung forever in every browser; the AI SDK then retried 3x. Server logs showed two concurrent POSTs to /chat/completions per turn — one 200 in ~8s, the other "fetch failed after 301209ms". Bypassing the custom undici transport did not help, confirming the cause is the concurrency, not the transport. Move generateTitle from before the response pipe into onFinish, so it runs solo AFTER the stream's provider call completes. A first turn that errors or aborts no longer auto-titles (fallback "Untitled chat" already handles a null title) — acceptable, and it removes the request that was stalling.
This commit is contained in:
@@ -449,6 +449,22 @@ export class AiChatService {
|
||||
});
|
||||
// Lifecycle: release the external MCP clients leased for this turn.
|
||||
await closeExternalClients();
|
||||
|
||||
// Generate the chat title for a freshly created chat AFTER the stream's
|
||||
// provider call has completed — NOT concurrently with it. The z.ai coding
|
||||
// endpoint stalls one of two concurrent requests to the same plan, which
|
||||
// black-holed the chat stream (~300s headers timeout) when title
|
||||
// generation raced it. Running it here (solo, fire-and-forget) avoids the
|
||||
// race; never block the turn on it, swallow any error.
|
||||
if (isNewChat && incomingText) {
|
||||
void this.generateTitle(chatId, workspace.id, incomingText).catch(
|
||||
(err) => {
|
||||
this.logger.warn(
|
||||
`Title generation failed: ${(err as Error)?.message ?? err}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
onError: async ({ error }) => {
|
||||
// NestJS Logger.error(message, stack?, context?): pass the real message
|
||||
@@ -493,18 +509,6 @@ export class AiChatService {
|
||||
},
|
||||
});
|
||||
|
||||
// Fire-and-forget async title generation for a freshly created chat. Never
|
||||
// block the stream on it; swallow any error.
|
||||
if (isNewChat && incomingText) {
|
||||
void this.generateTitle(chatId, workspace.id, incomingText).catch(
|
||||
(err) => {
|
||||
this.logger.warn(
|
||||
`Title generation failed: ${(err as Error)?.message ?? err}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Stream the UI-message protocol straight to the hijacked Node response.
|
||||
// Without onError the AI SDK masks the cause ('An error occurred.') and the
|
||||
// UI shows a generic failure. Surface the real provider message instead.
|
||||
|
||||
Reference in New Issue
Block a user