Skip to content

fix(core): require integer microcompaction keep count#5652

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/microcompact-keep-recent-integer
Jun 23, 2026
Merged

fix(core): require integer microcompaction keep count#5652
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/microcompact-keep-recent-integer

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR makes the microcompaction keep-recent count resolve to a safe integer before it is used for slicing and diagnostic metadata. Invalid QWEN_MC_KEEP_RECENT values now fall back to context.clearContextOnIdle.toolResultsNumToKeep, invalid settings values fall back to the default, and integer values below 1 are still floored to 1.

It also updates the settings schema, generated VS Code schema, and user docs so context.clearContextOnIdle.toolResultsNumToKeep is described as an integer count.

Why it's needed

QWEN_MC_KEEP_RECENT=1.5 was accepted because the previous code only checked Number.isFinite(...). JavaScript array slicing coerces that fractional count, but meta.keepRecent and mediaKept could still report fractional values. This made the retention behavior and diagnostic metadata disagree.

Reviewer Test Plan

How to verify

Review the new microcompactHistory tests for fractional, unsafe, and fallback keep-recent values. The key behavior to confirm is that invalid env values fall back to settings, invalid settings fall back to 5, and valid integers below 1 still resolve to 1.

Commands run locally:

  • npx vitest run src/services/microcompaction/microcompact.test.ts --coverage.enabled=false from packages/core - 53 tests passed.
  • npx prettier --check packages/core/src/services/microcompaction/microcompact.ts packages/core/src/services/microcompaction/microcompact.test.ts packages/cli/src/config/settingsSchema.ts docs/users/configuration/settings.md packages/vscode-ide-companion/schemas/settings.schema.json - passed.
  • npx eslint packages/core/src/services/microcompaction/microcompact.ts packages/core/src/services/microcompaction/microcompact.test.ts packages/cli/src/config/settingsSchema.ts - passed.
  • git --no-pager diff --check - passed.
  • node --import tsx/esm scripts/generate-settings-schema.ts - regenerated packages/vscode-ide-companion/schemas/settings.schema.json.
  • npm run typecheck --workspace @qwen-code/qwen-code-core - passed.
  • npm run typecheck --workspace @qwen-code/qwen-code - passed.
  • npm run build --workspace @qwen-code/qwen-code-core && npm run build --workspace @qwen-code/qwen-code - passed.

Evidence (Before & After)

Before: fractional env values such as QWEN_MC_KEEP_RECENT=1.5 could flow into meta.keepRecent, while slicing coerced the value differently. After: the new tests assert that fractional env values fall back to settings, unsafe env values are ignored, fractional settings fall back to the default, and metadata reports integer counts.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local macOS development environment with the repository npm workspaces installed.

Risk & Scope

  • Main risk or tradeoff: Existing fractional keep-recent values no longer partially apply; they fall back to the next valid source instead.
  • Not validated / out of scope: Windows and Linux were not tested locally.
  • Breaking changes / migration notes: No migration is needed. Existing integer values keep the same behavior, including flooring integers below 1 to 1.

Linked Issues

Fixes #5648

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

中文说明

这个 PR 做了什么

这个 PR 会先把 microcompaction 的 keep-recent 数量解析成安全整数,再用于切片和诊断元数据。无效的 QWEN_MC_KEEP_RECENT 现在会回退到 context.clearContextOnIdle.toolResultsNumToKeep,无效的 settings 值会回退到默认值,低于 1 的整数仍然会按 1 处理。

它还同步更新了 settings schema、生成的 VS Code schema 和用户文档,把 context.clearContextOnIdle.toolResultsNumToKeep 描述为整数数量。

为什么需要

之前 QWEN_MC_KEEP_RECENT=1.5 会被接受,因为旧逻辑只检查了 Number.isFinite(...)。JavaScript 数组切片会对这个小数做隐式转换,但 meta.keepRecentmediaKept 仍可能报告小数值,导致实际保留行为和诊断元数据不一致。

Reviewer Test Plan

如何验证

查看新增的 microcompactHistory 测试,覆盖小数、不安全整数和 keep-recent 回退逻辑。需要确认的关键行为是:无效 env 值会回退到 settings,无效 settings 会回退到 5,低于 1 的有效整数仍会解析为 1

本地运行过的命令:

  • npx vitest run src/services/microcompaction/microcompact.test.ts --coverage.enabled=false,在 packages/core 下运行,53 个测试通过。
  • npx prettier --check packages/core/src/services/microcompaction/microcompact.ts packages/core/src/services/microcompaction/microcompact.test.ts packages/cli/src/config/settingsSchema.ts docs/users/configuration/settings.md packages/vscode-ide-companion/schemas/settings.schema.json,通过。
  • npx eslint packages/core/src/services/microcompaction/microcompact.ts packages/core/src/services/microcompaction/microcompact.test.ts packages/cli/src/config/settingsSchema.ts,通过。
  • git --no-pager diff --check,通过。
  • node --import tsx/esm scripts/generate-settings-schema.ts,重新生成了 packages/vscode-ide-companion/schemas/settings.schema.json
  • npm run typecheck --workspace @qwen-code/qwen-code-core,通过。
  • npm run typecheck --workspace @qwen-code/qwen-code,通过。
  • npm run build --workspace @qwen-code/qwen-code-core && npm run build --workspace @qwen-code/qwen-code,通过。

