Skip to content

fix(cli,core): prevent memory monitor starvation during autonomous loops via heartbeat fallback#5097

Draft
zzhenyao wants to merge 5 commits into
QwenLM:mainfrom
zzhenyao:fix/microcompact-oom-V2.6
Draft

fix(cli,core): prevent memory monitor starvation during autonomous loops via heartbeat fallback#5097
zzhenyao wants to merge 5 commits into
QwenLM:mainfrom
zzhenyao:fix/microcompact-oom-V2.6

Conversation

@zzhenyao

Copy link
Copy Markdown
Contributor

What this PR does

Under autonomous agent/goal loops, the event loop has zero idle time — queueMicrotask and setInterval callbacks never fire, so both memory monitors are completely starved. UI history grows until OOM.

Fix: Core's scheduleCheck() detects starvation (≥60s since last successful check) and falls back to synchronous execution. On fallback, it fires a heartbeat callback to CLI. CLI checks if its own setInterval has been silent for ≥60s, and if so runs the full memory check inline.

Why it's needed

During autonomous agent/goal loops, the event loop has no idle time. Both monitors get starved:

  • Core scheduleCheck() uses queueMicrotask — no microtask gap, never executes
  • CLI useMemoryMonitor uses setInterval (30s/60s) — timer phase never reached

UI history grows unbounded

Reviewer Test Plan

Before / After

Before: Both monitors completely starved during autonomous loops. Memory grows until OOM.

After: Core detects starvation after 60s, falls back to synchronous check, notifies CLI via heartbeat. CLI detects its setInterval hasn't run in 60s, runs full check inline.

How to verify

  1. Run existing tests:
cd packages/core && npx vitest run src/services/memoryPressureMonitor.test.ts
cd packages/cli && npx vitest run src/ui/hooks/useMemoryMonitor.test.ts
  1. Build + typecheck:
npm run build && npm run typecheck
  1. Manual: start CLI, run a /goal with continuous tool operations, confirm [MEMORY_USAGE] logs keep appearing (previously stopped after a few minutes).

Tested on

OS Status
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux

Risk & Scope

  • Main risk or tradeoff: Normal path unchanged — starvation fallback only activates after 60s of zero idle time.
  • Breaking changes / migration notes: None. setOnToolCompleteCallback is additive.

Linked Issues

Closes: #4815
Follow-up to #4824 #4892

中文

本 PR 做了什么

自主 agent/goal 循环下事件循环零空闲,queueMicrotasksetInterval 回调完全饿死,两个内存监控器都不执行。UI 历史无限增长。

修复:Core 的 scheduleCheck() 检测饿死(距上次成功检查 ≥60s)后降级为同步执行,同时通过心跳通知 CLI。CLI 发现自己的 setInterval 超过 60s 没跑,主动执行完整内存检查。

为什么需要

自主 agent/goal 循环期间事件循环没有空闲,两个监控器都被饿死:

  • Core scheduleCheck()queueMicrotask — 没有微任务间隙,不执行
  • CLI useMemoryMonitorsetInterval(30s/60s)— timer 阶段到不了

UI 历史无限增长

审查者测试计划

改前 / 改后

改前: 自主循环期间两个监控器完全饿死,内存持续增长直到 OOM。

改后: Core 60s 后检测到饿死,降级同步检查,通过心跳通知 CLI。CLI 发现 setInterval 超 60s 没跑,主动执行完整检查。

如何验证

  1. 运行测试:
cd packages/core && npx vitest run src/services/memoryPressureMonitor.test.ts
cd packages/cli && npx vitest run src/ui/hooks/useMemoryMonitor.test.ts
  1. 构建 + 类型检查:
npm run build && npm run typecheck
  1. 手动:启动 CLI,跑持续工具操作的 /goal,确认 [MEMORY_USAGE] 日志持续输出(之前几分钟后就停了)。

测试环境

操作系统 状态
🍏 macOS N/A
🪟 Windows N/A
🐧 Linux

风险与范围

  • 主要风险或权衡:正常路径不变,饿死兜底仅在零空闲超 60s 时激活。
  • 破坏性变更 / 迁移说明:无。setOnToolCompleteCallback 是新增接口。

@zzhenyao zzhenyao marked this pull request as ready for review June 14, 2026 06:42
Comment thread packages/core/src/services/memoryPressureMonitor.ts
Comment thread packages/core/src/services/memoryPressureMonitor.ts
Comment thread packages/core/src/services/memoryPressureMonitor.ts
Comment thread packages/cli/src/ui/hooks/useMemoryMonitor.ts Outdated
@zzhenyao

Copy link
Copy Markdown
Contributor Author

Thanks for the review! @wenshao All R1 comments addressed:

  • Wrapped onStarvationCallback in try/catch. It crosses the core→CLI boundary and runs in the scheduler's finally block, so an uncaught throw would mask the original tool error.
  • Renamed setOnToolCompleteCallbacksetOnStarvationCallback. It only fires on the starvation fallback path, not on every tool completion.
  • Added [STARVATION] warn log when the 60s fallback triggers, so it's visible in diagnostics.
  • Skipped the shared constant suggestion. The two 60_000 values are independent: one is a starvation threshold, the other is a dedup guard.

qqqys
qqqys previously approved these changes Jun 14, 2026

@qqqys qqqys 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.

Prior critical issue is resolved at d32acbc: the core→CLI starvation callback is now guarded with try/catch, so a callback failure will not mask the original scheduler/tool error. I did not find any remaining critical issue in this pass.

@zzhenyao

Copy link
Copy Markdown
Contributor Author

Thank @qqqys for reviewing and approving!

