Skip to content

fix(core): add stream idle watchdog for silent responses - #4256

Open
Alexxigang wants to merge 4 commits into
QwenLM:mainfrom
Alexxigang:fix/stream-idle-watchdog
Open

fix(core): add stream idle watchdog for silent responses#4256
Alexxigang wants to merge 4 commits into
QwenLM:mainfrom
Alexxigang:fix/stream-idle-watchdog

Conversation

@Alexxigang

Copy link
Copy Markdown
Contributor

Summary

  • What changed:
    • adds an idle watchdog around streamed next() calls in GeminiChat
    • links the caller abort signal into an internal controller so stalled provider streams can be cancelled safely
    • surfaces silent-stream timeouts as InvalidStreamError('STREAM_IDLE_TIMEOUT') and reuses existing transient retry handling
    • adds regression coverage for timeout, timer reset on progress, and env-based disablement
  • Why it changed:
    • some provider SSE streams can go silent without closing, which leaves the CLI hanging indefinitely
  • Reviewer focus:
    • timeout / abort lifecycle cleanup
    • retry behavior after STREAM_IDLE_TIMEOUT
    • env gating (QWEN_CODE_STREAM_IDLE_TIMEOUT_MS, QWEN_CODE_DISABLE_STREAM_WATCHDOG)

Validation

  • Commands run:
    cd packages/core
    npx vitest run src/core/geminiChat.test.ts -t "stream idle watchdog"
    npm run test -- src/core/geminiChat.test.ts
    npm run lint -- src/core/geminiChat.ts src/core/geminiChat.test.ts
    npm run typecheck
    npm run build
  • Prompts / inputs used:
    • mocked silent async generator with no further chunks
    • mocked delayed chunk stream with 30ms gaps under a 50ms timeout
  • Expected result:
    • silent streams retry and then fail instead of hanging forever
    • streams that continue making progress within the timeout complete normally
    • the watchdog can be disabled via env for debugging
  • Observed result:
    • all focused tests pass, plus full geminiChat.test.ts, lint, typecheck, and build
  • Quickest reviewer verification path:
    • set QWEN_CODE_STREAM_IDLE_TIMEOUT_MS=50
    • run the new stream idle watchdog tests in src/core/geminiChat.test.ts
    • inspect makeApiCallAndProcessStream() and processStreamResponse() for cleanup and retry flow
  • Evidence (output, logs, screenshots, video, JSON, before/after, etc.):
    • 91 tests passed in src/core/geminiChat.test.ts
    • npm run lint, npm run typecheck, and npm run build all completed successfully

Scope / Risk

  • Main risk or tradeoff:
    • very slow but still healthy streams may now timeout if the configured threshold is set too low
  • Not covered / not validated:
    • live end-to-end reproduction against a real flaky network/provider stream
  • Breaking changes / migration notes:
    • none
    • new optional env vars: QWEN_CODE_STREAM_IDLE_TIMEOUT_MS, QWEN_CODE_DISABLE_STREAM_WATCHDOG

Testing Matrix

🐗 🐰 🐂
npm run N/A
npx N/A N/A
Docker N/A N/A N/A
Podman N/A N/A N/A
Seatbelt N/A N/A N/A

Testing matrix notes:

  • Verified in the local packages/core workspace only.

Linked Issues / Bugs

Fixes #4177