证据(Before & After)

Before:像 QWEN_MC_KEEP_RECENT=1.5 这样的小数 env 值可能进入 meta.keepRecent,但切片逻辑会用不同方式隐式转换该值。After:新增测试断言小数 env 值会回退到 settings,不安全整数 env 值会被忽略,小数 settings 会回退到默认值,并且元数据报告整数计数。

已测试平台

OS 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

环境(可选)

本地 macOS 开发环境,已安装仓库 npm workspaces 依赖。

风险和范围

  • 主要风险或取舍:已有的小数 keep-recent 值不再部分生效,而是回退到下一个有效来源。
  • 未验证或不在范围内:没有在本地测试 Windows 和 Linux。
  • 破坏性变更或迁移说明:不需要迁移。已有整数值保持原有行为,包括低于 1 的整数仍按 1 处理。

关联 Issue

Fixes #5648

AI 协助披露

我使用 Codex 审查变更、对照现有模式做 sanity check,并帮助发现潜在边界情况。

@tt-a1i
tt-a1i marked this pull request as ready for review June 22, 2026 15:40
@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @tt-a1i! (Re-run after post-review commit update.)

Template looks good ✓

On direction: straightforward bugfix for #5648 — fractional QWEN_MC_KEEP_RECENT values produce inconsistent diagnostic metadata (meta.keepRecent reports 1.5 while Array.slice(-1.5) coerces differently). Clearly within scope: correctness of core compaction logic.

On approach: the scope is tight and every change earns its place. The resolveKeepRecent() extraction gives a clean three-tier fallback (env → settings → default 5) with proper integer validation — regex /^-?\d+$/ rejects fractional strings before Number() conversion, Number.isSafeInteger() catches unsafe values, and Math.max(1, value) floors to 1. Schema and docs updates match the behavioral contract. Tests cover edge cases comprehensively (fractional, unsafe integer, empty env, fallback paths — 9 new test cases total).

Moving on to code review. 🔍

中文说明

感谢 @tt-a1i 的贡献!(审查后提交更新,重新运行。)

模板完整 ✓

方向:这是对 #5648 的直接 bugfix——小数 QWEN_MC_KEEP_RECENT 值导致诊断元数据不一致(meta.keepRecent 报告 1.5,但 Array.slice(-1.5) 的隐式转换方式不同)。完全在范围内:核心压缩逻辑的正确性。

方案:范围紧凑,每处改动都有必要。resolveKeepRecent() 提取提供了清晰的三级回退(env → settings → 默认值 5),配合严格的整数验证——正则 /^-?\d+$/Number() 转换前拒绝小数,Number.isSafeInteger() 捕获不安全值,Math.max(1, value) 下限为 1。Schema 和文档更新与行为契约一致。测试全面覆盖边界情况(小数、不安全整数、空 env、回退路径——共 9 个新测试用例)。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Code Review

The resolveKeepRecent extraction is clean and correct. Three-tier fallback (env → settings → default 5) with proper validation: regex /^-?\d+$/ rejects fractional strings before Number() conversion, Number.isSafeInteger() catches unsafe values, Math.max(1, value) floors to 1. The jsonSchemaOverride in settingsSchema.ts correctly declares the type as integer for VS Code schema consumers. No critical blockers, no AGENTS.md violations. Reuse check: nothing to flag — this is a small, specific validation with no existing equivalent utility.

Real-Scenario Testing

Internal-metadata bug — no TUI change. Tested by driving the real microcompactHistory function via tsx with 8 scenarios across both branches.

Before (main branch — bug present)

S1: env=1.5, settings=2
  keepRecent=1.5, toolsKept=1, toolsCleared=3, mediaKept=1.5, mediaCleared=1
  triggerReason=idle
S2: env=9007199254740990.5, settings=1
  keepRecent=undefined, toolsKept=undefined, toolsCleared=undefined, mediaKept=undefined, mediaCleared=undefined
  triggerReason=undefined
S3: env=9007199254740992, settings=1
  keepRecent=undefined, toolsKept=undefined, toolsCleared=undefined, mediaKept=undefined, mediaCleared=undefined
  triggerReason=undefined
S4: no env, settings=1.5
  keepRecent=1.5, toolsKept=1, toolsCleared=3, mediaKept=1.5, mediaCleared=1
  triggerReason=idle
S5: no env, settings=MAX_SAFE+1
  keepRecent=undefined, toolsKept=undefined, toolsCleared=undefined, mediaKept=undefined, mediaCleared=undefined
  triggerReason=undefined
E1: env="", settings=2
  keepRecent=1, toolsKept=1, toolsCleared=3, mediaKept=1, mediaCleared=1
  triggerReason=idle
C1: env=3, settings=2
  keepRecent=3, toolsKept=3, toolsCleared=1, mediaKept=2, mediaCleared=0
  triggerReason=idle
C2: env=0, settings=2
  keepRecent=1, toolsKept=1, toolsCleared=3, mediaKept=1, mediaCleared=1
  triggerReason=idle

After (this PR — bug fixed)

