{"record":{"id":"14dfaa152772bc03","repo":"siyuan-note/siyuan","slug":"ai-editor-stream-idle-timeout","errorCode":null,"errorMessage":"AI editor stream idle timeout","messagePattern":"AI editor stream idle timeout","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/ai.go","lineNumber":144,"sourceCode":"\t\tif \"\" == content || (openai.ChatMessageRoleUser != role && openai.ChatMessageRoleAssistant != role) {\n\t\t\tcontinue\n\t\t}\n\t\tmessages = append(messages, openai.ChatCompletionMessage{Role: role, Content: content})\n\t}\n\treturn append(messages, openai.ChatCompletionMessage{Role: openai.ChatMessageRoleUser, Content: prompt})\n}\n\ntype AIEditorChatStream struct {\n\tstream      *util.OpenAICompletionStream\n\tcancel      context.CancelFunc\n\tidleTimeout time.Duration\n}\n\nfunc (stream *AIEditorChatStream) Recv() (response openai.ChatCompletionStreamResponse, err error) {\n\ttimer, timerDone := startAIEditorCancelTimer(stream.idleTimeout, stream.cancel)\n\tresponse, err = stream.stream.Recv()\n\tif stopAIEditorCancelTimer(timer, timerDone) {\n\t\terr = errors.New(\"AI editor stream idle timeout\")\n\t}\n\treturn\n}\n\nfunc (stream *AIEditorChatStream) Close() {\n\tstream.cancel()\n\tstream.stream.Close()\n}\n\n// NewAIEditorChatStream 创建绑定到编辑器请求生命周期的模型流。\nfunc NewAIEditorChatStream(ctx context.Context, ids []string, input, action string, history []AIEditorMessage) (*AIEditorChatStream, error) {\n\tif !Conf.AI.HasAnyProvider() {\n\t\treturn nil, errors.New(\"no AI provider configured\")\n\t}\n\n\tprov, m := Conf.AI.GetEditingModel()\n\tif nil == prov || nil == m {\n\t\treturn nil, errors.New(\"no AI editing model configured\")","sourceCodeStart":126,"sourceCodeEnd":162,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/model/ai.go#L126-L162","documentation":"AIEditorChatStream.Recv (kernel/model/ai.go:144) arms an idle timer (fixed at 120 seconds in NewAIEditorChatStream) around every Recv on the underlying OpenAI streaming connection. If no SSE chunk arrives within that window, the timer cancels the stream context and Recv replaces the provider's error with this sentinel message. It guards the kernel against a provider that accepts the request but then stalls forever mid-generation.","triggerScenarios":"Streaming an AI editor chat completion where the model stops emitting tokens for more than 120 s: provider-side stall or queueing before the first/next token, dropped connection without RST, or an intermediary (proxy, gateway) that buffers SSE events.","commonSituations":"Self-hosted models under heavy load that pause between tokens; VPN/Wi-Fi drops mid-stream; corporate proxies buffering text/event-stream responses; very long reasoning phases on models that emit nothing while thinking.","solutions":["Check network stability to the provider endpoint, then simply retry the request — the stream is not resumable, it must be re-issued","Shorten the work: reduce the selected content, prompt size, or the editing Max Completion Tokens so the model finishes faster","Switch to a faster or closer provider/model that streams tokens steadily and sends SSE keep-alives","If a proxy sits between kernel and provider, disable SSE buffering for text/event-stream"],"exampleFix":"// before\nresp, err := stream.Recv() // one stall kills the whole interaction\n// after\nvar resp openai.ChatCompletionStreamResponse\nvar err error\nfor attempt := 0; attempt < 2; attempt++ {\n    resp, err = stream.Recv()\n    if err == nil || err.Error() != \"AI editor stream idle timeout\" {\n        break\n    }\n    stream.Close()\n    stream, err = NewAIEditorChatStream(ctx, ids, input, action, history)\n    if err != nil {\n        break\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for {\n    resp, err := stream.Recv()\n    if err != nil {\n        if err.Error() == \"AI editor stream idle timeout\" {\n            stream.Close()\n            stream, err = NewAIEditorChatStream(ctx, ids, input, action, history)\n            if err != nil {\n                return err // config/network problem, stop retrying\n            }\n            continue // bounded by a max-attempt counter in production\n        }\n        return err\n    }\n    emit(resp)\n}","preventionTips":["Bound retries (1-2 attempts) — an idle timeout mid-generation is usually a provider problem, not transient noise","Keep selections and prompts compact so total generation stays well under provider stall tolerance","Prefer providers/endpoints known to stream steadily and emit SSE keep-alives; avoid buffering proxies for text/event-stream"],"tags":["ai","openai","streaming","timeout","sse"],"backgroundTag":"stream-idle-timeout","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}