this.onStarvationCallback?.();
} catch (err) {
debugLogger.error(
`onStarvation callback failed: ${err instanceof Error ? err.message : String(err)}`,

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 new catch block uses the inline pattern err instanceof Error ? err.message : String(err), but getErrorMessage is already imported (line 12) and used 8 other times in this file. getErrorMessage additionally handles error.cause chains and guards against String(error) throwing.

Suggested change
`onStarvation callback failed: ${err instanceof Error ? err.message : String(err)}`,
`onStarvation callback failed: ${getErrorMessage(err)}`,

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Local verification: this PR crashes the CLI on startup — please do not merge as-is

I built d32acbc1a3 and ran it as the real binary on Linux (Node 22.22.2). The CLI fails to start: it throws on mount before reaching the prompt. The unit suite is green and CI/approval looked clean, but the tests were changed in a way that masks the crash.

Blocking: unconditional startup crash

ERROR  useConfig must be used within a ConfigProvider
 - useConfig
 - useMemoryMonitor
 - AppContainer
 - renderWithHooks → … → performWorkOnRoot

A/B (identical launch command, env, and workspace — only the PR's code differs):

Binary Result
pre-fix (merge-base f9080e44) * Type your message or @path/to/file (starts normally)
PR (d32acbc1a3) ERROR useConfig must be used within a ConfigProvider (crashes on mount)

Reproduced on a minimal clean launch (node dist/cli.js --auth-type openai --approval-mode yolo), no special env — it's a React context error, so it happens on every start.

Root cause

useMemoryMonitor now calls useConfig() (packages/cli/src/ui/hooks/useMemoryMonitor.ts), and AppContainer invokes that hook in its component body:

AppContainer.tsx:314   useMemoryMonitor(historyManager);          // hook runs here (body)
AppContainer.tsx:3714  <ConfigContext.Provider value={config}>    // provider only rendered here (return)

gemini.tsx:347 renders <AppContainer> with no ConfigProvider above it, so when useMemoryMonitoruseConfig() runs at line 314 there is no ConfigContext in scope and useConfig() throws (ConfigContext.tsx).

The fix is local: AppContainer already has config in scope — it's destructured from props at AppContainer.tsx:306 (const { settings, config, … } = props;), and used at line 3714. Thread that config into useMemoryMonitor (param/option) instead of calling useConfig() inside the hook. Note: simply swapping useConfig() for a non-throwing useContext(ConfigContext) would stop the crash but leave config undefined in the body, so getMemoryPressureMonitor() would be skipped and the heartbeat would silently never register — the config must actually be threaded.

Why the green tests didn't catch it

Both test edits in this PR mock away the exact integration that breaks:

  • AppContainer.test.tsx (+3) adds vi.mock('./hooks/useMemoryMonitor.js', () => ({ useMemoryMonitor: () => {} })) — the hook is stubbed out, so the test never runs the real useConfig call under AppContainer.
  • useMemoryMonitor.test.ts (+9) adds a vi.mock('../contexts/ConfigContext.js', …) returning a fake getMemoryPressureMonitor, so the hook test never hits the real provider lookup.

So the suite passes while the binary crashes.

Secondary: the new behavior is untested

Mutation test — reverting both source files to merge-base (removing the entire starvation/heartbeat fix) and keeping the PR's tests:

core  memoryPressureMonitor.test.ts → 72 passed   (unchanged)
cli   useMemoryMonitor.test.ts + AppContainer.test.tsx → 97 passed   (unchanged)

Nothing fails — so the starvation fallback and the heartbeat have zero behavioral coverage. memoryPressureMonitor.test.ts is untouched by the PR; the cli test edits are mock-plumbing only.

The mechanism itself is sound (when reachable)

I exercised the real built MemoryPressureMonitor directly (throwaway probe): an explicit starvation state (a pending check + Date.now() advanced >60 s) does run performCheck() synchronously and fire onStarvationCallback once, and a callback that throws is swallowed (the R1 try/catch works). The renamed setOnStarvationCallback and the [STARVATION] warn log are present. So the core logic is fine — it just never gets a chance to run because the app crashes at mount.

Minor: cold-start threshold nuance (fix while you're in here)

lastCheckTime initializes to 0, so now - lastCheckTime > 60_000 is true immediately with real Date.now(). On a fresh monitor, if the first microtask is starved, the 2nd scheduleCheck triggers the synchronous fallback right away rather than after 60 s. The existing dedup test still passes only because the duplicate performCheck's eviction is coincidentally suppressed by the same-millisecond cleanup cooldown (it counts evictNotAccessedSince, not performCheck; my probe shows performCheck actually runs twice). Initializing lastCheckTime = Date.now() in the constructor would make the 60 s threshold hold from process start. (Non-blocking, and moot until the crash is fixed.)

Verdict

Do not merge. This PR crashes the CLI on every startup; the feature it adds never initializes. Needs: (1) thread config into useMemoryMonitor instead of useConfig(); (2) a test that mounts AppContainer with the real (un-mocked) useMemoryMonitor so this regression is actually guarded; ideally (3) the lastCheckTime init tweak. Happy to re-verify once pushed.

中文版(Chinese version)

⚠️ 本地验证:此 PR 会导致 CLI 启动即崩溃 —— 请勿按现状合并

我在 Linux(Node 22.22.2)上将 d32acbc1a3 构建为真实二进制运行。CLI 无法启动:在 mount 阶段抛错,根本到不了输入提示。 单元测试全绿、CI/approval 看起来没问题,但测试改动恰好把这个崩溃掩盖了。

阻断性:无条件启动崩溃

ERROR  useConfig must be used within a ConfigProvider
 - useConfig → useMemoryMonitor → AppContainer → renderWithHooks → …

A/B(启动命令、环境、工作区完全相同,唯一差别是 PR 代码):

二进制 结果
修复前(merge-base f9080e44 * Type your message or @path/to/file(正常启动)
PR(d32acbc1a3 ERROR useConfig must be used within a ConfigProvider(mount 即崩溃)

用最简洁的干净命令(node dist/cli.js --auth-type openai --approval-mode yolo、无特殊环境)也能复现——这是 React context 错误,每次启动都会发生。

根因

useMemoryMonitor 现在调用了 useConfig()packages/cli/src/ui/hooks/useMemoryMonitor.ts),而 AppContainer 在其组件函数体里调用该 hook:

AppContainer.tsx:314   useMemoryMonitor(historyManager);          // hook 在此(函数体)执行
AppContainer.tsx:3714  <ConfigContext.Provider value={config}>    // provider 仅在 return 里渲染

gemini.tsx:347 渲染 <AppContainer> 时其上方没有 ConfigProvider,因此 useMemoryMonitoruseConfig() 在第 314 行执行时作用域内没有 ConfigContextuseConfig() 抛错(ConfigContext.tsx)。

修复就在本地: AppContainer 其实已经持有 config —— 它在 AppContainer.tsx:306 从 props 解构(const { settings, config, … } = props;),并在 3714 行使用。把这个 config 作为参数/选项传给 useMemoryMonitor,不要在 hook 内部调用 useConfig()。注意:仅把 useConfig() 换成不抛错的 useContext(ConfigContext) 能避免崩溃,但函数体里 config 会是 undefined,于是 getMemoryPressureMonitor() 被跳过、心跳静默地永远不会注册——必须真正把 config 传进去。

为什么全绿的测试没抓到

本 PR 的两处测试改动恰好把会崩溃的那段集成 mock 掉了:

  • AppContainer.test.tsx(+3)新增 vi.mock('./hooks/useMemoryMonitor.js', () => ({ useMemoryMonitor: () => {} }))——hook 被打桩,测试根本不会在 AppContainer 下跑真实的 useConfig
  • useMemoryMonitor.test.ts(+9)新增 vi.mock('../contexts/ConfigContext.js', …) 返回假的 getMemoryPressureMonitor,于是 hook 测试不会触发真实的 provider 查找。

所以测试通过、二进制却崩溃。

次要:新行为没有测试覆盖

变异测试——把两个源文件都回退到 merge-base(移除整个 starvation/heartbeat 修复),保留 PR 的测试:

core  memoryPressureMonitor.test.ts → 72 通过(不变)
cli   useMemoryMonitor.test.ts + AppContainer.test.tsx → 97 通过(不变)

没有任何测试失败——说明 starvation 兜底与心跳行为覆盖。memoryPressureMonitor.test.ts 本 PR 未改动;cli 测试改动仅是 mock 管线。

机制本身是对的(只要能跑到)

我直接驱动了真实构建的 MemoryPressureMonitor(一次性探针):显式构造 starvation(有 pending check + Date.now() 推进 >60s)确实会同步执行 performCheck() 并触发一次 onStarvationCallback;回调抛错会被吞掉(R1 的 try/catch 生效)。重命名后的 setOnStarvationCallback[STARVATION] warn 日志都在。所以 core 逻辑没问题——只是因为 app 在 mount 阶段崩溃,它根本没机会运行。

小问题:冷启动阈值(顺手一起修)

lastCheckTime 初始为 0,因此在真实 Date.now()now - lastCheckTime > 60_000 立即为真。新建 monitor 时若第一个 microtask 被饿死,第 2 次 scheduleCheck 会立刻触发同步兜底,而非等满 60s。既有 dedup 测试仍通过,只是因为重复的 performCheck 的 eviction 恰好被同毫秒的 cleanup cooldown 抑制(它统计的是 evictNotAccessedSince 而非 performCheck;我的探针显示 performCheck 实际跑了两次)。在构造函数里 lastCheckTime = Date.now() 可让 60s 阈值从进程启动时起算。(不阻断,且在崩溃修好前无意义。)

结论

不要合并。 此 PR 每次启动都会让 CLI 崩溃,它新增的功能根本不会初始化。需要:(1) 把 config 传给 useMemoryMonitor,不要用 useConfig();(2) 增加一个真正 mount AppContainer + 真实(不 mock)useMemoryMonitor 的测试,以守护此回归;最好再加上 (3) lastCheckTime 初始化的小修。推送后我可以再验证一次。

zzhenyao added 3 commits June 15, 2026 06:04
…nd add behavioral test coverage

- Thread `config` into `useMemoryMonitor` as a parameter instead of calling
`useConfig()` inside the hook, fixing the unconditional startup crash caused
by hook execution before `ConfigContext.Provider` was mounted.
- Initialize `lastCheckTime = Date.now()` so the 60 s starvation threshold
holds from process start.
- Add 5 behavioral tests: 2 for starvation fallback in core, 3 for heartbeat
callback in CLI.
@zzhenyao

Copy link
Copy Markdown
Contributor Author

Thanks for the review! @wenshao All feedback from R2 and the local verification report has been addressed.

  • Fixed startup crash: threaded config into useMemoryMonitor as a parameter instead of calling useConfig() inside the hook.
  • Added 5 behavioral tests for starvation fallback (core) and heartbeat callback (CLI).
  • Added 2 integration tests rendering real AppContainer + real useMemoryMonitor via vi.importActual.
  • Initialized lastCheckTime = Date.now() so the 60 s starvation threshold holds from process start.
  • Switched to getErrorMessage(err) in the starvation catch block for consistency.

@zzhenyao zzhenyao requested a review from wenshao June 14, 2026 22:59
@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@zzhenyao — a process ask for this PR and for future ones, so review cycles aren't spent on things one local run would catch.

Why I'm raising it. In R2 I built this PR as the real binary and it crashed on every startupuseConfig must be used within a ConfigProvider, thrown on mount before the prompt ever rendered — while CI was fully green and the PR even carried an approval. That green was an illusion: the PR's own test edits vi.mock'd away the exact useMemoryMonitorConfigProvider integration that was broken, so the suite passed over a binary that could not start. A deterministic startup crash like that is something a single node dist/cli.js surfaces in seconds — it should not take a reviewer building an A/B against the merge-base to find it.

You've since pushed a startup-crash fix (10f3a9a, threading config in) plus real-AppContainer integration tests — right direction. I have not rebuilt and re-run this round, and that is rather the point: confirming the binary starts cleanly should not be the reviewer's job to discover.

The ask — before pushing, not after review:

  1. Run it as a real binary, not just vitest. npm run build && npm run bundle, then launch node dist/cli.js … and confirm it reaches the prompt. Green unit tests ≠ the program runs — especially when the tests mock the integration you changed.
  2. For behavioral fixes (this one is starvation/OOM), attach your own before/after evidence — e.g. [MEMORY_USAGE] still logging through a real /goal loop, memory flat instead of climbing. Right now "How to verify → Manual" is written for the reviewer to perform; that run is the work that should ship with the PR, not be delegated out of it.
  3. Don't mock the thing under test green. A green suite sitting on top of a crashing binary is worse than a red one — it moves the discovery cost onto review.

This isn't about the idea: the starvation/heartbeat mechanism itself checks out, and your OOM series (#4824 / #4914 / #4982) is solid, landed work. It's specifically the "did I actually run it?" step. If every round needs a reviewer to build and launch the binary just to catch a deterministic startup crash, that's a lot of borrowed time per PR. 🙏

🇨🇳 中文

@zzhenyao —— 一个关于这个 PR、也关于今后的流程要求,免得 review 的精力花在本地跑一次就能发现的问题上。

为什么提这个。 在 R2,我把这个 PR 构建成真实二进制运行,结果每次启动都崩溃——useConfig must be used within a ConfigProvider,在挂载、还没渲染出输入提示符时就抛出——而当时 CI 全绿、PR 甚至已经拿到一个 approval。这个「绿」是假象:PR 自己的测试改动用 vi.mock 把出问题的 useMemoryMonitorConfigProvider 集成正好 mock 掉了,于是测试在一个根本起不来的二进制上全过。这种确定性的启动崩溃,本地跑一次 node dist/cli.js 几秒钟就能暴露——不该等到 reviewer 去对着 merge-base 做 A/B 才发现。

你之后提交了启动崩溃的修复(10f3a9a,把 config 作为参数传入)和真实 AppContainer 的集成测试——方向是对的。这一轮我没有重新构建运行,而这恰恰是重点:确认二进制能干净启动,不该由 reviewer 来发现。

要求——在 push 之前,而不是 review 之后:

  1. 作为真实二进制运行,而不只是 vitest npm run build && npm run bundle,然后启动 node dist/cli.js …,确认能到达输入提示符。单测绿 ≠ 程序能跑——尤其当测试把你改动的集成 mock 掉的时候。
  2. 行为类修复(这次是 starvation/OOM),请附上你自己的 before/after 实证——比如在真实 /goal 循环下 [MEMORY_USAGE] 持续打印、内存不再增长。现在 "How to verify → Manual" 这一步是写给 reviewer 去执行的;那次运行,本应随 PR 一起交付,而不是甩给 review。
  3. 不要为了让测试变绿而 mock 掉被测对象。 一个架在崩溃二进制之上、却全绿的测试套件,比红的更糟——它把发现问题的成本转嫁给了 review。

这不是针对想法本身:starvation/heartbeat 机制本身是成立的,你的 OOM 系列(#4824 / #4914 / #4982)也都是扎实落地的工作。问题专门在「我到底跑过没有」这一步。如果每一轮都要 reviewer 构建并启动二进制,才能抓出一个确定性的启动崩溃,那每个 PR 借用的时间就太多了。🙏

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Verification: prevent memory-monitor starvation via heartbeat fallback

Verdict: PASS for the observable refactor, wiring, and no-regression — verified at runtime. Transparent caveat: the novel 60s-starvation fallback branch itself could not be reproduced at the real surface (a normal async agentic workload does not starve the event loop), so that specific path rests on the PR's unit tests. Details below.

I built the PR in an isolated worktree (real npm ci + build) and drove the real TUI under tmux against a real model (DeepSeek), with debug logging on (QWEN_DEBUG_LOG_FILE=1) so the [MEMORY_USAGE] / [MEMORY_PRESSURE] / [STARVATION] logs are observable.

Claim (my read of the diff): under autonomous loops with zero idle time, Core's queueMicrotask and CLI's setInterval memory monitors starve → UI history grows → OOM. Fix: Core's scheduleCheck() (called on every tool completion, coreToolScheduler.ts:2927) detects ≥60s microtask starvation, runs the check synchronously, and fires onStarvationCallback(); the CLI hook registers that callback and, if its own setInterval has been silent ≥60s, runs runMemoryCheck() inline. useMemoryMonitor is refactored to share runMemoryCheck and now takes config.

Method: worktree .qwen/tmp/review-pr-5097 (head ccc5a51, fresh merge-base f9080e44, 6 files, one author/theme, no inflation). Real model via proxy; QWEN_DEBUG_LOG_FILE=1; debug log at ~/.qwen/debug/latest.

Steps

  1. No startup crash — the CLI launched cleanly with the new useMemoryMonitor({ ...historyManager, config }) wiring. (The PR's own commits fixed a startup crash from config/useConfig in this hook, so this is a real regression gate.) No TypeError/undefined/monitor errors in the debug log.

  2. Core monitor active[MEMORY_PRESSURE] init logged the host limit (15204 MiB) and V8 heap limit (2096 MiB); config.getMemoryPressureMonitor() returns a live monitor (so the CLI heartbeat setOnStarvationCallback registers against a real object).

  3. Refactored normal path works[MEMORY_USAGE] fired on the 30s setInterval with correct fields, e.g. heapUsed=122.1MB … heapUtilization=81.2%. The extracted runMemoryCheck produces identical output to the pre-refactor inline interval.

  4. Monitor keeps running during a busy tool loop (the PR's own success criterion) — I drove a 15-command run_shell_command burst; [MEMORY_USAGE] kept appearing on an exact 30s cadence, zero drift, throughout:

    23:13:34  heapUsed=122.1MB  util=81.2%
    23:14:04  …    23:14:34  …  23:15:04  …  23:15:34
    23:16:04  …    23:16:34  …  23:17:04  …  23:17:34  heapUsed=109.4MB  util=95.4%
    

    The 15 tools each hit scheduleCheck() on completion, and the monitor never missed a tick.

  5. 🔍 Tried to induce the starvation fallback — did NOT fire. Across the whole session (incl. the tool burst) [STARVATION] count stayed 0, and the 30s timer never drifted. That is the honest finding: a normal agentic workload is async (tool I/O, model calls, renders all yield), so timers and microtasks are serviced on time and the loop never starves. True ≥60s microtask starvation requires sustained synchronous event-loop blocking, which isn't reachable through the CLI surface without injecting a synthetic 60s busy-loop into the process.

Sample (debug log — monitor continuity under load)

[MEMORY_MONITOR] [MEMORY_USAGE] heapUsed=122.1MB, heapTotal=150.4MB, rss=288.6MB, … heapUtilization=81.2%   (23:13:34)
[MEMORY_MONITOR] [MEMORY_USAGE] heapUsed=109.4MB, heapTotal=114.7MB, rss=…,        … heapUtilization=95.4%   (23:17:34)
— exact 30s spacing across a 15-tool burst; no [STARVATION], no missed tick

Findings

  • The refactor is safe: the existing memory monitor still runs (correct output, exact cadence, no drift under load) and there is no startup regression — which is the PR's main merge risk, and it's clean.
  • ⚠️ The core new mechanism (60s starvation → synchronous fallback → heartbeat → inline check) was not exercised at runtime. A normal autonomous loop doesn't starve the event loop on this host (timers fired precisely on time even under continuous tool activity). Reproducing the actual fallback needs a sustained synchronous block, which the CLI surface doesn't expose. That branch is covered by the PR's unit tests (memoryPressureMonitor.test.ts / useMemoryMonitor.test.ts, which mock the clock). If you want live coverage, it would take a synthetic ≥60s busy-loop harness, not a normal /goal.
  • Design observation (not a defect): the fallback is tool-completion-driven — it can only run when scheduleCheck() is next called, i.e. when a tool completes. During a true synchronous block nothing runs (by definition); the check fires on the first completion after the block. In autonomous loops tools complete continuously, so there's always a completion to ride on — but a 60s block with no subsequent tool completion would still defer the check.
  • Observation: [MEMORY_USAGE] (and the whole monitor's visibility) is gated behind QWEN_DEBUG_LOG_FILE. The PR's manual test ("confirm [MEMORY_USAGE] logs keep appearing") therefore only works with debug logging enabled. The 60s threshold is hardcoded (no env override), which also makes field repro hard.
  • Per the verify method I did not run the PR's test suite — this is runtime observation, not CI.
中文版本(点击展开)

验证:通过心跳兜底防止内存监控器饿死

结论:通过(PASS)—— 可观察的重构、接线与无回归均已在运行时验证。透明说明: 新增的 60s 饿死兜底分支本身无法在真实界面复现(正常的异步 agent 工作负载不会让事件循环饿死),因此该具体路径依赖 PR 的单元测试。详见下文。

我在隔离 worktree 中构建本 PR(真实 npm ci + 构建),并在 tmux 下驱动真实 TUI,对接真实模型(DeepSeek),开启调试日志(QWEN_DEBUG_LOG_FILE=1)以便观察 [MEMORY_USAGE] / [MEMORY_PRESSURE] / [STARVATION] 日志。

被验证的声明: 零空闲的自主循环下,Core 的 queueMicrotask 与 CLI 的 setInterval 内存监控器都会饿死 → UI 历史增长 → OOM。修复:Core 的 scheduleCheck()(每次工具完成时调用,coreToolScheduler.ts:2927)检测到 ≥60s 微任务饿死后同步执行检查,并触发 onStarvationCallback();CLI hook 注册该回调,若自身 setInterval 超 60s 未跑则内联执行 runMemoryCheck()useMemoryMonitor 被重构以共享 runMemoryCheck,并新增 config 入参。

方法: worktree .qwen/tmp/review-pr-5097(head ccc5a51,新 merge-base f9080e44,6 文件,同一作者同一主题,无膨胀)。真实模型经代理;QWEN_DEBUG_LOG_FILE=1;调试日志在 ~/.qwen/debug/latest

步骤

  1. 无启动崩溃 —— CLI 以新的 useMemoryMonitor({ ...historyManager, config }) 接线干净启动。(PR 自己的提交修复了该 hook 中来自 config/useConfig 的启动崩溃,所以这是真实的回归关口。)调试日志中无 TypeError/undefined/监控器错误。

  2. Core 监控器激活 —— [MEMORY_PRESSURE] 初始化记录了主机上限(15204 MiB)与 V8 堆上限(2096 MiB);config.getMemoryPressureMonitor() 返回真实监控器(CLI 心跳的 setOnStarvationCallback 因此注册在真实对象上)。

  3. 重构后的正常路径有效 —— [MEMORY_USAGE] 按 30s setInterval 输出且字段正确,如 heapUsed=122.1MB … heapUtilization=81.2%。抽取出的 runMemoryCheck 与重构前内联逻辑输出一致。

  4. 繁忙工具循环期间监控器持续运行(PR 自己的成功标准) —— 我驱动了 15 条 run_shell_command 连发;[MEMORY_USAGE] 全程以精确 30s 间隔持续输出,零漂移

    23:13:34  util=81.2%  →  23:14:04 → 23:14:34 → … → 23:17:34  util=95.4%
    

    15 个工具各自在完成时命中 scheduleCheck(),监控器从未漏跳一拍。

  5. 🔍 尝试诱发饿死兜底——未触发。 整个会话(含工具连发)[STARVATION] 计数始终为 0,且 30s 定时器从未漂移。这是诚实的结论:正常 agent 工作负载是异步的(工具 I/O、模型调用、渲染都会让出事件循环),定时器与微任务都按时被服务,循环不会饿死。真正的 ≥60s 微任务饿死需要持续的同步事件循环阻塞,而 CLI 界面无法在不注入合成 60s 忙循环的情况下触达该状态。

样例(调试日志——负载下监控器持续性)

[MEMORY_USAGE] heapUsed=122.1MB … heapUtilization=81.2%   (23:13:34)
[MEMORY_USAGE] heapUsed=109.4MB … heapUtilization=95.4%   (23:17:34)
— 15 工具连发期间精确 30s 间隔;无 [STARVATION],无漏跳

观察

  • 重构是安全的:既有内存监控器仍正常运行(输出正确、间隔精确、负载下无漂移),且无启动回归——这是本 PR 的主要合并风险,结论是干净的。
  • ⚠️ 核心新机制(60s 饿死 → 同步兜底 → 心跳 → 内联检查)未在运行时被执行。 本机上正常自主循环不会让事件循环饿死(持续工具活动下定时器仍精确按时)。复现真正的兜底需要持续同步阻塞,而 CLI 界面不暴露该路径。该分支由 PR 单元测试(memoryPressureMonitor.test.ts / useMemoryMonitor.test.ts,mock 时钟)覆盖。若要运行时覆盖,需要一个合成的 ≥60s 忙循环装置,而非普通 /goal
  • 设计观察(非缺陷):兜底是由工具完成驱动的——只能在下一次 scheduleCheck()(即工具完成)时运行。真正的同步阻塞期间什么都不会运行(按定义如此);检查在阻塞结束后的第一次完成时触发。自主循环中工具持续完成,因此总有完成事件可借;但一个无后续工具完成的 60s 阻塞仍会推迟检查。
  • 观察:[MEMORY_USAGE](及整个监控器的可见性)受 QWEN_DEBUG_LOG_FILE 门控。PR 的手动测试(「确认 [MEMORY_USAGE] 日志持续输出」)因此仅在开启调试日志时有效。60s 阈值是硬编码的(无环境变量覆盖),这也使现场复现困难。
  • 按验证方法,我运行 PR 的测试套件——本报告是运行时观察,而非 CI。

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @zzhenyao!

Template looks good ✓ — all required sections present. Minor: "Evidence (Before & After)" is blank rather than explicitly marked N/A for this non-UI fix, and "Not validated / out of scope" bullet is absent. Not blocking.

Direction: This is squarely in-scope. OOM crashes during autonomous loops are a real reliability problem — the event loop starving queueMicrotask and setInterval is a well-understood failure mode in Node.js. The memory pressure monitor already exists; this PR adds a fallback path for when its normal scheduling mechanism can't fire. Closing #4815 is a meaningful user-facing win.

Approach: The scope is tight and proportional. Three changes: (1) starvation detection in scheduleCheck() with a 60s threshold and synchronous fallback, (2) a heartbeat callback bridging core→CLI, (3) refactoring useMemoryMonitor to accept config as a parameter (fixing the startup crash from R2). The runMemoryCheck extraction is a natural refactor that avoids code duplication. The generation counter to invalidate stale microtasks is a clean pattern. I don't see a materially simpler path — the two monitors live in different packages, so a cross-boundary callback is the right coupling.

Moving on to code review and testing. 🔍

中文说明

感谢贡献,@zzhenyao

模板完整 ✓ —— 所有必需章节均在。小瑕疵:"Evidence (Before & After)" 留空而非标注 N/A(这是非 UI 修复),"Not validated / out of scope" 条目缺失。不阻断。

方向: 完全在项目范围内。自主循环中的 OOM 崩溃是真实的可靠性问题——事件循环饿死 queueMicrotasksetInterval 是 Node.js 中已知的失败模式。内存压力监控器已经存在,本 PR 为其正常调度机制无法触发时添加了兜底路径。关闭 #4815 是有意义的用户体验改善。

方案: 范围紧凑且成比例。三个改动:(1) scheduleCheck() 中的饿死检测(60s 阈值)和同步兜底,(2) 连接 core→CLI 的心跳回调,(3) 重构 useMemoryMonitor 接受 config 参数(修复 R2 的启动崩溃)。runMemoryCheck 的抽取是自然的避免重复的重构。用 generation 计数器让过期微任务失效是干净的模式。没看到更简单的路径——两个监控器在不同包中,跨边界回调是正确的耦合方式。

进入代码审查和测试 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The implementation is clean and well-structured. Six files, one coherent theme, no scope creep.

Core (memoryPressureMonitor.ts): The starvation fallback in scheduleCheck() is correct — the generation counter ensures stale microtasks are discarded after a synchronous check runs, and the pendingCheck flag is properly cleared before the sync path so subsequent calls aren't blocked. The lastCheckTime = Date.now() initialization (from R3 feedback) prevents cold-start false positives. The try/catch around onStarvationCallback is appropriate for a cross-boundary call.

CLI (useMemoryMonitor.ts): The runMemoryCheck extraction via useCallback is clean — it deduplicates the interval path and the starvation-triggered path without introducing a separate helper function. The lastIntervalRunRef tracking and the 60s guard in the heartbeat callback correctly prevent redundant checks when the interval is already running. The cleanup (setOnStarvationCallback(undefined) on unmount) is present.

AppContainer.tsx: The config threading is minimal — one-line change from useMemoryMonitor(historyManager) to useMemoryMonitor({ ...historyManager, config }). No risk of the R2 startup crash since config is destructured from props before the hook call.

Tests: Behavioral coverage is real, not mock-plumbing. The starvation test in core advances Date.now past 60s and asserts performCheck ran (via evictSpy). The CLI heartbeat tests verify both the "interval stale → run" and "interval fresh → skip" paths. The AppContainer integration tests use vi.importActual to run the real hook, which guards against the R2 regression. Good test design.

No blockers found.

Real-Scenario Testing

Built the PR (npm run build && npm run bundle) and launched the bundled CLI under tmux with QWEN_DEBUG_LOG_FILE=1.

Startup (no crash)

┌──────────────────────────────────────────────────────────────────────────┐
│ >_ Qwen Code (v0.18.0)                                                   │
│                                                                          │
│ API Key | qwen3.7-max (/model to change)                                 │
│ ~/work/qwen-code/qwen-code/.qwen/worktrees/triage                        │
└──────────────────────────────────────────────────────────────────────────┘
Tips: Type / to open the command popup; Tab autocompletes slash commands and
 saved prompts.

CLI launched cleanly — no useConfig must be used within a ConfigProvider error. The R2 startup crash is confirmed fixed.

Debug log — memory monitor active

[MEMORY_PRESSURE] Using host memory limit: 15990 MiB
[MEMORY_PRESSURE] Effective memory limit: 15990 MiB; V8 heap limit: 4144 MiB
[MEMORY_MONITOR] [MEMORY_USAGE] heapUsed=97.4MB, heapTotal=103.5MB, rss=195.0MB, external=20.3MB, arrayBuffers=0.4MB, heapUtilization=94.1%

Monitor initialized correctly and [MEMORY_USAGE] fired on the 30s interval with correct fields. No [STARVATION] log — expected, since a normal interactive session doesn't starve the event loop (consistent with @wenshao's runtime verification).

Unit tests

core  memoryPressureMonitor.test.ts  → 74 passed  ✅
cli   useMemoryMonitor.test.ts       → 11 passed  ✅
cli   AppContainer.test.tsx          → 91 passed  ✅
中文说明

代码审查

实现干净且结构清晰。6 个文件,同一主题,无范围膨胀。

Core(memoryPressureMonitor.ts): scheduleCheck() 中的饿死兜底是正确的——generation 计数器确保同步检查运行后过期的微任务被丢弃,pendingCheck 标志在同步路径前被正确清除,后续调用不会被阻塞。lastCheckTime = Date.now() 初始化(来自 R3 反馈)防止了冷启动误报。onStarvationCallback 周围的 try/catch 对跨边界调用是适当的。

CLI(useMemoryMonitor.ts): 通过 useCallback 抽取的 runMemoryCheck 是干净的——它去重了定时器路径和饿死触发路径,没有引入独立的辅助函数。lastIntervalRunRef 跟踪和心跳回调中的 60s 守卫在定时器已运行时正确防止重复检查。清理(unmount 时 setOnStarvationCallback(undefined))存在。

AppContainer.tsx config 传递是最小化的——一行改动。没有 R2 启动崩溃的风险,因为 config 在 hook 调用之前已从 props 解构。

测试: 行为覆盖是真实的,不是 mock 管线。Core 的饿死测试推进 Date.now 超过 60s 并断言 performCheck 运行(通过 evictSpy)。CLI 心跳测试验证了"定时器过期 → 运行"和"定时器新鲜 → 跳过"两条路径。AppContainer 集成测试使用 vi.importActual 运行真实 hook,守护了 R2 回归。测试设计良好。

未发现阻断性问题。

真实场景测试

构建 PR(npm run build && npm run bundle),在 tmux 下以 QWEN_DEBUG_LOG_FILE=1 启动打包后的 CLI。

CLI 干净启动——无 useConfig must be used within a ConfigProvider 错误。R2 启动崩溃已确认修复。

调试日志显示监控器正确初始化,[MEMORY_USAGE] 在 30s 定时器上触发,字段正确。无 [STARVATION] 日志——符合预期,因为正常交互会话不会饿死事件循环。

单元测试全部通过:core 74 + cli hook 11 + AppContainer 91 = 176 测试 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This PR solves a real and painful problem — OOM crashes during autonomous loops when memory monitors get starved — with a minimal, well-targeted fix. The starvation detection via a 60s timestamp check in scheduleCheck() is the right place to put it (tool-completion boundary, where the check can actually run), and the generation counter for invalidating stale microtasks is a clean correctness safeguard.

The R2→R3 iteration is worth noting: the original submission had a startup crash (useConfig outside ConfigProvider) that the tests mocked away. The collaborator caught it via a real binary build, and the author responded by threading config as a parameter, adding real integration tests with vi.importActual, and fixing the lastCheckTime cold-start issue. The PR is materially stronger for that round.

My independent take: I wouldn't have structured this differently. The cross-package callback is the natural coupling for core→CLI, the runMemoryCheck extraction avoids duplication without over-abstracting, and the test coverage actually guards the behavior (not just mock plumbing). The one honest caveat — echoed by the collaborator's runtime verification — is that the 60s starvation path itself is hard to exercise at the real CLI surface, since normal async workloads don't block the event loop that way. The unit tests with mocked clocks are the right coverage for that branch.

Build is clean, all 176 relevant tests pass, CLI starts without errors under tmux, and the memory monitor initializes and runs correctly.

Verdict: Approve.

中文说明

本 PR 解决了一个真实且痛苦的问题——自主循环中内存监控器被饿死导致的 OOM 崩溃——用了最小化、精准的修复。通过 scheduleCheck() 中的 60s 时间戳检测饿死是正确的位置(工具完成边界,检查实际能运行的地方),用 generation 计数器使过期微任务失效是干净的保障措施。

R2→R3 的迭代值得注意:原始提交有一个启动崩溃(useConfigConfigProvider 外部),测试把它 mock 掉了。协作者通过真实二进制构建发现了它,作者随后把 config 作为参数传入、添加了使用 vi.importActual 的真实集成测试、并修复了 lastCheckTime 冷启动问题。PR 因此变得更强。

我的独立判断:不会用不同的方式构建。跨包回调是 core→CLI 的自然耦合,runMemoryCheck 的抽取避免了重复而不过度抽象,测试覆盖真正守护了行为(不仅仅是 mock 管线)。唯一诚实的说明——与协作者的运行时验证一致——60s 饿死路径本身在真实 CLI 界面难以触发,因为正常的异步工作负载不会那样阻塞事件循环。带 mock 时钟的单元测试是该分支的正确覆盖。

构建干净,176 个相关测试全部通过,CLI 在 tmux 下无错误启动,内存监控器正确初始化和运行。

结论:通过。

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.

LGTM, looks ready to ship. ✅

const monitor = config.getMemoryPressureMonitor();
if (!monitor) return;

monitor.setOnStarvationCallback(() => {

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] This heartbeat — the CLI half of the fix — is never registered in the real interactive CLI, so the starvation fallback is dead in production. The unit + AppContainer tests pass only because they stub the monitor.

config.getMemoryPressureMonitor() returns undefined here at mount: the root config's monitor is created inside config.initialize() (config.ts:1776), which is awaited in a post-mount effect (AppContainer.tsx:521-524). At mount, getMemoryPressureMonitor() finds no own and no inherited monitor and returns undefined (config.ts:3780-3798), so if (!monitor) return (line 129) bails. The effect's deps [config, runMemoryCheck] are both stable (config is a fixed prop; compactOldItems is useCallback(…, [])runMemoryCheck stable), so the effect never re-runs after initialize() later creates the monitor. Net: setOnStarvationCallback is never called, and the core monitor's this.onStarvationCallback?.() is always a no-op. Both tests inject a getMemoryPressureMonitor stub that returns a monitor synchronously at render, which never reproduces the "undefined until initialize() resolves" timing.

Fix: gate registration on init readiness so the effect re-runs once the monitor exists — e.g. thread the existing configInitialized state into the hook and add it to this effect's deps (or register from the same effect that awaits config.initialize()), plus a test where getMemoryPressureMonitor() returns undefined first and a real monitor only after an awaited initialize().

Two related gaps to fix alongside (so the feature helps the autonomous-loop scenario it targets):

  • Per-child monitor: subagent/background tool completions call scheduleCheck() on a separate MemoryPressureMonitor that getMemoryPressureMonitor() lazily clones for prototype-inheriting configs (config.ts:3781-3795); that child monitor has no onStarvationCallback, so starvation during subagent loops won't reach the CLI even after the fix above. Propagate/route the callback to child monitors (or register against the root and have children delegate).
  • Core branch reachability: the core fallback if (this.pendingCheck && now - this.lastCheckTime > 60_000) (memoryPressureMonitor.ts:330) appears unreachable from its only caller — scheduleCheck() runs in the finally of the awaited executeSingleToolCall (coreToolScheduler.ts:2927), and the queued microtask drains between awaited completions, so pendingCheck is false at the next call. Worth a test that drives the real awaited sequence rather than two back-to-back synchronous scheduleCheck() calls.

— claude-opus-4-8[1m] via Qwen Code /qreview

@zzhenyao

Copy link
Copy Markdown
Contributor Author

a process ask for this PR and for future ones, so review cycles aren't spent on things one local run would catch.

@wenshao Thanks for the detailed review, sorry for wasting your time.

I ran npm run preflight, then merged the PR into my local-fix branch and built a binary for daily use. Since the PR only enhances the event loop, I assumed it wouldn't affect other functionality. Never actually run the real CLI on the PR branch. When it crashed on startup, I assumed it was a merge conflict with fix/microcompact-oom-V2.6, fixed the conflict, and kept going. So I couldn't tell if the bug was in the PR or from the merge.

This PR wasted your review time, my apologies. Going forward I'll build and run the real CLI directly on the PR branch for verification.

@wenshao

wenshao commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

@zzhenyao Thanks for working on the memory-monitor starvation fix — flagging the key blocker so you can pick it back up when you continue (I see it's still a draft).

The main one is a [Critical]: the heartbeat (the CLI half of the fix) is never wired up in the real interactive CLI, so the fallback is a no-op in production. At mount, config.getMemoryPressureMonitor() returns undefined — the root monitor is created inside config.initialize() (config.ts:1776), which is awaited in a post-mount effect (AppContainer.tsx:521-524). So the registration effect hits if (!monitor) return (useMemoryMonitor.ts:129) and bails, and its deps ([config, runMemoryCheck]) are both stable, so it never re-runs once initialize() later creates the monitor → setOnStarvationCallback is never called. The unit + AppContainer tests pass only because they stub a monitor that exists synchronously at render, which never reproduces the "undefined until initialize() resolves" timing.

Suggested fix: gate registration on init-readiness so the effect re-runs once the monitor exists — e.g. thread the existing configInitialized state into the hook and add it to this effect's deps (or register from the same effect that awaits config.initialize()), plus a test where getMemoryPressureMonitor() returns undefined first and a real monitor only after an awaited initialize().

Two related gaps worth fixing in the same pass, so the feature actually helps the autonomous-loop scenario it targets:

  1. Per-child monitor: subagent/background completions call scheduleCheck() on a separate monitor that getMemoryPressureMonitor() lazily clones for prototype-inheriting configs — that child has no onStarvationCallback, so starvation during subagent loops won't reach the CLI even after the fix above. Route/propagate the callback to child monitors (or register against the root and have children delegate).
  2. Core fallback reachability: the if (this.pendingCheck && now - lastCheckTime > 60_000) branch (memoryPressureMonitor.ts:330) looks unreachable from its only caller — the queued microtask drains between awaited tool completions, so pendingCheck is false at the next call. Worth a test that drives the real awaited sequence rather than two back-to-back synchronous scheduleCheck() calls.

Full detail is in the inline thread on useMemoryMonitor.ts. No rush given it's a draft — ping for a re-review once the heartbeat registration is wired (with a test that reproduces the post-initialize() timing). Thanks!

中文说明

@zzhenyao 感谢推进内存监控 starvation 的修复——把主要卡点标一下,方便你继续(看到还是 draft)。

最主要的是一条 [Critical]:heartbeat(本修复的 CLI 一半)在真实交互式 CLI 里从未被注册,所以这个 fallback 在生产中是空操作。 挂载时 config.getMemoryPressureMonitor() 返回 undefined——根 monitor 是在 config.initialize()(config.ts:1776)里创建的,而它在一个挂载后的 effect 中被 await(AppContainer.tsx:521-524)。于是注册用的 effect 命中 if (!monitor) return(useMemoryMonitor.ts:129)直接返回,且它的依赖([config, runMemoryCheck])都是稳定的,所以等 initialize() 之后创建了 monitor,这个 effect 也不会再跑 → setOnStarvationCallback 永远不会被调用。单测 + AppContainer 测试能过,只是因为它们 stub 了一个在 render 时就同步存在的 monitor,根本没复现"initialize() resolve 之前为 undefined"的时序。

建议修法: 把注册门控在初始化就绪上,让 monitor 存在后 effect 能重跑——比如把现有的 configInitialized 状态传进 hook 并加入该 effect 的依赖(或者直接在那个 await config.initialize() 的 effect 里注册);并补一个测试:getMemoryPressureMonitor() 先返回 undefined,在 await initialize() 之后才返回真实 monitor。

顺带建议同一轮一起修的两个相关缺口,让这个特性真正能帮到它针对的 autonomous-loop 场景:

  1. 每个子 config 的 monitor: subagent/后台完成时调用的是 getMemoryPressureMonitor() 为原型继承 config 惰性克隆出来的另一个 monitor,那个子 monitor 没有 onStarvationCallback,所以即使修好上面那条,subagent 循环中的 starvation 也到不了 CLI。把回调路由/传播到子 monitor(或统一注册到根、子的委托给根)。
  2. core fallback 可达性: if (this.pendingCheck && now - lastCheckTime > 60_000) 这条分支(memoryPressureMonitor.ts:330)从其唯一调用方看似乎不可达——微任务会在两次 await 的工具完成之间清空,所以下次调用时 pendingCheck 已是 false。建议补一个驱动真实 await 时序的测试,而不是连着两次同步调用 scheduleCheck()

完整细节在 useMemoryMonitor.ts 的 inline 线程里。draft 阶段不急——heartbeat 注册接好(并带上能复现 initialize() 后时序的测试)之后,ping 我重新 review。谢谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: Severe OOM with qwen --resume and Escape key broken

4 participants