S1: env=1.5, settings=2
  keepRecent=2, toolsKept=2, toolsCleared=2, mediaKept=2, mediaCleared=0
  triggerReason=idle
S2: env=9007199254740990.5, settings=1
  keepRecent=1, toolsKept=1, toolsCleared=3, mediaKept=1, mediaCleared=1
  triggerReason=idle
S3: env=9007199254740992, settings=1
  keepRecent=1, toolsKept=1, toolsCleared=3, mediaKept=1, mediaCleared=1
  triggerReason=idle
S4: no env, settings=1.5
  keepRecent=undefined, toolsKept=undefined, toolsCleared=undefined, mediaKept=undefined, mediaCleared=undefined
  triggerReason=undefined
S5: no env, settings=MAX_SAFE+1
  keepRecent=undefined, toolsKept=undefined, toolsCleared=undefined, mediaKept=undefined, mediaCleared=undefined
  triggerReason=undefined
E1: env="", settings=2
  keepRecent=2, toolsKept=2, toolsCleared=2, mediaKept=2, mediaCleared=0
  triggerReason=idle
C1: env=3, settings=2
  keepRecent=3, toolsKept=3, toolsCleared=1, mediaKept=2, mediaCleared=0
  triggerReason=idle
C2: env=0, settings=2
  keepRecent=1, toolsKept=1, toolsCleared=3, mediaKept=1, mediaCleared=1
  triggerReason=idle

Key behavioral differences

Scenario Before (bug) After (fixed)
S1 env=1.5 keepRecent=1.5, mediaKept=1.5fractional metadata keepRecent=2 ← falls back to settings
S2 env=9007199254740990.5 meta=undefined — compaction silently disabled keepRecent=1 ← falls back to settings
S3 env=9007199254740992 (unsafe) meta=undefined — compaction silently disabled keepRecent=1 ← falls back to settings
S4 settings=1.5 keepRecent=1.5, mediaKept=1.5fractional metadata falls back to default 5 (meta=undefined = no compactable items exceed threshold)
E1 env="" (blank) keepRecent=1 ← empty string coerced to 0→1 keepRecent=2 ← falls back to settings
C1/C2 valid integers identical identical ✓

Unit Tests

$ cd packages/core && npx vitest run src/services/microcompaction/microcompact.test.ts --coverage.enabled=false

 RUN  v3.2.4

 ✓ src/services/microcompaction/microcompact.test.ts (56 tests) 22ms

 Test Files  1 passed (1)
      Tests  56 passed (56)
   Duration  427ms

All 56 tests pass, including 9 new tests covering fractional, unsafe, empty-env, and fallback scenarios.

中文说明

代码审查

resolveKeepRecent 提取干净且正确。三级回退(env → settings → 默认值 5),验证合理:正则 /^-?\d+$/Number() 转换前拒绝小数,Number.isSafeInteger() 捕获不安全整数,Math.max(1, value) 下限为 1。settingsSchema.ts 中的 jsonSchemaOverride 正确将类型声明为 integer,供 VS Code schema 使用。无关键阻塞项,无 AGENTS.md 违规。复用检查:无需标记——这是一个特定场景的小型验证逻辑,无现有等价工具函数。

实际场景测试

内部元数据 bug——无 TUI 变更。通过 tsx 驱动真实的 microcompactHistory 函数,在两个分支上运行 8 个场景。

关键行为差异: S1(env=1.5)Before 报告小数 metadata keepRecent=1.5;After 回退到 settings=2。S2/S3(不安全整数 env)Before 静默禁用压缩;After 正确回退到 settings。E1(空 env)Before 把空字符串当作 0→1;After 正确回退到 settings。C1/C2(有效整数)两个版本行为一致 ✓。56 个单元测试全部通过,包括 9 个新增边界测试。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

This is a well-scoped bugfix that does exactly what it says. The resolveKeepRecent extraction is the right approach — clean three-tier fallback with proper integer validation at each level. The before/after evidence confirms all three failure modes from #5648 are fixed: fractional env values now fall back to settings (instead of producing inconsistent metadata), unsafe integers fall back (instead of silently disabling compaction), and blank env strings fall back to settings (instead of coercing to 0→1). All valid integer inputs remain byte-identical.

56 tests pass including 9 new edge-case tests. The post-review commit added tests for the reviewer's feedback (happy-path, zero/negative floor) without scope creep. Schema and docs are updated to match.

Approving. ✅

中文说明

这是一个范围明确的 bugfix,完全按描述实现。resolveKeepRecent 提取是正确的方案——清晰的三级回退,每级都有严格的整数验证。Before/after 证据确认 #5648 中的三种失败模式均已修复:小数 env 值现在回退到 settings(而不是产生不一致的元数据),不安全整数回退(而不是静默禁用压缩),空 env 字符串回退到 settings(而不是隐式转换为 0→1)。所有有效整数输入保持完全一致。

56 个测试全部通过,包括 9 个新边界测试。审查后的提交根据 reviewer 反馈增加了测试(正常路径、零/负数下限),无范围蔓延。Schema 和文档已同步更新。

批准合并。✅

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. ✅

@wenshao

wenshao commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification — recommend merge