Comment thread packages/core/src/core/geminiChat.ts Outdated
*
* @example
* ```ts
* ``ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] JSDoc @example code fence markers are damaged — double backticks (``ts / ``) instead of triple backticks (```ts / ```). This breaks JSDoc rendering for sendMessageStream.

Suggested change
* ``ts
* ```ts
* const chat = ai.chats.create({model: 'gemini-2.0-flash'});
* const response = await chat.sendMessageStream({
* message: 'Why is the sky blue?'
* });
* for await (const chunk of response) {
* console.log(chunk.text);
* }
* ```

— DeepSeek/deepseek-v4-pro via Qwen Code /review

}, timeoutMs);
});

return Promise.race([nextPromise, timeoutPromise]).finally(clearTimers);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Each stream chunk in the hot while(true) loop allocates a new Promise.race + 2 setTimeout/clearTimeout pairs + 3 Promise objects via streamWatchdog.next(). For a stream with 20-50 chunks this creates measurable overhead. Consider lifting the timeout timer outside the loop and using timeoutId.refresh() per chunk (Node.js timer refresh API) to eliminate per-chunk timer churn while preserving the idle-detection guarantee.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

Comment thread packages/core/src/core/geminiChat.ts Outdated
},
authType: this.config.getContentGeneratorConfig()?.authType,
persistentMode: isUnattendedMode(),
signal: params.config?.abortSignal,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] retryWithBackoff receives only params.config?.abortSignal (the user's signal), not the internal streamAbortController.signal. When the stream idle watchdog fires during a retry delay, retryWithBackoff won't notice the internal abort and will wait the full delay before calling apiCall — which will then immediately fail because streamAbortController.signal is already aborted. Consider passing streamAbortController.signal (or a combined signal) to retryWithBackoff so internal aborts skip unnecessary retry delays.

— DeepSeek/deepseek-v4-pro via Qwen Code /review

@tanzhenxin tanzhenxin added the type/bug Something isn't working as expected label Jun 2, 2026
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream idle watchdog logic is sound — correct lifecycle management, proper retry integration via InvalidStreamError, and adequate test coverage. However, the branch has a character-encoding issue that corrupts em-dashes and JSDoc fences throughout both files, including a runtime string sent to the model.

Encoding corruption (blocker):
UTF-8 em-dashes (, bytes e2 80 94) have been corrupted to mojibake (e.g. e9 97 3f) in ~15 lines across geminiChat.ts and geminiChat.test.ts. The most impactful instance is OUTPUT_RECOVERY_MESSAGE at line 179 — the model will now receive Resume directly é—?no apology instead of Resume directly — no apology. This likely stems from a commit-encoding mismatch on the fork. A rebase with correct UTF-8 handling should fix all instances at once.

Other issues in this diff:

  • The JSDoc @example code fences for sendMessageStream are damaged (triple backticks became double) — see existing inline comment at line 767.
  • The class-level JSDoc block for GeminiChat (/** Chat session that enables sending messages... */) was deleted, likely unintentionally.

Once the encoding corruption is resolved, the watchdog feature itself looks good to merge.

— qwen-code via Qwen Code /review

Comment thread packages/core/src/core/geminiChat.ts Outdated
*/
const OUTPUT_RECOVERY_MESSAGE =
'Output token limit hit. Resume directly no apology, no recap of what ' +
'Output token limit hit. Resume directly �?no apology, no recap of what ' +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] This runtime string has encoding corruption: the em-dash (UTF-8 e2 80 94) became é—? (e9 97 3f). The model will receive garbled text: Resume directly é—?no apology. This is part of a broader encoding issue across the branch — approximately 15 lines in both files have corrupted em-dashes. Please rebase with correct UTF-8 encoding to fix all instances.

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream idle watchdog design is sound: the createStreamIdleWatchdog lifecycle (timer reset per chunk, finally cleanup in processStreamResponse, catch-path cleanup in makeApiCallAndProcessStream) is correct, and the InvalidStreamError('STREAM_IDLE_TIMEOUT') reuse of the existing transient retry budget is the right approach. Test coverage for timeout, timer reset on progress, and env-based disablement is adequate.

However, the branch-wide encoding corruption remains unresolved. At least 19 em-dash (, U+2014) instances across both files are corrupted to \xe9\x97? at the byte level, and arrow characters () are similarly damaged. Critically, this affects the runtime string OUTPUT_RECOVERY_MESSAGE (line 179), which means the model will receive garbled text (Resume directly \xe9\x97?no apology) during output-limit recovery. The JSDoc @example code fences (triple backticks reduced to double backticks at line 767) also need restoration.

These encoding issues must be fixed before merge. A clean rebase from a UTF-8-safe editor or git checkout origin/main -- packages/core/src/core/geminiChat.ts packages/core/src/core/geminiChat.test.ts followed by reapplying only the watchdog logic would resolve this cleanly.

— qwen3-coder via Qwen Code /review

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@Alexxigang heads up — this PR currently has merge conflicts with main and can't be merged as-is. Could you merge main in (or rebase) and resolve them when you get a chance?

Conflicting files:

  • packages/core/src/core/geminiChat.test.ts
  • packages/core/src/core/geminiChat.ts

The rest merges cleanly. Thanks!

中文

@Alexxigang 提个醒 —— 这个 PR 目前和 main 有合并冲突,暂时没法直接合入。方便的时候麻烦把最新的 main merge 进来(或 rebase)解决一下冲突。

冲突文件:

  • packages/core/src/core/geminiChat.test.ts
  • packages/core/src/core/geminiChat.ts

其余文件可以自动合并。谢谢!

@wenshao

wenshao commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Resolve merge conflicts in geminiChat.ts and geminiChat.test.ts between
the stream idle watchdog feature and main's partial-tool-use repair,
coerceUsageCount, normalizeModelToolCallIds, and deferred recording.

The watchdog's while(true)+watchdog stream loop and abort-signal linking
are integrated inside main's try/catch/streamError architecture, with
watchdog cleanup in the finally block of processStreamResponse.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

PR #4256 Merge Resolution Summary

Commit: e0ff6a377fix(core): merge origin/main into fix/stream-idle-watchdog
Base: origin/main
Head: fix/stream-idle-watchdog (99dff95)

Conflicted Files

  1. packages/core/src/core/geminiChat.ts (10 conflict regions)
  2. packages/core/src/core/geminiChat.test.ts (2 conflict regions)

Resolution Strategy

Started from main's clean version of each file (which has the partial-tool-use repair subsystem, coerceUsageCount, normalizeModelToolCallIds, syncFunctionCallsField, deferred recording, etc.) and surgically applied the PR's watchdog changes on top using string-replacement scripts.

Conflict Details — geminiChat.ts

Conflicts 1–2: Constants and types

  • Main: Added recovery-related constants (OUTPUT_RECOVERY_TAIL_CHARS, RECOVERY_OVERLAP_MAX_SCAN_CHARS, etc.) and the partial-tool-use repair subsystem.
  • PR: Added DEFAULT_STREAM_IDLE_TIMEOUT_MS = 90_000, STREAM_IDLE_WARNING_FRACTION = 0.5, and STREAM_IDLE_TIMEOUT to the InvalidStreamErrorType union.
  • Resolution: Both sets kept. Watchdog constants placed after main's recovery constants; STREAM_IDLE_TIMEOUT added to the type union.

Conflicts 3–5: Helper functions

  • Main: Added isStreamWatchdogDisabled absent; no equivalent helpers.
  • PR: Added isStreamWatchdogDisabled(), getStreamIdleTimeoutMs(), linkAbortSignal(), createStreamIdleWatchdog() — all new watchdog infrastructure.
  • Resolution: All four PR functions inserted after main's existing helper block, before the GeminiChat class definition.

Conflicts 6–9: makeApiCallAndProcessStream method

  • Main: Wrapped generateContentStream call with normalizeModelToolCallIds post-processing, added streamError variable + try/catch around stream consumption, post-loop partial-tool-use repair outside the try block.
  • PR: Added AbortController + linkAbortSignal + createStreamIdleWatchdog before retryWithBackoff,

@DragonnZhang DragonnZhang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Stream Idle Watchdog

Finding — cleanupAbortLink not called on success path → abort-listener leak

File: packages/core/src/core/geminiChat.ts, processStreamResponse()

The finally block in processStreamResponse calls streamWatchdog?.cleanup() but does not call cleanupAbortLink(). The cleanupAbortLink function removes the abort event listener that was registered on the parent AbortSignal by linkAbortSignal(). On the normal (non-error) completion path, this listener is never removed.

} finally {
  streamWatchdog?.cleanup();
  // cleanupAbortLink() is missing here
}

Over a long session, one dangling abort listener accumulates on the caller's AbortSignal per successful stream call. If the signal is the same long-lived object (e.g., a session-level controller), this grows without bound.

Fix: Add cleanupAbortLink() to the finally block alongside streamWatchdog?.cleanup():

} finally {
  streamWatchdog?.cleanup();
  cleanupAbortLink?.();
}

The cleanupAbortLink return value from linkAbortSignal() should be captured in makeApiCallAndProcessStream and threaded into processStreamResponse (or called directly in makeApiCallAndProcessStream's own finally block).


Everything else checked out: QWEN_CODE_STREAM_IDLE_TIMEOUT_MS parsing correctly guards against NaN and zero; linkAbortSignal handles an already-aborted signal immediately; STREAM_IDLE_TIMEOUT reaches the isTransientStreamError retry path and is retried like other InvalidStreamError types.


Generated by Claude Code

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution — PR #4256

PR: fix(core): add stream idle watchdog for silent responses

Conflicted file

  • packages/core/src/core/geminiChat.ts

Non-conflicted (auto-merged)

  • packages/core/src/core/geminiChat.test.ts — auto-merged cleanly

Conflict 1: makeApiCallAndProcessStream function body (top)

HEAD (PR): Added stream idle watchdog setup — creates AbortController, links it to the caller's abort signal via linkAbortSignal(), and creates a createStreamIdleWatchdog() instance.

origin/main: Extracted generator variable from the new overrides parameter pattern: const generator = overrides?.contentGenerator ?? this.config.getContentGenerator();

Resolution: Kept both. The watchdog setup comes first, then the generator extraction. They are independent additions to the same function preamble.

Conflict 2: makeApiCallAndProcessStream retry configuration

HEAD (PR): Changed const streamResponse to let streamResponse and wrapped retryWithBackoff() in a try/catch block to ensure watchdog cleanup on error. Used simpler cgConfig?.retryErrorCodes for error codes.

origin/main: Added the overrides pattern for retry variables (authType, extraRetryErrorCodes, persistentMode) to support fallback model chains, but kept const streamResponse without try/catch.

Resolution: Combined both. Used origin/main's overrides-aware variable declarations (authType, extraRetryErrorCodes with overrides fallback, persistentMode) AND HEAD's let + try/catch wrapping for watchdog cleanup.

Verification

  • No conflict markers remain in any file
  • Only packages/core/src/core/geminiChat.ts was modified (the conflicted file)
  • Final diff against origin/main correctly shows all PR watchdog additions (types, functions, watchdog integration in makeApiCallAndProcessStream and processStreamResponse) combined with main's overrides pattern

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code did not run conflict resolution for this request.

PR #4256 does not currently have merge conflicts with main.

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Re-run — maintainer requested fresh triage.

Template looks good ✓

Problem: Real and well-documented. Issue #4177 describes the SSE silent-drop hang with detailed analysis, upstream reference (claude-code), and a clear proposed design. Users on flaky networks get a frozen spinner with no recovery path other than Ctrl+C.

Direction: Aligned. A stream idle watchdog that aborts stalled streams and reuses the existing InvalidStreamError transient retry budget is the right approach — matches what upstream claude-code does, and is the highest-ROI fix for the most painful symptom.

Size: 165 production lines (geminiChat.ts: +157/−8), 181 test lines (geminiChat.test.ts: +179/−2, excluded from count). Well under thresholds.

Approach: Focused — 2 files, clean separation of watchdog logic from existing stream processing. The Promise.race pattern with per-chunk timer reset is straightforward.

One consistency note: openaiContentGenerator already ships its own stream idle watchdog with a different env var (QWEN_STREAM_IDLE_TIMEOUT_MS, default 240s) vs this PR's QWEN_CODE_STREAM_IDLE_TIMEOUT_MS (default 90s). Not a blocker — the two code paths are independent — but worth a follow-up to unify the naming and defaults for users who configure both providers.

Moving on to code review. 🔍

中文说明

重新审查 — 维护者要求重新 triage。

模板完整 ✓

问题:真实且有充分文档。Issue #4177 详细描述了 SSE 静默断开的挂起问题,包含上游参考(claude-code)以及明确的设计方案。用户在弱网环境下只能看到冻结的 spinner,除了 Ctrl+C 无计可施。

方向:对齐。添加流空闲看门狗来中止停滞的流并复用现有 InvalidStreamError 瞬态重试预算是正确做法——与上游 claude-code 一致,是解决最痛点最高 ROI 的修复。

规模:165 行生产代码,181 行测试代码(已排除)。远低于阈值。

方案:聚焦——2 个文件,看门狗逻辑与现有流处理干净分离。Promise.race + 逐块重置计时器模式简洁明了。

一致性备注:openaiContentGenerator 已有自己的流空闲看门狗,使用不同的环境变量(QWEN_STREAM_IDLE_TIMEOUT_MS,默认 240s)与本 PR 的 QWEN_CODE_STREAM_IDLE_TIMEOUT_MS(默认 90s)。不阻塞合并——两条代码路径是独立的——但建议后续统一命名和默认值。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Re-run — reviewing the latest diff after maintainer's re-triage request.

One confirmed bug, unchanged from the previous triage:

cleanupAbortLink not called in processStreamResponse finally block. The finally block at line ~3645 calls streamWatchdog?.cleanup() but omits cleanupAbortLink?.(). The linkAbortSignal() function registers an abort event listener on the caller's AbortSignal — without cleanup, that listener leaks on every successful stream completion. Over a long session with a session-level controller, this accumulates unboundedly.

Note: the catch block in makeApiCallAndProcessStream correctly calls both streamWatchdog?.cleanup() and cleanupAbortLink() — so the error path is fine. Only the happy path through processStreamResponse leaks.

Fix is one line:

} finally {
  streamWatchdog?.cleanup();
  cleanupAbortLink?.();  // ← add this
}

Same issue flagged by @DragonnZhang and the prior triage run.

Everything else checks out:

  • Watchdog lifecycle is correct — timer starts on next(), resets via clearTimers in .finally(), fires InvalidStreamError('STREAM_IDLE_TIMEOUT') on timeout.
  • STREAM_IDLE_TIMEOUT reaches isTransientStreamError (via error instanceof InvalidStreamError) and is retried within the existing transient budget — verified at line 2516.
  • linkAbortSignal correctly handles already-aborted signals and propagates to the internal controller.
  • getStreamIdleTimeoutMs properly guards against NaN, zero, and negative values.
  • Warning at 50% timeout is useful for debuggability.

Non-blocking notes:

  • Per-chunk Promise.race + 2×setTimeout could use timeoutId.refresh() for lower overhead. Not a blocker — the 90s default means timer churn is minimal.
  • openaiContentGenerator already ships its own watchdog with QWEN_STREAM_IDLE_TIMEOUT_MS (default 240s). This PR uses QWEN_CODE_STREAM_IDLE_TIMEOUT_MS (default 90s). A follow-up to unify the naming would be nice but is not blocking.

Testing

Unit tests: could not re-run locally (npm install timed out in this environment). The previous triage confirmed 195/195 pass (3 new watchdog tests + 192 existing), and the diff has not changed since. The new tests cover all three critical paths: silent stream timeout → retry exhaustion, timer reset on chunk arrival, and env-var disablement.

Tmux before/after: N/A for this PR. The fix targets provider-side silent stream stalls that cannot be reproduced locally without network manipulation. The unit tests comprehensively cover the watchdog behavior with fake timers and mock streams.

中文说明

代码审查

重新审查 — 维护者要求重新 triage 后的最新 diff 审查。

一个已确认的 bug,与上次相同:

cleanupAbortLink 未在 processStreamResponse 的 finally 块中调用。 linkAbortSignal() 在调用方的 AbortSignal 上注册了 abort 事件监听器——不清理的话,每次成功的流完成都会泄漏一个监听器。注意:makeApiCallAndProcessStream 的 catch 块已正确调用两者——只有 processStreamResponse 的正常路径泄漏。修复只需一行代码。@DragonnZhang 和之前的 triage 均已指出。

其他部分均已验证正确: 看门狗生命周期、STREAM_IDLE_TIMEOUT 重试路径(已验证 isTransientStreamError 在第 2516 行正确匹配)、linkAbortSignal 信号传播、超时值校验、半超时警告。

非阻塞备注: 逐块 Promise.race 开销可用 timeoutId.refresh() 优化;openaiContentGenerator 已有自己的看门狗使用不同的环境变量名和默认值,建议后续统一。

测试

单元测试:本地环境无法重新运行(npm install 超时)。上次 triage 确认 195/195 通过,且 diff 自此未有变更。新测试覆盖了三个关键路径。

Tmux before/after:本 PR 不适用。 该修复针对 provider 侧的静默流停滞,无法在本地环境中复现。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Re-run verdict — same as before.

The design is sound — a focused, 165-line watchdog that follows the upstream claude-code pattern and solves a real user pain point. The approach matches what I'd propose independently: Promise.race per chunk with an idle timer, wired into the existing InvalidStreamError retry budget, with env-var gating for escape.

One bug still blocks merge: the cleanupAbortLink listener leak in processStreamResponse's finally block. It's a one-line fix that both the prior triage and @DragonnZhang have flagged. The error path in makeApiCallAndProcessStream already cleans up correctly — only the happy path through processStreamResponse needs the same one-liner added to its finally block.

The maintainers' existing CHANGES_REQUESTED reviews already gate this PR correctly — not re-submitting to avoid noise. Once @Alexxigang pushes the cleanupAbortLink fix, this is ready to ship.

中文说明

重新审查结论——与之前相同。

设计合理——165 行的看门狗实现,遵循上游 claude-code 的模式,解决了真实的用户痛点。方案与独立提案一致:逐块 Promise.race + 空闲计时器,接入现有 InvalidStreamError 重试预算,环境变量控制退出。

一个 bug 仍然阻塞合并:processStreamResponse 的 finally 块中 cleanupAbortLink 监听器泄漏。只需一行修复,之前的 triage 和 @DragonnZhang 均已指出。makeApiCallAndProcessStream 的错误路径已正确清理——只需在 processStreamResponse 的 finally 块中添加相同的一行。

维护者的现有 CHANGES_REQUESTED 审查已正确把关——不再重复提交。一旦 @Alexxigang 推送 cleanupAbortLink 修复,即可合并。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Downgraded from Approve to Comment: CI failing (Test on ubuntu-latest, Node 22.x).

— qwen3.7-max via Qwen Code /review

} catch (e) {
streamError = e;
} finally {
streamWatchdog?.cleanup();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The for await (const chunk of streamResponse) loop was replaced with a manual while (true) + streamResponse.next() loop to interpose the watchdog's Promise.race. However, for await...of implicitly calls streamResponse.return() on early exit (exception thrown inside the loop body, or from yield chunk if the consumer throws), which triggers the underlying generator's internal finally blocks. The manual loop never calls streamResponse.return(), so the SDK's async generator is abandoned without explicit finalization on error paths.

This is a regression for all error paths through processStreamResponse, not just watchdog timeouts — any mid-iteration throw (from normalizeModelToolCallIds, syncFunctionCallsField, or the yield chunk itself) now skips the generator's cleanup.

Suggested change
streamWatchdog?.cleanup();
streamWatchdog?.cleanup();
await streamResponse.return?.();

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No review findings. Downgraded from Approve to Comment: CI failing (Test (ubuntu-latest, Node 22.x)). Suggestion-level recommendations are in the Suggestion summary comment below.

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Suggestions — commit e0a3b42

File Issue Suggested fix
packages/core/src/core/geminiChat.ts:~1131 InvalidStreamError timeout message omits the model name — the warning log above includes it, but the error that propagates to the user does not Include model: `Model ${model} stream went idle for ${timeoutMs}ms without receiving a chunk.`
packages/core/src/core/geminiChat.ts:~1128 abortController.abort() called with no reason — watchdog timeout is indistinguishable from user-initiated abort at the signal level Pass a reason: abortController.abort(new Error(\Stream idle timeout after ${timeoutMs}ms`))`
packages/core/src/core/geminiChat.test.ts No test verifies the positive case of caller abort signal propagation through linkAbortSignal to the internal streamAbortController Add a test that calls abortController.abort() on a slow stream and verifies the internal signal is also aborted

— qwen3.7-max via Qwen Code /review

} catch (e) {
streamError = e;
} finally {
streamWatchdog?.cleanup();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] cleanupAbortLink() is passed to processStreamResponse but never invoked. The finally block only calls streamWatchdog?.cleanup(), so the abort event listener attached by linkAbortSignal on the caller's AbortSignal is never removed on any path that enters processStreamResponse (success, stream error, or watchdog abort). In long sessions, listeners accumulate — Node.js emits MaxListenersExceededWarning after 10+ and the orphaned closures retain references preventing GC.

Suggested change
streamWatchdog?.cleanup();
} finally {
streamWatchdog?.cleanup();
cleanupAbortLink?.();
}

— qwen3.7-max via Qwen Code /review

@yiliang114 yiliang114 added this to the v1.0.0 milestone Jul 13, 2026
github-merge-queue Bot pushed a commit that referenced this pull request Jul 21, 2026
…7389)

* fix(ci): stop /resolve reports from being guillotined mid-sentence

Every substantive /resolve summary was hitting the 2000-byte cap exactly
and stopping mid-word: #2993, #4256 and #6206 all ended at 2100 bytes
total, cut inside a sentence, with nothing saying the report had been
clipped rather than abandoned.

Two causes, both fixed:

- The contract asked for a file-by-file inventory, which duplicates the
  diff and grows without bound. It now asks for what only the resolver
  knows — the root cause on the base branch, whether the merge was
  semantic or merely textual, what the resolution's correctness rests
  on, and what it could not verify (this command runs no tests and may
  not touch non-conflicted files, so a merge that breaks an untouched
  test can only be reported).
- The cap was silent and too low. It is now 6000, above the 4000 the
  prompt asks for, and a report that still exceeds it says so.

Also adds the project's collapsed Chinese section to the contract; no
/resolve report had one.

* fix(ci): make the truncation test fatal-decode real and link the run in the notice (#7389)

---------

Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SSE stream idle watchdog to abort hung streams (weak-network hang)

7 participants