{"record":{"id":"dbbfb6083c5f64fe","repo":"siyuan-note/siyuan","slug":"model-stream-idle-timeout","errorCode":null,"errorMessage":"model stream idle timeout","messagePattern":"model stream idle timeout","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/agent/agent.go","lineNumber":2515,"sourceCode":"\t\t\te := SessionEntry{\n\t\t\t\tID:                   id,\n\t\t\t\tType:                 \"assistant\",\n\t\t\t\tContent:              m.Content,\n\t\t\t\tReasoningCont:        m.ReasoningContent,\n\t\t\t\tResponseOutput:       util.CloneOpenAIResponseOutput(m.ResponseOutput),\n\t\t\t\tResponseOutputTokens: m.ResponseOutputTokens,\n\t\t\t\tRoundID:              m.RoundID,\n\t\t\t\tToolCalls:            m.ToolCalls,\n\t\t\t}\n\t\t\tentries = append(entries, e)\n\t\t}\n\t}\n\treturn entries\n}\n\nvar (\n\terrModelRequestTimeout    = errors.New(\"model request timeout\")\n\terrModelStreamIdleTimeout = errors.New(\"model stream idle timeout\")\n)\n\nfunc createStreamWithRetry(ctx context.Context, client *openai.Client, req openai.ChatCompletionRequest, maxRetries int,\n\trequestTimeout, streamIdleTimeout time.Duration, retryDelay func(string, int) time.Duration,\n\tch chan<- AgentEvent) (*util.OpenAICompletionStream, openai.ChatCompletionStreamResponse, context.CancelFunc, error) {\n\treturn createProtocolStreamWithRetry(ctx, client, util.OpenAIProtocolChatCompletions, req, nil, maxRetries,\n\t\trequestTimeout, streamIdleTimeout, retryDelay, ch)\n}\n\nfunc createProtocolStreamWithRetry(ctx context.Context, client *openai.Client, protocol string,\n\treq openai.ChatCompletionRequest, responseInput []any, maxRetries int, requestTimeout, streamIdleTimeout time.Duration,\n\tretryDelay func(string, int) time.Duration,\n\tch chan<- AgentEvent) (*util.OpenAICompletionStream, openai.ChatCompletionStreamResponse, context.CancelFunc, error) {\n\tif maxRetries < 0 {\n\t\tmaxRetries = 0\n\t}\n\n\tvar lastErr error","sourceCodeStart":2497,"sourceCodeEnd":2533,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/agent/agent.go#L2497-L2533","documentation":"errModelStreamIdleTimeout is returned by recvStreamWithIdleTimeout when no chunk arrives from the model stream within streamIdleTimeout. A timer wraps stream.Recv(); if it fires before the next chunk (stopCancelTimer returns true), the read is aborted with this sentinel, which classifyRetry maps to the \"timeout\" retry class.","triggerScenarios":"In agent.go (~line 2515/2598): stream.Recv() blocks longer than streamIdleTimeout after connection establishment — the provider opened the stream but stopped sending chunks (server hang, mid-generation stall) — so recvStreamWithIdleTimeout returns errModelStreamIdleTimeout; exercised by TestStreamIdleTimeoutAfterPartialResponse.","commonSituations":"Provider stalls mid-generation (long tool-output generation, overloaded backend); network middleboxes silently drop the connection without TCP FIN; extremely long time between the first token and later tokens; streamIdleTimeout configured too aggressively for a slow model.","solutions":["Increase the stream idle timeout in the AI provider settings to tolerate slow chunk gaps.","Rely on the automatic retry (classifyRetry returns \"timeout\"); if retries exhaust, check provider status.","Switch to a different provider/endpoint if streams repeatedly stall mid-generation.","Check for proxies/load balancers that kill idle HTTP connections and bypass or reconfigure them."],"exampleFix":"// before: aggressive idle timeout aborts slow streams\nresp, err := recvStreamWithIdleTimeout(stream, 5*time.Second, streamCancel)\n// after: tolerate slow chunk gaps\nresp, err := recvStreamWithIdleTimeout(stream, 60*time.Second, streamCancel)","handlingStrategy":"retry","validationCode":"// ensure idle timeout exceeds worst observed inter-chunk gap\nif streamIdleTimeout < 10*time.Second {\n    streamIdleTimeout = 60 * time.Second\n}","typeGuard":null,"tryCatchPattern":"resp, err := recvStreamWithIdleTimeout(stream, streamIdleTimeout, streamCancel)\nif err != nil {\n    if errors.Is(err, errModelStreamIdleTimeout) {\n        // recreate the stream via createStreamWithRetry; classifyRetry maps this to \"timeout\"\n    }\n}","preventionTips":["Configure a streamIdleTimeout tolerant of slow chunk gaps (tens of seconds).","Bypass or reconfigure proxies that kill idle connections.","Prefer providers with reliable streaming; switch away if stalls are frequent.","Keep automatic retry enabled so idle-timeout aborts are transparently restarted."],"tags":["timeout","streaming","llm","network"],"backgroundTag":"request-timeout","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}