I verified this PR on Linux by building two real @qwen-code/qwen-code-core dist artifacts — the PR head vs. its parent 76bdabad — and driving the actual compiled microcompactHistory from both with identical inputs in tmux, rather than only re-running the unit tests. This exercises the shipped code path, not the TypeScript source.

Method

  • AFTER = PR head 6ac6bbe6. BEFORE = revert only microcompact.ts to the parent commit, rebuild. Confirmed the two compiled artifacts genuinely differ (the resolveKeepRecent marker is present only in AFTER; md5sum differs).
  • Drove the exported microcompactHistory end-to-end across 10 scenarios, comparing the reported meta against the actually retained history. 6 scenarios are expected to change behavior, 4 are expected to stay identical — all 10 matched expectation (0 mismatches).

Behavioral evidence (observed values)

# Input BEFORE (parent) AFTER (PR)
S1 QWEN_MC_KEEP_RECENT=1.5, settings=2, 3 images keepRecent=1.5, mediaKept=1.5but only 1 image actually kept (metadata ≠ reality) keepRecent=2, mediaKept=2, 2 kept — consistent
S2 env 9007199254740990.5 (rounds to a safe int), settings=1 compaction silently disabled — nothing cleared, no meta falls back to settings → keepRecent=1, clears 1
S3 unsafe-int env 9007199254740992 (2⁵³), settings=1 compaction silently disabled falls back to settings → keepRecent=1, clears 1
S4 settings 1.5, no env, 6 tool results keepRecent=1.5 → keeps 1 / clears 5 default 5 → keeps 5 / clears 1
S5 settings MAX_SAFE_INTEGER+1, no env compaction silently disabled default 5 → keeps 5 / clears 1
E1 empty env "" (set but blank), settings=2 treated as 01 falls back to settings → 2 (no dedicated test covers this)
C1–C4 valid 3 / 0→1 / -2→1 / " 4 " (trim) byte-for-byte identical to BEFORE

The bug from #5648 reproduces exactly (S1): with a fractional QWEN_MC_KEEP_RECENT, BEFORE reports a fractional mediaKept=1.5 while Array.slice(-1.5) keeps only 1 item — retention and diagnostic metadata disagree. AFTER, both are integers and agree.

A second failure mode the PR also fixes (S2/S3/S5): a fractional or unsafe count that exceeds the compactable-item count makes the old code silently disable compaction entirely (a huge keepRecentslice keeps everything → nothing cleared). AFTER falls back to a sane default.

Tests

  • Full suite on AFTER: vitest run microcompact.test.ts53/53 pass.
  • Mutation / teeth check: revert only the source to the parent while keeping the new tests → exactly the 6 new tests fail, all 47 pre-existing tests still pass. The new tests genuinely guard the fix (they are not vacuous).
  • Generated schema: re-running scripts/generate-settings-schema.ts produces no diff — the committed vscode-ide-companion/schemas/settings.schema.json is in sync with settingsSchema.ts.

Risk

Low / surgical. Only invalid inputs change behavior (non-integer, unsafe-integer, or blank env). Every valid integer — including the floor-to-1 and negative cases — is unchanged. The only newly-rejected values are > MAX_SAFE_INTEGER (≈9×10¹⁵), which are meaningless as a "tool results to keep" count.

Verdict: ✅ the PR behaves exactly as described. Recommend merge.

🇨🇳 中文版(点击展开)

✅ 本地运行时验证 — 建议合并

我在 Linux 上对本 PR 做了真实验证:分别用 PR head 和它的父提交 76bdabad 构建了两份真实的 @qwen-code/qwen-code-core dist 产物,并在 tmux 中用完全相同的输入驱动编译后的真实 microcompactHistory,而不仅仅是重跑单元测试。这样测试的是实际发布的代码路径,而非 TypeScript 源码。

方法

  • AFTER = PR head 6ac6bbe6BEFORE = 只把 microcompact.ts 回退到父提交后重新构建。已确认两份编译产物确实不同(resolveKeepRecent 标记只在 AFTER 中出现,md5sum 不同)。
  • 10 个场景端到端驱动导出的 microcompactHistory,把上报的 meta实际保留下来的历史做对比。其中 6 个场景预期行为应改变,4 个预期应保持不变 —— 全部 10 个均符合预期(0 个不一致)

行为证据(实测值)

# 输入 BEFORE(父提交) AFTER(本 PR)
S1 QWEN_MC_KEEP_RECENT=1.5,settings=2,3 张图片 keepRecent=1.5mediaKept=1.5 —— 但实际只保留了 1 张图片(元数据 ≠ 真实行为) keepRecent=2mediaKept=2,保留 2 张 —— 一致
S2 env 9007199254740990.5(会四舍五入成安全整数),settings=1 压缩被静默禁用 —— 什么都没清理,无 meta 回退到 settings → keepRecent=1,清理 1 条
S3 不安全整数 env 9007199254740992(2⁵³),settings=1 压缩被静默禁用 回退到 settings → keepRecent=1,清理 1 条
S4 settings 1.5,无 env,6 条工具结果 keepRecent=1.5 → 保留 1 / 清理 5 默认 5 → 保留 5 / 清理 1
S5 settings MAX_SAFE_INTEGER+1,无 env 压缩被静默禁用 默认 5 → 保留 5 / 清理 1
E1 空 env ""(已设置但为空),settings=2 被当作 01 回退到 settings → 2没有专门的测试覆盖此项
C1–C4 合法 3 / 0→1 / -2→1 / " 4 "(去空格) 与 BEFORE 完全一致

#5648 的 bug 被精确复现(S1):QWEN_MC_KEEP_RECENT 为小数时,BEFORE 上报了小数 mediaKept=1.5,而 Array.slice(-1.5) 实际只保留了 1 条 —— 真实保留行为与诊断元数据不一致。AFTER 两者都是整数且一致。

本 PR 还修复了第二个失效模式(S2/S3/S5): 当小数或不安全的计数超过可压缩条目数时,旧代码会完全静默地禁用压缩(巨大的 keepRecentslice 保留全部 → 什么都不清理)。AFTER 会回退到合理的默认值。

测试

  • AFTER 全量测试: vitest run microcompact.test.ts53/53 通过
  • 变异 / 牙齿检查(teeth check): 只把源码回退到父提交、保留新测试 → 恰好这 6 个新测试失败,47 个已有测试全部通过。说明新测试确实在守护该修复(并非空测试)。
  • 生成的 schema: 重新运行 scripts/generate-settings-schema.ts 没有产生 diff —— 提交的 vscode-ide-companion/schemas/settings.schema.jsonsettingsSchema.ts 保持一致。

风险

低 / 改动精准。 只有非法输入的行为会改变(非整数、不安全整数、或空 env)。所有合法整数 —— 包括 floor-to-1 和负数场景 —— 行为完全不变。唯一新被拒绝的值是 > MAX_SAFE_INTEGER(≈9×10¹⁵),作为「保留多少条工具结果」的计数毫无意义。

结论:✅ 本 PR 的行为与描述完全一致,建议合并。

).toBe('grep results');
});

it('ignores fractional QWEN_MC_KEEP_RECENT values', () => {

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] Missing happy-path test for valid env var override. All 6 new tests exercise invalid QWEN_MC_KEEP_RECENT values that trigger fallback. No test sets QWEN_MC_KEEP_RECENT='3' with toolResultsNumToKeep: 1 and asserts keepRecent === 3.

If the env-parsing branch in resolveKeepRecent were accidentally broken (e.g. regex inverted, return value swapped), every existing test would still pass because they all exercise the fallback path. The env-var override mechanism — the primary purpose of the env var — has zero positive verification.

it('uses env var integer when it overrides settings', () => {
  process.env['QWEN_MC_KEEP_RECENT'] = '3';
  const history: Content[] = Array.from({ length: 4 }).flatMap((_, i) => [
    makeToolCall('read_file'),
    makeToolResult('read_file', `content ${i}`),
  ]);

  const result = microcompactHistory(history, twoHoursAgo, {
    ...DEFAULT_SETTINGS,
    toolResultsNumToKeep: 1,
  });

  expect(result.meta).toBeDefined();
  expect(result.meta!.keepRecent).toBe(3);
  expect(result.meta!.toolsKept).toBe(3);
  expect(result.meta!.toolsCleared).toBe(1);
});

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

added this exact happy-path coverage. QWEN_MC_KEEP_RECENT=3 now overrides toolResultsNumToKeep: 1, and the test asserts keepRecent=3, 3 kept, 1 cleared.

).toBe('grep results');
});

it('ignores fractional QWEN_MC_KEEP_RECENT values', () => {

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] Missing test for env var '0' or a negative integer. These values pass the regex /^-?\d+$/ and Number.isSafeInteger, then hit Math.max(1, value) — a distinct branch from all current tests. The existing 'should floor keepRecent to 1' test only covers the settings path (toolResultsNumToKeep: 0), not the env-var path.

If Math.max(1, ...) were removed or weakened, 0 or -3 from the env would propagate to slice(-keepRecent), producing incorrect keep-recent counts with no failing test to catch it.

it('clamps zero and negative env var values to 1', () => {
  process.env['QWEN_MC_KEEP_RECENT'] = '0';
  const history: Content[] = [
    makeToolCall('read_file'),
    makeToolResult('read_file', 'old content'),
  ];

  const result = microcompactHistory(history, twoHoursAgo, {
    ...DEFAULT_SETTINGS,
    toolResultsNumToKeep: 1,
  });

  expect(result.meta).toBeDefined();
  expect(result.meta!.keepRecent).toBe(1);
});

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

covered this too. Added an it.each for QWEN_MC_KEEP_RECENT=0 and -2; both now pin the env-var clamp path to keepRecent=1.

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report (maintainer)

Built and tested this PR locally to confirm the fix before merge.

Environment

  • macOS (Darwin 25.5), Node v22.22.2
  • PR head 6ac6bbe64, base 76bdabadc, current origin/main 83caca65e
  • Method: isolated git worktree at the PR head with a clean npm ci + fresh core/acp-bridge build, then verified each claim independently.

⚠️ Merge status: CONFLICTING — a rebase is required before merge

The code is correct (verified below), but the PR does not merge cleanly onto current main. There is exactly one conflict, and it is trivial:

  • docs/users/configuration/settings.md — main reformatted the settings table (column-width reflow) while this PR edited the toolResultsNumToKeep row, so the whole table block conflicts on whitespace. Pure docs; no code impact.
  • settingsSchema.ts and settings.schema.json auto-merge cleanly (this PR's edits are in a different region than main's new customIgnoreFiles / modelProviders changes).

Recommended: rebase onto main, take main's table formatting + re-apply the one number → integer row change, then run npm run generate:settings-schema to keep the generated schema in sync.

Results

Check Command Result
Target tests vitest run microcompaction/microcompact.test.ts ✅ 53/53 pass (47 + 6 new)
New tests the 6 integer-validation cases ✅ pass
Generated schema in sync npm run generate:settings-schemagit diff ✅ no diff (matches committed)
Types tsc --noEmit (core + cli) ✅ 0 errors each
Lint / Format eslint / prettier --check on changed files ✅ clean

Mutation test (the key evidence). I reverted microcompact.ts to the main version (the Number.isFinite(Number(env)) parse) while keeping the new tests — all 6 fail. Each guards a distinct case (fractional env, settings fallback, unsafe-int env, unsafe-int settings, fractional settings). The standout is checks env integer syntax before numeric conversion with QWEN_MC_KEEP_RECENT=9007199254740990.5:

Number('9007199254740990.5') === 9007199254740990   // Number.isSafeInteger → true (!)

Float rounding turns that fractional string into a safe integer, so the old finiteness check would silently accept it. The new regex /^-?\d+$/ rejects the fractional string before numeric conversion — a genuinely load-bearing guard, and the test pins it.

Correctness review

  • New resolveKeepRecent(env, settings) always returns a positive safe integer (default 5). Env path: integer-syntax regex → Number.isSafeIntegerMath.max(1, …); settings path: Number.isSafeIntegerMath.max(1, …), else default 5. Negative env (-1) floors to 1. This resolves bug(core): microcompaction keepRecent accepts fractional counts #5648 (fractional keepRecent made slice(-1.5) keep 1 while Math.min(count, 1.5) reported 1.5 in metadata).
  • Schema side is consistent: jsonSchemaOverride: { type: 'integer' } in settingsSchema.ts, the regenerated settings.schema.json (number → integer), and the docs row all agree, and the generated schema is verified in sync with the generator.
  • The new env-var tests are properly isolated (afterEach(clearEnv) deletes QWEN_MC_KEEP_RECENT), so there's no cross-test pollution.

Verdict: code LGTM — but please rebase before merging. The logic is correct and thoroughly mutation-verified; the only blocker is the trivial settings.md table conflict from main's reformatting.

🇨🇳 中文版

✅ 本地验证报告(维护者)

合并前在本地构建并测试了本 PR 以确认修复。

环境

  • macOS(Darwin 25.5),Node v22.22.2
  • PR head 6ac6bbe64,base 76bdabadc,当前 origin/main 83caca65e
  • 方法:在 PR head 上建独立 git worktree,做干净的 npm ci 并重建 core/acp-bridge,再逐条独立验证。

⚠️ 合并状态:CONFLICTING —— 合并前需要 rebase

代码是正确的(下方已验证),但本 PR 无法干净地合并到当前 main。恰好一个冲突,且很简单:

  • docs/users/configuration/settings.md —— main 重排了设置表(列宽 reflow),而本 PR 改了 toolResultsNumToKeep 那一行,于是整块表在空白字符上冲突。纯文档,无代码影响。
  • settingsSchema.tssettings.schema.json 能自动合并(本 PR 的改动和 main 新增的 customIgnoreFiles / modelProviders 在不同区域)。

建议:rebase 到 main,采用 main 的表格式 + 重新应用那一处 number → integer 行改动,然后跑 npm run generate:settings-schema 让生成的 schema 保持同步。

结果

检查项 命令 结果
目标测试 vitest run microcompaction/microcompact.test.ts ✅ 53/53 通过(47 + 6 新)
新增测试 6 个整数校验用例 ✅ 通过
生成 schema 是否同步 npm run generate:settings-schemagit diff ✅ 无 diff(与提交一致)
类型 tsc --noEmitcore + cli ✅ 各 0 错误
Lint / 格式 对改动文件跑 eslint / prettier --check ✅ 干净

变异测试(关键证据)。 我把 microcompact.ts 还原成 main 版本(Number.isFinite(Number(env)) 解析),保留新测试 —— 6 个全部失败。每个守护一个不同情形(小数 env、settings 回退、不安全整数 env、不安全整数 settings、小数 settings)。最精彩的是 checks env integer syntax before numeric conversion,env=9007199254740990.5

Number('9007199254740990.5') === 9007199254740990   // Number.isSafeInteger → true(!)

浮点舍入把这个小数字符串变成了安全整数,所以旧的 finiteness 检查会静默接受它。新的正则 /^-?\d+$/ 在数值转换之前就拒绝这个小数字符串 —— 是个真正起作用的守卫,这个测试正好锁定它。

正确性审查

  • 新的 resolveKeepRecent(env, settings) 永远返回正安全整数(默认 5)。env 路径:整数语法正则 → Number.isSafeIntegerMath.max(1, …);settings 路径:Number.isSafeIntegerMath.max(1, …),否则默认 5。负数 env(-1)被 floor 到 1。这修复了 bug(core): microcompaction keepRecent accepts fractional counts #5648(小数 keepRecentslice(-1.5) 保留 1,而 Math.min(count, 1.5) 在 metadata 里报 1.5)。
  • schema 侧一致:settingsSchema.ts 里的 jsonSchemaOverride: { type: 'integer' }、重新生成的 settings.schema.jsonnumber → integer)、文档那一行三者一致,且生成的 schema 已验证与生成器同步。
  • 新的 env 变量测试做了正确隔离(afterEach(clearEnv) 删除 QWEN_MC_KEEP_RECENT),无跨测试污染。

结论:代码 LGTM —— 但合并前请先 rebase。 逻辑正确且经充分变异测试验证;唯一阻塞是 main 重排表格带来的那个简单的 settings.md 冲突。

@tt-a1i
tt-a1i force-pushed the fix/microcompact-keep-recent-integer branch from 6ac6bbe to 503020e Compare June 23, 2026 05:57

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

Both Critical findings from the previous review (missing happy-path test, missing zero/negative test) have been properly addressed. The resolveKeepRecent implementation is clean and well-tested — all 56 tests pass, tsc and eslint are green.

LGTM ✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

✅ Local runtime verification — real tmux A/B on the built code

I verified this PR by building both arms into real artifacts (head 503020e3 vs its parent d350dd8d) and exercising the actual compiled code — both the microcompactHistory function and the real dist/cli.js running headless in a tmux pty. Bottom line: the integer-validation fix is correct, the unsafe-integer case is a genuine "context-clearing silently disabled" bug that this PR fixes, valid integers are unchanged, and the tests have teeth. Recommend merge.

What the PR changes

resolveKeepRecent() now requires a safe integer for both the QWEN_MC_KEEP_RECENT env var (checked with /^-?\d+$/ before Number(), then Number.isSafeInteger) and the toolResultsNumToKeep setting; invalid values fall through (env → settings → default 5), valid ones are floored at 1. The old code accepted any Number.isFinite value. The settings schema's toolResultsNumToKeep also moves numberinteger.

A — Compiled microcompactHistory A/B across the validation matrix

Drove the real compiled function from each build (6 tool-result pairs, idle trigger). keepRecent / kept / cleared:

input BEFORE (keepRecent, kept, cleared) AFTER (keepRecent, kept, cleared)
env "3", set 1 3, 3, 3 3, 3, 3 (same)
env "0" / "-2", set 1 1, 1, 5 1, 1, 5 (same, floored)
env " 3 " (spaces) 3, 3, 3 3, 3, 3 (same, trimmed)
env "1.5", set 3 1.5, 1, 5 3, 3, 3 (env ignored → settings)
env "9007199254740992" (2⁵³), set 1 NO-OP — nothing cleared 1, 1, 5 (env ignored → settings)
env "9007199254740990.5", set 2 NO-OP — nothing cleared 2, 2, 4 (regex rejects before precision loss)
settings 1.5 1.5, 1, 5 5, 5, 1 (→ default)
settings 2⁵³+1 NO-OP — nothing cleared 5, 5, 1 (→ default)
settings 5 5, 5, 1 5, 5, 1 (same)

→ BEFORE accepts fractional counts (1.5) and, worse, huge/unsafe values make slice(-keepRecent) keep everything → microcompaction clears nothing. AFTER rejects them and falls back correctly. Valid integers are identical (no regression).

B — Real CLI E2E in tmux (the user-facing bug)

Ran the real dist/cli.js headless (-p … --approval-mode yolo) against a mock server returning two ~15 KB tool results, with toolResultsTotalCharsThreshold=20000, toolResultsNumToKeep=1, and the same malformed QWEN_MC_KEEP_RECENT=9007199254740992 for both arms — only the build differs:

  • BEFORE → debug log: [TOOL-RESULT MC] tool result chars 30298 > 20000, cleared 0 tool result(s), kept 1. Outbound round-3 request carries both tool results in full (15189 chars each). The malformed env silently disabled clearing.
  • AFTER → debug log: [TOOL-RESULT MC] … cleared 1 tool result(s) (~3788 tokens). Outbound round-3 request: oldest result = [Old tool result content cleared], newest full. Clearing works despite the bad env.

C — Settings-schema A/B (ajv draft-07, as VS Code validates)

value OLD (number) NEW (integer)
5, 0, -2 accept accept
1.5, 2.7 accept REJECT
9007199254740992 accept accept

NEW flags fractional values in settings.json; the schema can't express "safe integer" (2⁵³ passes), which is exactly why the runtime guard in (A) matters too — defense in depth. Regenerating settings.schema.json from source is byte-identical to the committed file (not hand-edited).

D — Unit tests + mutation (teeth)

  • microcompact.test.ts: 56/56 pass on head.
  • Mutation (revert prod to parent, keep head tests): exactly the 6 new integer-validation tests fail (fractional env, env-syntax-before-conversion, unsafe-int env, unsafe/fractional settings); the 3 integer-syntax-compatible new tests + 47 others stay green → the tests genuinely pin the new behavior.

Verdict

LGTM — verified on real builds. The headline win is that a malformed QWEN_MC_KEEP_RECENT/toolResultsNumToKeep no longer silently turns off idle/size context clearing (which would let history grow unbounded). No regression for valid integers. 👍

🇨🇳 中文版(点击展开)

✅ 本地运行时验证 —— 在真实构建产物上用 tmux 做 A/B 对比

我把 PR 两侧都编译成真实产物(head 503020e3 对比父提交 d350dd8d),并运行真正的编译代码 —— 既包括 microcompactHistory 函数,也包括在 tmux 伪终端中无头运行的真实 dist/cli.js。结论:整数校验修复正确;“非安全整数”这一情形是一个真实的“上下文清理被静默关闭”的 bug,本 PR 修复了它;合法整数行为不变;测试有效。建议合并。

PR 做了什么

resolveKeepRecent() 现在要求 QWEN_MC_KEEP_RECENT 环境变量(先用 /^-?\d+$/ 校验整数语法,再 Number.isSafeInteger)和 toolResultsNumToKeep 设置都必须是安全整数;非法值逐级回退(env → 设置 → 默认 5),合法值下限取 1。旧代码接受任何 Number.isFinite 的值。设置 schema 中 toolResultsNumToKeep 也由 number 改为 integer

A —— 编译后的 microcompactHistory 在校验矩阵上的 A/B

驱动各构建中真实编译的函数(6 对工具结果,idle 触发)。keepRecent / 保留 / 清除:

输入 BEFORE(keepRecent, 保留, 清除) AFTER(keepRecent, 保留, 清除)
env "3", set 1 3, 3, 3 3, 3, 3 (相同)
env "0" / "-2", set 1 1, 1, 5 1, 1, 5 (相同,下限取 1)
env " 3 "(空格) 3, 3, 3 3, 3, 3 (相同,已 trim)
env "1.5", set 3 1.5, 1, 5 3, 3, 3 (env 被忽略 → 设置)
env "9007199254740992"(2⁵³), set 1 空操作 —— 什么都没清除 1, 1, 5 (env 被忽略 → 设置)
env "9007199254740990.5", set 2 空操作 —— 什么都没清除 2, 2, 4 (正则在精度丢失前先拒绝)
settings 1.5 1.5, 1, 5 5, 5, 1 (→ 默认)
settings 2⁵³+1 空操作 —— 什么都没清除 5, 5, 1 (→ 默认)
settings 5 5, 5, 1 5, 5, 1 (相同)

→ BEFORE 会接受小数(1.5),更糟的是超大/非安全整数会让 slice(-keepRecent) 保留全部 → 微压缩什么都不清除。AFTER 会拒绝它们并正确回退。合法整数完全一致(无回归)。

B —— tmux 中的真实 CLI 端到端(面向用户的 bug)

无头运行真实 dist/cli.js-p … --approval-mode yolo),mock 服务返回两个约 15 KB 的工具结果,设置 toolResultsTotalCharsThreshold=20000toolResultsNumToKeep=1,两侧都用相同的非法值 QWEN_MC_KEEP_RECENT=9007199254740992,仅构建不同:

  • BEFORE → 调试日志:[TOOL-RESULT MC] tool result chars 30298 > 20000, cleared 0 tool result(s), kept 1。第 3 轮外发请求中两个工具结果都是完整的(各 15189 字符)。非法 env 静默关闭了清理
  • AFTER → 调试日志:[TOOL-RESULT MC] … cleared 1 tool result(s) (~3788 tokens)。第 3 轮外发请求中:最旧结果 = [Old tool result content cleared],最新结果完整。即使 env 非法,清理也正常工作。

C —— 设置 schema 的 A/B(ajv draft-07,即 VS Code 的校验方式)

OLD (number) NEW (integer)
5, 0, -2 接受 接受
1.5, 2.7 接受 拒绝
9007199254740992 接受 接受

NEW 会在 settings.json 中标记小数值;schema 无法表达“安全整数”(2⁵³ 能通过),这正是为何 (A) 中的运行时校验同样重要 —— 纵深防御。从源码重新生成 settings.schema.json 与提交版逐字节一致(非手工编辑)。

D —— 单元测试 + 变异测试(有效性)

  • microcompact.test.ts:head 上 56/56 通过
  • 变异测试(把生产代码退回父提交、保留 head 测试):恰好 6 个新增的整数校验用例失败(小数 env、先校验语法再转换、非安全整数 env、非安全/小数设置);其余 3 个“整数语法兼容”的新用例 + 47 个其他用例仍通过 → 测试确实锁住了新行为。

结论

LGTM —— 已在真实构建上验证。最大的收益是:非法的 QWEN_MC_KEEP_RECENT/toolResultsNumToKeep 不再静默关闭 idle/size 上下文清理(否则历史会无限增长)。合法整数无回归。👍

Verified locally on Linux: head-vs-parent build A/B of the compiled function + a real headless dist/cli.js run in tmux + ajv schema A/B + mutation testing; working tree restored clean afterwards.

@wenshao

wenshao commented Jun 23, 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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao merged commit 099b47e into QwenLM:main Jun 23, 2026
40 checks passed
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(core): microcompaction keepRecent accepts fractional counts

5 participants