Skip to content

perf(cli): Cache persisted session catalogs - #8892

Merged
doudouOUC merged 4 commits into
QwenLM:mainfrom
doudouOUC:agent/session-list-catalog-cache
Aug 11, 2026
Merged

perf(cli): Cache persisted session catalogs#8892
doudouOUC merged 4 commits into
QwenLM:mainfrom
doudouOUC:agent/session-list-catalog-cache

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds a process-local, two-second single-flight cache for the persisted session catalogs used by organized and metadata-filtered daemon session lists. Catalogs are isolated by resolved runtime root, exact workspace identity, and archive state; persisted JSONL summaries and worktree sidecars are cached, while live bridge state, organization metadata, filtering, sorting, and pagination remain request-local.

Explicit REST and ACP metadata, close, delete, archive, and unarchive operations invalidate the affected catalog states before their responses are sent. The implementation also pins each read to the selected workspace runtime, adds cache status telemetry for scans, waiters, hits, and failures, and keeps default numeric pagination and the session-info disk scan on their existing paths.

Why it's needed

Organized, pinned, source-filtered, and LiveTask session lists currently repeat the same full JSONL and worktree-sidecar scan when several requests arrive together. On workspaces with hundreds or thousands of sessions, those duplicate synchronous filesystem reads increase route latency and event-loop lag without producing different persisted data.

Sharing only the short-lived persisted catalog removes redundant work while preserving current live and organization freshness, runtime isolation, cursor behavior, truncation semantics, and public REST/ACP response shapes.

Reviewer Test Plan

How to verify

Start a daemon with an isolated runtime directory and a workspace containing persisted sessions. Send five concurrent organized-all, organized-pinned, parent/source-filtered, and LiveTask-style requests for the same active workspace; confirm that one request performs the catalog scan, the other requests join it, and all responses retain the existing wire shape.

Repeat the requests within two seconds and confirm that no JSONL or worktree sidecar is read again. Change live client/prompt/title state and organization pin/group state between requests and confirm that the next response reflects those changes without waiting for cache expiry.

Exercise metadata update, close, delete, archive, and unarchive through REST and ACP, then list the affected active and archived catalogs before the two-second TTL expires. Confirm that each mutation exposes the new state immediately, failed loaders do not serve stale data, and identical workspace paths under different runtime roots never share results.

Confirm that default numeric cursor pagination still calls storage with the requested page size and that session-info remains an independent disk scan.

Evidence (Before & After)

N/A — this is a daemon performance and consistency change with no TUI or Web Shell UI change.

Tested on

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

Environment (optional)

macOS arm64, Node.js v22.22.3, npm 10.9.8, source daemon plus global qwen 0.21.8 baseline, temporary QWEN_RUNTIME_DIR and temporary workspaces only.

Risk & Scope

  • Main risk or tradeoff: Unknown external writers can race a catalog scan, so persisted-only results may remain stale for up to two seconds after the scan completes; explicit daemon mutations invalidate synchronously, while live and organization fields are always recomputed per request.
  • Not validated / out of scope: Windows and Linux were not tested locally. The full workspace build and typecheck remain blocked in this checkout by pre-existing Ink selection type mismatches and missing Web Shell generated package declarations outside this diff. Daemon outer timeouts, Web Shell polling, cross-workspace scan scheduling, and core storage rewrites are out of scope.
  • Breaking changes / migration notes: None. REST and ACP request and response schemas are unchanged.

Linked Issues

N/A

中文说明

此 PR 的作用

此 PR 为 organized 和 metadata 过滤型 daemon session list 使用的持久化会话目录增加进程内、两秒 TTL 的 single-flight 缓存。目录按解析后的 runtime root、精确 workspace identity 和 archive state 隔离;持久化 JSONL 摘要与 worktree sidecar 会被缓存,而 live bridge 状态、organization metadata、过滤、排序和分页仍在每次请求中独立计算。

显式 REST 与 ACP metadata、close、delete、archive 和 unarchive 操作会在响应发送前失效受影响的目录状态。实现还将每次读取固定到选中的 workspace runtime,增加 scan、waiter、hit 和 failure 的缓存状态 telemetry,并让默认数字游标分页与 session-info 磁盘扫描继续使用现有路径。

为什么需要此改动

organized、pinned、source-filtered 和 LiveTask session list 在多个请求同时到达时,目前会重复执行相同的完整 JSONL 与 worktree sidecar 扫描。在包含数百或数千个 session 的 workspace 中,这些重复的同步文件系统读取会增加路由延迟和 event-loop lag,却不会产生不同的持久化数据。

仅共享短生命周期的持久化目录可以消除冗余工作,同时保持现有 live 与 organization 新鲜度、runtime 隔离、cursor 行为、truncation 语义以及公开 REST/ACP response shape 不变。

Reviewer 测试计划

验证方法

使用隔离的 runtime directory 启动 daemon,并准备一个包含持久化 session 的 workspace。针对同一个 active workspace 并发发送五个 organized-all、organized-pinned、parent/source-filtered 和 LiveTask 风格请求;确认只有一个请求执行目录扫描,其余请求加入该扫描,并且所有响应保持现有 wire shape。

在两秒内重复这些请求,确认不会再次读取 JSONL 或 worktree sidecar。在两次请求之间修改 live client/prompt/title 状态以及 organization pin/group 状态,确认下一个响应无需等待缓存过期即可反映这些变化。

通过 REST 和 ACP 执行 metadata update、close、delete、archive 和 unarchive,然后在两秒 TTL 到期前列出受影响的 active 与 archived 目录。确认每个 mutation 都立即暴露新状态,失败的 loader 不会提供 stale data,并且不同 runtime root 下相同 workspace path 永远不会共享结果。

确认默认数字游标分页仍使用请求的 page size 调用 storage,并且 session-info 仍是独立的磁盘扫描。

证据(改动前后)

N/A——这是 daemon 性能与一致性改动,不包含 TUI 或 Web Shell UI 变化。

测试平台

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

环境(可选)

macOS arm64、Node.js v22.22.3、npm 10.9.8、源码 daemon 与全局 qwen 0.21.8 基线;仅使用临时 QWEN_RUNTIME_DIR 和临时 workspace。

风险与范围

  • 主要风险或取舍:未知外部 writer 可能与目录扫描竞态,因此仅持久化结果在扫描完成后最多可能保持两秒旧值;显式 daemon mutation 会同步失效,而 live 与 organization 字段始终在每次请求中重新计算。
  • 未验证 / 范围外:本地未测试 Windows 和 Linux。当前 checkout 的全 workspace build 与 typecheck 仍被此 diff 范围外既有的 Ink selection 类型不匹配和 Web Shell 生成包声明缺失阻塞。Daemon 外层 timeout、Web Shell polling、跨 workspace 扫描调度和 core storage 重写均不在本 PR 范围内。
  • Breaking change / 迁移说明:无。REST 与 ACP request/response schema 保持不变。

关联 Issue

N/A

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E Test Report

Environment: macOS arm64, Node.js v22.22.3, npm 10.9.8, global qwen 0.21.8 for the baseline, source daemon for the implementation, and isolated temporary runtime/workspace directories. No user session directory was read or modified.

The empty-runtime baseline sent five concurrent list variants to global qwen; every request returned HTTP 200 in approximately 1–8 ms.

Persisted sessions Cold concurrent p50 Cold concurrent p95 Warm p50 Warm p95
300 397.8 ms 414.2 ms 8.2 ms 11.3 ms
1,000 987.6 ms 1,009.1 ms 6.9 ms 8.7 ms

After the 1,000-session run, daemon status reported event-loop mean 21.2 ms, p99 24.1 ms, and max 33.8 ms. Response envelopes retained only the existing sessions and optional nextCursor fields.

Deterministic verification passed for the cache state machine (9 tests), focused REST/session-list coverage (80 tests), ACP list and mutation coverage (27 tests), LiveTask coverage (14 tests), and selected multi-workspace/runtime-isolation coverage (7 tests). The five concurrent query variants performed one complete catalog load and one sidecar pass; warm hits performed neither, while live and organization changes remained immediately visible.

Prettier, ESLint, and git diff --check passed. The full workspace build and typecheck remain blocked by pre-existing Ink selection type mismatches and missing Web Shell generated package declarations outside this change; no modified file appeared in those diagnostics.

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 4855eb0, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC marked this pull request as ready for review August 11, 2026 02:40
@doudouOUC
doudouOUC enabled auto-merge August 11, 2026 02:40
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Re-run after two test-hardening commits (aa12aa9d, 4855eb0a) — production code is unchanged since the previous pass (verified via the compare API), so the gate assessment stands with one material update.

Template looks good ✓ (all sections, bilingual).

  • Problem: real and structural — organized and metadata-filtered lists walk the full JSONL catalog plus worktree sidecars, and concurrent request variants repeated that work. Since the last pass there is quantification too: the author's E2E report records cold-concurrent p50 of ~398 ms (300 sessions) and ~988 ms (1,000 sessions) dropping to ~7 ms warm, with five concurrent variants performing exactly one catalog load and one sidecar pass. Those are author-reported macOS numbers — flagged as such — but they are concrete, the methodology is described, and the load counts are deterministic.
  • Direction: aligned. Daemon session lists are a hot path for the Web Shell sidebar (sections poll on a 10s interval, several surfaces request organized lists concurrently) and for LiveTask refreshes; feat(web-shell): Share session catalog scheduling #8891 (catalog scheduling) has merged and this is its natural follow-on. CHANGELOG has no direct reference, but the area is clearly relevant.
  • Size: ~682 production logic lines / ~1,086 test lines / 31 lines of design doc. Everything sits under packages/cli/src/serve/** plus one design doc — no core-module paths touched, so the two-tier core gate does not apply.
  • Approach: the earlier "single-flight only first?" question is settled: the author defended the 2s TTL (it covers sequential reload bursts beyond exact promise overlap, and the internal maintenance removal paths deliberately rely on the documented two-second bound), and a maintainer has since approved the current head. Recorded as a design call, not a blocker. The diff remains focused — no drive-by changes.
  • Risk: no elevated-risk path matches.

Moving on to code review. 🔍

中文说明

本轮 re-run 基于两个测试加固 commit(aa12aa9d4855eb0a)——经 compare API 核实,生产代码自上一轮审查以来未变,门禁结论维持,仅有一处实质更新。

模板完整 ✓(各节齐全,中英双语)。

  • 问题:真实且结构性——organized 与 metadata 过滤列表会完整遍历 JSONL 目录和 worktree sidecar,并发请求重复同样的工作。与上轮相比现在有了量化:作者的 E2E 报告记录了冷并发 p50 约 398 ms(300 sessions)/ 约 988 ms(1,000 sessions),热态降至约 7 ms,五个并发请求只触发一次目录加载和一次 sidecar 扫描。这些是作者自述的 macOS 数据——已如实标注——但数字具体、方法有描述、加载次数可确定性验证。
  • 方向:对齐。Daemon session 列表是 Web Shell 侧边栏(各分区 10s 轮询、多个界面并发请求 organized 列表)和 LiveTask 刷新的热路径;feat(web-shell): Share session catalog scheduling #8891(目录调度)已合入,本 PR 是其自然延续。CHANGELOG 无直接引用,但该领域明显相关。
  • 规模:约 682 行生产逻辑 / 约 1,086 行测试 / 31 行设计文档。全部改动位于 packages/cli/src/serve/** 加一篇设计文档——未触及核心模块路径,两级核心门禁不适用。
  • 方案:此前提出的"能否先只做 single-flight?"已有结论:作者为 2s TTL 给出了合理辩护(覆盖精确 promise 重叠之外的短时突发重载;内部维护性删除路径有意依赖文档化的两秒边界),且维护者已批准当前 head。记为设计决策,不作阻塞。diff 保持聚焦,无夹带改动。
  • 风险:未命中高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 4855eb0a2f1f147e2b99313531c2899e64828e36 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-run note: the two commits since the previous pass (aa12aa9d, 4855eb0a) are test-only — verified via the compare API (four *.test.ts files, +149/−2) — so the earlier production assessment carries over, and this pass re-checked the production hunks at the current head and reviewed the new tests in detail.

Independent baseline (unchanged from last pass): for "concurrent requests repeat the same catalog scan", the minimal design is a keyed map of in-flight promises (single-flight) with no TTL.

Against the diff:

  • Production (byte-identical to the previously reviewed commit): single-flight plus a 2s TTL snapshot cache with generation counters, invalidation hooks on every REST/ACP mutation route, clone-before-mutate, an eviction cap, and span telemetry. The concurrency core remains correct under tracing: an invalidated in-flight load cannot install its snapshot (generation check), a stale rejection cannot clear a newer load (promise-identity check), failures are never cached and never served stale, and the shared snapshot stays read-only across requests (copy-on-write sidecar enrichment plus clonePersistedSummary at both consumption sites). Runtime isolation is pinned through Storage.runWithResolvedRuntimeBaseDir with the resolved runtime root in the cache key; all four production callers pass the owning runtime's sessionRuntimeBaseDir. Default numeric pagination and session-info stay on their existing paths, as claimed.
  • The new tests are the interesting part of this re-run — they convert the earlier /review round's test-efficacy findings into real pins: a TTL-boundary case (an installed value is rejected once its read-path age reaches the TTL), eviction actually reclaiming retained-summary capacity, LiveTask list reads pinned to the owning workspace's bridge by identity instead of expect.anything(), cross-workspace archive/unarchive/delete visibility asserted before and after each mutation, invalidation timing pinned by a gated in-flight close ("catalogs only change after the archive mutation settles"), and the new span attributes asserted by name. These pin load-bearing behavior, not ceremony.
  • Standing observations, both resolved: the un-invalidated internal removal paths (ACP orphan cleanup, LiveTask fresh-session rollback, scheduled-task removal) are confirmed deliberate by the author and documented as the two-second consistency bound in the design doc; the single-flight-only scope question was settled by the maintainer's approval of the current head. Neither blocks.
sequenceDiagram
    participant P1 as List requester
    participant P2 as Catalog cache
    participant P3 as JSONL and sidecar scan
    participant P4 as Mutation route
    P1->>P2: lookup scope
    alt fresh snapshot
        P2-->>P1: cache hit
    else scan in flight
        P2-->>P1: join shared promise
    else cold
        P2->>P3: run scan once
        P3-->>P2: install snapshot, 2s TTL
        P2-->>P1: scan result
    end
    P4->>P2: invalidate after mutation
Loading
Files changed (12)
File What changed
docs/design/session-list-persisted-catalog-cache.md Design doc: cache scope, consistency boundary, the known two-second staleness window
packages/cli/src/serve/server/persisted-session-list-cache.ts New cache class: single-flight, 2s non-sliding TTL, generations, eviction cap, unreffed expiry timers
packages/cli/src/serve/server/persisted-session-list-cache.test.ts Unit tests: coalescing, failure handling, TTL boundary, isolation, invalidation races, eviction capacity reclamation
packages/cli/src/serve/server/session-list.ts Wires the cache into organized and metadata-filtered lists, moves sidecar enrichment into the scan, pins reads to the resolved runtime, adds span attributes
packages/cli/src/serve/server.ts Re-exports the invalidation helper
packages/cli/src/serve/routes/session.ts Invalidation hooks on REST close, delete, archive, unarchive, metadata routes; passes runtimeBaseDir on the list path
packages/cli/src/serve/acp-http/dispatch.ts Same invalidation hooks for the ACP close, delete, archive, unarchive, metadata paths
packages/cli/src/serve/live/live-task-service.ts Pins LiveTask list reads and session removal to the owning workspace runtime
packages/cli/src/serve/server.test.ts Integration tests: single-flight across query variants, invalidation on mutation including settle-ordering, runtime-root isolation, telemetry attributes
packages/cli/src/serve/acp-http/transport.test.ts Over-the-wire ACP test that mutations invalidate active and archived catalogs
packages/cli/src/serve/live/live-task-service.test.ts Asserts LiveTask reads and removals hit the correct runtime root and bridge identity
packages/cli/src/serve/multi-workspace-sessions.test.ts Cross-workspace mutation visibility asserted before and after archive, unarchive, delete

Test evidence (the PR's own CI, read via API — no PR code executed here)

On the reviewed commit, all four pull_request-event workflow runs completed green: Qwen Code CI (including Test (ubuntu-latest, Node 22.x) — build + typecheck + unit suite), Serve A/B (automated A/B reports no response-shape change against the base across 4 scenarios), Qwen Live Host CI, and SDK Java (including Real daemon E2E / Java 11). Desktop Shell (ubuntu + windows) and web-shell E2E Smoke are also green. macOS/Windows unit tests and CLI integration tests are skipped by design — per ci.yml they run only in the merge queue, so that is the normal PR-stage signal, not a gap. The remaining check-runs on this SHA are bot orchestration jobs (autofix routing, review plumbing) — skipped or cancelled by design, not PR CI. Nothing red, nothing pending.

What the suite proves: mechanism and wiring — coalescing, TTL, invalidation ordering, runtime isolation, telemetry, wire shape (Serve A/B). What it does not prove: the size of the perf win. The numbers in play come from the author's E2E report (macOS arm64, self-run): cold-concurrent p50 ~988 ms at 1,000 sessions vs ~7 ms warm, one catalog load for five concurrent variants, bounded event-loop lag. Plausible and consistent with the mechanism — but author-reported, not independently re-run here.

Check Conclusion
Test (ubuntu-latest, Node 22.x) success
Serve A/B (ubuntu-latest, Node 22.x) success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
Live Host (macos-latest) success
Real daemon E2E / Java 11 success
Test (macos-latest, Node 22.x) skipped (merge-queue only)
Test (windows-latest, Node 22.x) skipped (merge-queue only)
Integration Tests (CLI, No Sandbox) skipped (merge-queue only)

Sandboxed verification would settle the remaining claim: @qwen-code /verify — an independent scan-count/latency A/B against the base build would turn the author-reported perf numbers into a measurement anyone can reproduce; the suite passes with the TTL layer removed, so green CI alone cannot substantiate the win. The author has write access and can trigger it directly; a maintainer's /verify would run it as a sponsored, risk-screened execution. Optional given the maintainer approval already on this head — but it is the one gap left.

中文说明

代码审查(re-run):自上一轮以来的两个 commit(aa12aa9d4855eb0a仅为测试改动——经 compare API 核实(四个 *.test.ts 文件,+149/−2)——生产代码评估沿用上轮结论;本轮在当前 head 复核了生产代码 hunk,并逐条审查了新测试。

独立基线不变:对"并发请求重复同一目录扫描",最简方案是只做 single-flight、不做 TTL。对照 diff:生产代码(与上轮审查的 commit 逐字节一致)为 single-flight 加 2s TTL 快照缓存、代数失效、每个 REST/ACP mutation 路由的失效钩子、写时复制、淘汰上限与 span 遥测;并发核心经推演仍然正确(被失效的在途加载无法安装快照、过期拒绝不会清掉更新的加载、失败永不缓存也永不回退旧值、共享快照跨请求只读)。Runtime 隔离经 Storage.runWithResolvedRuntimeBaseDir 固定,缓存 key 含解析后的 runtime root,四个生产调用方均传入所属 runtime 的 sessionRuntimeBaseDir;默认数字分页与 session-info 保持原路径。新测试是本 re-run 的亮点:把上一轮 /review 提出的测试有效性问题逐一落实——TTL 边界拒绝、淘汰确实回收容量配额、LiveTask 列表读取按身份钉住所属 workspace 的 bridge、跨 workspace 变更前后可见性、以受控在途 close 钉住失效时序("目录只在 archive mutation 落定后变化")、新 span 属性按名断言。两点遗留观察均已解决:未接失效的内部删除路径经作者确认为有意为之并写入设计文档的两秒一致性边界;single-flight-only 的范围问题由维护者对当前 head 的批准定论。均不构成阻塞。

测试证据(来自 PR 自己的 CI,通过 API 读取,本审查未执行任何 PR 代码):该 commit 上四个 pull_request 事件 workflow 全部绿色完成——Qwen Code CI(含 ubuntu 单元门禁:build + typecheck + 单测)、Serve A/B(与 base 相比 4 个场景无响应结构变化)、Qwen Live Host CI、SDK Java(含 Real daemon E2E)。Desktop Shell 双平台与 web-shell E2E Smoke 亦绿色。macOS/Windows 单测与 CLI 集成测试按 ci.yml 设计仅在 merge queue 运行,PR 阶段跳过属正常信号。该 SHA 上其余 check-run 为机器人编排任务(autofix 路由、review 管线),按设计跳过或取消,非 PR CI。无红色、无 pending。单测证明的是机制与接线(合并、TTL、失效时序、隔离、遥测、wire shape);不能证明收益大小——目前的数字来自作者 E2E 报告(macOS arm64 自测):1,000 sessions 冷并发 p50 约 988 ms 对热态约 7 ms、五并发一次目录加载、event-loop lag 有界。合理且与机制一致,但为作者自述,未在本审查中独立复跑。

沙箱验证可以解决剩余声明:@qwen-code /verify —— 对 base build 做独立的扫描次数/延迟 A/B,可把作者自述的性能数字变成任何人可复现的测量值;去掉 TTL 层单测依然全绿,因此仅凭绿色 CI 无法证实收益。作者有 write 权限可直接触发;维护者的 /verify 则以受赞助、带风险筛查的方式执行。鉴于当前 head 已有维护者批准,此步可选——但这是唯一剩余的缺口。

Qwen Code · qwen3.8-max

Reviewed at 4855eb0a2f1f147e2b99313531c2899e64828e36 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — careful, well-tested concurrency work with green CI and a maintainer approval on this head; the one standing reservation is that the perf win rests on author-reported measurements.

The previous pass deferred with three questions for a human. All three now have answers:

  1. Can the win be measured? The author's E2E report brings numbers: cold-concurrent p50 ~398 ms (300 sessions) / ~988 ms (1,000 sessions) dropping to ~7 ms warm, five concurrent variants causing exactly one catalog load plus one sidecar pass, event-loop lag bounded. Self-reported on macOS — but concrete, methodology described, and consistent with what the code demonstrably does (the deterministic load counts are the part I can verify from the design).
  2. Single-flight only as a first step? The author kept the TTL with a defensible rationale — it covers sequential reload bursts beyond exact promise overlap, and the internal maintenance removal paths deliberately rely on the documented two-second bound. The design call is now settled anyway: a maintainer reviewed and approved this exact head.
  3. Un-invalidated internal removal paths? Confirmed deliberate, bounded by the 2s TTL, and documented in the design doc's consistency section.

What moved the verdict beyond those answers: the two follow-up commits are exactly the response you want to review feedback — they converted the /review round's test-efficacy findings into tests that pin load-bearing behavior (invalidation settle-ordering, eviction capacity accounting, bridge/runtime identity, the TTL boundary). If I have to maintain this in six months: the cache class is small, its invariants are explicit, and the tests say what is allowed to break. That is thank-them territory, not curse-them.

Reservations kept on record, none blocking: "faster" is measured by the author on one platform — an independent @qwen-code /verify A/B against the base build remains available if anyone wants it before merge, author-triggerable given write access. And the invalidation wiring is permanent coupling: every future mutation route must remember it. The new tests mitigate that, but the discipline burden is real and worth knowing when the next mutation path lands.

Approving, pinned to the reviewed commit. main asks for two approvals; this joins @yiliang114's — it does not replace a maintainer's merge timing decision.

中文说明

置信度:4/5 —— 仔细且测试充分的并发工作,CI 全绿,当前 head 已有维护者批准;唯一保留意见是性能收益基于作者自述的测量。

上一轮以三个问题转交人工裁决,现在三个都有了答案:

  1. 收益能否测量? 作者的 E2E 报告给出了数字:冷并发 p50 约 398 ms(300 sessions)/ 约 988 ms(1,000 sessions),热态降至约 7 ms,五个并发请求只触发一次目录加载和一次 sidecar 扫描,event-loop lag 有界。macOS 自述——但数字具体、方法有描述,且与代码可验证的行为一致(确定性加载次数是可以从设计核实的部分)。
  2. 先只做 single-flight? 作者保留了 TTL 并给出合理辩护——覆盖精确 promise 重叠之外的短时突发重载,内部维护性删除路径有意依赖文档化的两秒边界。况且设计决策已然定论:维护者审查并批准了当前 head。
  3. 未接失效的内部删除路径? 经确认是有意为之,由 2s TTL 兜底,并写入设计文档的一致性章节。

除答案之外真正改变结论的是:两个后续 commit 正是面对 review 反馈应有的回应——把上一轮 /review 的测试有效性发现转化为钉住关键行为的测试(失效落定时序、淘汰容量核算、bridge/runtime 身份、TTL 边界)。如果六个月后由我维护:缓存类很小、不变量明确、测试说明了什么允许被打破。这是值得感谢而非抱怨的代码。

保留意见(均不阻塞):"更快"由作者在单一平台上测量——合入前如有人想要独立验证,对 base build 的 @qwen-code /verify A/B 仍然可用,作者有 write 权限可自行触发。另外失效接线是永久耦合:未来每个 mutation 路由都必须记住它。新测试降低了风险,但纪律负担真实存在,下条 mutation 路径落地时值得记起。

批准,钉在所审查的 commit 上。main 需要两个批准;本批准与 @yiliang114 的并列——不替代维护者对合入时机的决定。

Qwen Code · qwen3.8-max

Reviewed at 4855eb0a2f1f147e2b99313531c2899e64828e36 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Thanks — keeping the 2s TTL design. The E2E report above already records 300- and 1,000-session cold-concurrent and warm p50/p95 results, plus deterministic catalog/sidecar load counts; while that is not a same-dataset base-build A/B, it measures the burst path this PR targets. The TTL intentionally covers sequential reload bursts beyond exact promise overlap, and the listed internal maintenance removal paths deliberately rely on the documented two-second bound under the approved consistency boundary. No code change is needed for this review.

@doudouOUC doudouOUC self-assigned this Aug 11, 2026

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — the walk completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; This PR adds a process-local, two-second-TTL single-fligh...: none — all checks I started completed within budget..

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — the walk completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;This PR adds a process-local, two-second-TTL single-fligh...:none — all checks I started completed within budget.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment thread packages/cli/src/serve/acp-http/dispatch.ts
Comment thread packages/cli/src/serve/routes/session.ts
Comment thread packages/cli/src/serve/server/session-list.ts
Comment thread packages/cli/src/serve/server/persisted-session-list-cache.ts
Comment thread packages/cli/src/serve/server/persisted-session-list-cache.ts
Comment thread packages/cli/src/serve/server.test.ts
Comment thread packages/cli/src/serve/server/persisted-session-list-cache.test.ts Outdated
Comment thread packages/cli/src/serve/server/session-list.ts
Comment thread packages/cli/src/serve/server/persisted-session-list-cache.test.ts
Comment thread packages/cli/src/serve/server/persisted-session-list-cache.test.ts
doudouOUC and others added 2 commits August 11, 2026 14:00
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Review follow-up completed on 4855eb0a2f (test hardening commits aa12aa9d1e and 4855eb0a2f).

Review item Action
Workspace-scoped warm-cache invalidation Fixed: archive, unarchive, and delete now have selected-secondary-runtime regression coverage.
Session-list span attributes Fixed: all documented attributes are asserted.
Read-path TTL age guard Fixed: tested independently from the expiry timer.
LiveTask runtime bridge wiring Fixed: exact bridge object identity is asserted.
Archived-session delete invalidation Fixed: archived cache is warm and non-empty before delete.
Invalidation ordering Fixed: an in-flight archive/read race pins post-mutation invalidation.
Retained-summary accounting Fixed: capacity reclamation is behaviorally asserted.
Eviction-test microtask ordering Fixed: the probe loader is deferred until assertions complete.
ACP close/metadata over-the-wire coverage Deferred: proposed metadata assertion conflicts with asynchronous persistence semantics; separate coverage can avoid asserting synchronous durability.
Superseded load deleting a newer value Pushed back: managed-Promise identity and generation checks make the described transition unreachable.
Empty-slot and passive-timer private cleanup assertions Pushed back: no public discriminator without exposing internal cache state; stale serving and capacity remain independently covered.
Multiple-lookups-per-span overwrite Deferred: valid observability follow-up requiring events/child-span aggregation design outside this cache PR.

Verification: 25 cache/LiveTask tests passed; 3 targeted server mutation/telemetry tests passed; 101 multi-workspace tests passed; changed-file ESLint passed. The combined server + multi-workspace run had 1 unrelated flaky registry-source assertion among 1,016 tests, which passed immediately in isolation. Full build remains locally blocked by pre-existing Ink type/export mismatches in selection UI files; the prior PR SHA was fully green and the new test-only SHA has fresh CI running.

@yiliang114 yiliang114 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. The cache core is correct and unusually well-tested: single-flight waiters receive failures (never cached, inFlight cleared on both outcomes), TTL is non-sliding from scan completion with an independent read-path age guard, generation counters keep invalidated/superseded loads from installing or deleting newer values, keys derive from the same runtime.sessionRuntimeBaseDir + workspaceCwd on read and invalidate sides so cross-workspace collision is impossible in every current call path, reads are pinned via resolved-runtime ALS with SessionService capturing the root at construction, and memory is bounded (50k-summary cap, oversized served-not-installed, 2s lifetime). Invalidation runs after the mutation settles and before responses are sent, including ACP over-the-wire archive/unarchive/delete; the 12 bot suggestions are all dispositioned (7 fixed with tests, 2 deferred, 3 rejected with verified rationale). CI green on head.

Two P2 hardening items for follow-up, not blockers: (1) runtimeBaseDir is optional on the read options (session-list.ts:88) — all three current daemon callers pass it, but a future caller omitting it silently reads the process-default root and its invalidations would never match; make it required or assert presence. (2) Pre-existing: the ACP _qwen/sessions/{delete,archive,unarchive} handlers build SessionService without the runtime root (dispatch.ts:4337/4362/4386) unlike removeOrphanSession and the REST equivalents — now that reads are pinned to sessionRuntimeBaseDir, a secondary-workspace dispatcher with a different ambient root mutates a different root than the catalog being listed; one-line-each fix. P3s (internal maintenance deletes TTL-only, span-attribute overwrite on multi-lookup, no over-the-wire close/update_metadata test, external writers TTL-covered) are all documented, bounded tradeoffs. Ship it.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 41 passed · 1 failed · 42 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:41 通过 · 1 失败 · 42 总计

Verification report

PR 8892 Deep Verification — perf(cli): Cache persisted session catalogs

Verdict: findings — 41/42 scripted assertions passed; the one failure is a test-pinning gap, not a behavior defect.
Verified head: 4855eb0a2f1f147e2b99313531c2899e64828e36 (HEAD^2), base tip 9946d13a3578e770f88a4788aad6f1890237b58c (HEAD^1), merge commit 7f11a670b8.

中文摘要
  • 结论 findings:42 条脚本化断言通过 41 条;唯一失败是一条测试覆盖缺口(非行为缺陷), shipped 代码本身全部正确。
  • A/B 结论(核心主张成立):在隔离 runtime 目录 + 250 个已持久化 session 的真实 daemon 上,用 LD_PRELOAD openat 计数真实磁盘读取:base(无缓存)10 个列表请求共 4064 次目录文件打开;head(有缓存)仅 517 次(-87.3%)。5 个并发变体在 head 上被 single-flight 合并为每个 archive state 一次扫描(cold=517);2 秒 TTL 内的重复请求 0 次磁盘读取(warm=0);TTL 过期后重新扫描(505)。两条 arm 的 15 个响应 body 在值上完全一致(仅 organized 视图 worktree 字段的 JSON key 顺序不同,见 Findings 如何自定义密钥文件 .env可能与其他文件冲突 #3)。
  • 失效语义全部正确:delete/archive/unarchive 在 TTL 内立即可见且强制重新读盘(两 arm 行为逐字节对称,各 15/15 断言通过);loader 失败时返回 500、绝不回退 stale 快照、且失败不污染缓存。
  • 突变矩阵:10 个单点突变(9 个新守卫 + 1 个阳性对照),9 个被相应测试以行为断言杀死;M5 幸存:install 守卫中的 generation 检查(防止"扫描进行中被 invalidate、且无后续请求"时安装旧快照)未被任何测试钉住——名为 "does not let an invalidated load install or clear a newer load" 的测试在该守卫被删除后依然通过(它实际依赖 inFlight 同一性检查)。探针证明该守卫是 load-bearing 的(见 Findings pre-release: fix ci #1)。
  • 未覆盖:真实 ACP WebSocket + 真实 model 的 live session 元数据/close 路径(无凭据)、双 runtime root 的 E2E(机制已由单测+突变证明)、50k 上限的真实负载测试、延迟/event-loop lag 的直接测量(以文件打开数为机制证据)。

Central claim + A/B

Central claim: a process-local, 2 s TTL, single-flight cache deduplicates persisted-catalog scans across concurrent and repeated organized/metadata/LiveTask-style list requests, without changing response content or freshness semantics.

The A/B drives a real compiled daemon (packages/cli/dist/index.js vs a base worktree rebuilt at HEAD^1 with only packages/cli recompiled; package.json/lockfile and packages/core are untouched by the PR, and the base tree's @qwen-code/qwen-code-core realpath was asserted to resolve into the unchanged head core). Seeded fixture: 250 active + 6 archived JSONL sessions (deterministic ids/timestamps), 5 worktree sidecars, 4 parent/child, 6 source-tagged, 3 custom titles. An LD_PRELOAD openat shim (harness/fscount.so source in tmp/fscount.c) logs every successful open under the chats dir — an OS-level, mock-free oracle independent of the PR's own telemetry.

Oracle = count of successful open()/openat() of catalog files per request window (ab-harness.mjs phases):

phase (requests) base (no cache) head (cached) interpretation
cold — 5 concurrent variants (org-all ×2 [LiveTask shape], org-archived, sourceType, parentSessionId) 2032 (4 full active scans ×505 + 1 archived ×12) 517 (1 active + 1 archived scan) single-flight + shared scope across query kinds
warm — same 5, ~0.4 s later (inside TTL) 2032 0 TTL reuse; base rescans everything
expire — 1 organized-all after >2.4 s 505 505 TTL lapse rescans (parity)
numeric default path, twice 41 / 41 41 / 41 cache bypass preserved (parity)
session-info, twice 256 / 256 256 / 256 independent disk scan preserved (parity)
total catalog reads over the 10 list requests 4064 517 (-87.3%) load-bearing proof

Wire shape: all 15 response bodies saved per arm are value-identical (order-insensitive deep compare, ab-assert.mjs); 10/15 are byte-identical, the 5 organized-view bodies differ only in JSON key order (Finding #3). 17/17 scripted A/B assertions passed. Raw cells: harness/run-head.result.txt, harness/run-base.result.txt. Witness: 01-ab-catalog-reads-head-vs-base.png.

Findings

1. The generation install-guard is load-bearing but unpinned by any test (Medium — test gap, code is correct)

The cache installs a completed scan only if current.generation === generation && snapshot.sessions.length <= maxRetainedSummaries (persisted-session-list-cache.ts). Deleting the generation clause (mutant M5) leaves the PR's own test does not let an invalidated load install or clear a newer load green (1 passed | 10 skipped): that test starts a replacement load immediately after invalidate(), so current.inFlight?.promise === managed already blocks the stale install and the generation clause never decides the outcome — the test passes for a different reason than its name claims.

The clause is not dead code: it is the only thing guarding the interleaving invalidate arrives mid-scan and no replacement request joins. A scratch probe (harness/zz-verify-generation.test.ts.probe; run log harness/mut-M5-probe.log) drives exactly that sequence — lookupinvalidate → resolve old load → next lookup must be scan:

  • clean source: 1 passed (stale snapshot correctly not installed);
  • M5 mutant: 1 failedexpected 'cache_hit' to be 'scan': the pre-invalidation snapshot is installed and served to the next request for up to the full TTL — the exact stale-read class this PR exists to prevent.

Consequence: a future refactor could delete the generation check and every test in this PR would stay green. Suggested fix (measured, in scratch copy): add the probe test verbatim to persisted-session-list-cache.test.ts. It passes on the shipped code and dies cleanly under M5; the affected suite's other counts are unchanged (10 passed neighbors stay green). The reject path needs no analogous clause — it never installs a value — and the failure-not-retained property is pinned (M6 killed by does not retain loader failures).

2. Reviewer Test Plan step "Exercise metadata update … through REST" is unreachable for persisted-only sessions (Low — plan/code mismatch, pre-existing)

PATCH /session/:id/metadata is wrapped in withOwnerMutableSession, which requires a live session; persisted-only sessions get 401 on both arms (identical, so not a regression — the guard predates the PR). The plan's metadata-freshness step therefore cannot be performed against the fixture it describes ("workspace containing persisted sessions"); it only works for sessions with an attached client, which the harness cannot create without model credentials. The invalidation wiring itself is real and pinned: mutant M9 (removing the route's own finally { invalidateSessionLists(runtime, ['active']) }) is killed by invalidates a warm catalog on metadata update… with expected "listSessions" to be called 2 times, but got 1 times (harness/logs/mut-M9.log). Note this route does not use the shared runWithSessionListInvalidation helper, which is why M2 does not kill that test. Suggestion: reword the plan step to "attach a live session, then PATCH metadata".

3. Organized-view responses change JSON key order vs base (Informational)

For the five organized-view bodies, head emits worktree before groupId/color/isPinned, base emits it after — because enrichment moved inside the cached loader (loadAllPersistedSummaries) ahead of applyOrganization, while base enriched after. Values are identical (verified order-insensitively); JSON objects are unordered, so no REST/ACP consumer should observe this — but any byte-comparing snapshot/golden-file client would. No action needed unless such consumers exist.

Injection screening

PR text contained no steering instructions; no injection attempts observed.

Not covered

  • Per-commit attribution. Checkout is depth 2 (shallow); only HEAD^2 of the 4 commits in the metadata snapshot is locally reachable (git rev-list HEAD^1..HEAD^2 = 1 commit). The aggregate HEAD^1..HEAD diff is what was verified. Commit 2 ("Pin LiveTask rollback storage runtime") is nonetheless pinned behaviorally: mutant M8 (unpinning removeSession from runWithWorkspaceRuntimeStorage) is killed by live-task-service.test.ts asserting the runtime base dir of the deletion.
  • Live-session mutation paths end-to-end (REST/ACP metadata on live sessions, session close): require an attached ACP client + model credentials this sandbox does not have. The close/close-invalidates wiring is covered by the PR's own server.test.ts test (green under the gate) and shares the M2-killed helper.
  • Two different runtime roots in one daemon (E2E). Reachable in production (per-workspace resolveSessionRuntimeBaseDir in run-qwen-serve.ts), but standing up a managed secondary runtime with its own root was out of budget. Proven by construction instead: cache key includes path.resolve(runtimeBaseDir) and mutant M4 (dropping it from the key) is killed by the isolation unit test (expected "spy" to be called 4 times, but got 3 times).
  • Direct latency / event-loop-lag measurement. The mechanism (87.3% fewer catalog file opens; zero opens on warm repeats) is proven; actual ms savings under thousands of sessions on a loaded runner were not measured — a perf number here would measure the shared CI box, not the PR.
  • 50,000-summary retention cap under real load — unit-tested by the PR (eviction + reclaim + oversized-serve, all killed by M7-family mutations), not load-tested.
  • Windows (PR author tested macOS only). This run is Linux, the previously-untested lane; the A/B and invalidation cells above are Linux evidence.
  • previous-report.md: none present — first round.

Methodology

Environment: CI verify container (node:22-bookworm, user node, no root, no strace — hence a gcc-compiled LD_PRELOAD openat/openat64 shim logging <monotonic_ns> <pid> <R|W> <path> filtered by chats-dir prefix; sources tmp/fscount.c). Head build = CI's npm run build at the merge commit; base control = git worktree add tmp/base-tree HEAD^1 + scripts/build_package.js for packages/cli only (wired to the already-installed root node_modules; realpath of @qwen-code/qwen-code-core asserted → head tree core, which the PR does not touch; base dist asserted free of persistedSessionListCache symbols, head dist asserted to contain them). Harness harness/ab-harness.mjs boots one daemon per arm (serve --port <free> --workspace <scratch ws> with isolated QWEN_RUNTIME_DIR/QWEN_HOME), seeds deterministic fixtures, and runs cold/warm/expire/numeric/session-info phases with per-phase fs-log windows; harness/inv-harness.mjs drives delete/archive/unarchive/metadata/failure-recovery on both arms. Mutation matrix: 10 single-point mutants applied as exact-string edits (9 guards — harness/M*.json specs, raw vitest output in harness/logs/mut-M*.log — plus one positive control inverting the pre-existing pinned-first ordering to prove the suite is live in this area), each run against its intended target test and restored afterward. Targeted gates: npx vitest run on the five changed suites in packages/cli (1338/1338 green: server.test.ts 913, transport.test.ts 299, multi-workspace-sessions.test.ts 101, live-task-service.test.ts 14, persisted-session-list-cache.test.ts 11). Raw logs live under harness/ and harness/logs/; evidence images: 01-ab-catalog-reads-head-vs-base.png, 02-invalidation-head-vs-base.png, 03-mutation-matrix.png.

Assertion ledger (41 scripted checks → assertions.json 40 pass / 1 fail)

group checks result
A/B (ab-assert.mjs: head cache cells, base control validity, parity, reduction, wire shape) 17 17 pass
Mutation freshness (inv-harness.mjs, 15 distinct assertions executed on both arms, identical outcomes) 15 15 pass
Mutation matrix pinning (M1–M9 guards + positive control; each "target test catches mutant") 10 9 pass, 1 fail (M5 — Finding #1)

Targeted-gate totals (1338/1338) are cited in Methodology and not double-counted in the ledger. The M5 row's fail is the Finding #1 test-pinning gap: the shipped behavior is correct (my probe passes on clean source), but the PR's own test does not isolate the guard.

Evidence images

01-ab-catalog-reads-head-vs-base

02-invalidation-head-vs-base

03-mutation-matrix

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

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

Reviewed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): You are review agent reverse-audit — Reverse audit agen...: none — no checks left unfinished.; You are review agent reverse-audit — Reverse audit agen...: none — all checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above were completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above reached a conclusion within budget., and 1 more.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):You are review agent reverse-audit — Reverse audit agen...:none — no checks left unfinished.;You are review agent reverse-audit — Reverse audit agen...:none — all checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above were completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above reached a conclusion within budget.,另有 1 条。

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +164 to +166
expect(newLookup.status).toBe('scan');
oldLoad.resolve(snapshot(1));
await oldLookup.promise;

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 install-side generation guard (current.generation === generation in the install condition, persisted-session-list-cache.ts ~104) is pinned by no test: both superseded-load tests perform a second cache.lookup() immediately after invalidate(), which replaces slot.inFlight before the old load settles, so the promise-identity check fails first and the generation conjunct is never consulted. — Failure scenario: probe-verified — removing the conjunct leaves this file (11/11) and server.test.ts (915/915) green. Reachable in production: loadAllPersistedSummaries pages up to 50k sessions plus worktree-sidecar reads, so a mutation invalidation landing mid-scan with no list request before completion installs the pre-mutation snapshot, and the next list within the 2 s TTL serves stale data (archived session in the active list, deleted session still present). A discriminating probe (invalidate mid-scan, old load resolves with no intervening lookup) fails on the mutant with expected 'cache_hit' to be 'scan'. Suggested fix: add a superseded-load test with no intervening lookup — lookup (scan starts) → invalidate() → old load resolves → next lookup must be 'scan'.

中文说明

安装侧的 generation 守卫(安装条件中的 current.generation === generation,persisted-session-list-cache.ts ~104)没有任何测试固定:两个「被取代 load」测试都在 invalidate() 之后立即执行第二次 cache.lookup(),这会在旧 load settle 之前替换掉 slot.inFlight,因此 promise 身份检查会先失败,generation 条件分支根本不会被执行到。——失败场景:已用探针验证——删除该条件后本文件(11/11)与 server.test.ts(915/915)仍然全绿。生产可达:loadAllPersistedSummaries 分页扫描最多 5 万个会话并逐个读取 worktree sidecar,若 mutation 的失效落在扫描中途、且扫描完成前没有新的 list 请求,则会安装变更前的快照,随后 2 秒 TTL 内的 list 将返回过期数据(active 列表出现已归档会话、已删除会话仍然存在)。判别探针(扫描中途 invalidate、旧 load 在无中间 lookup 时 resolve)在突变体上以 expected 'cache_hit' to be 'scan' 失败。建议修复:新增一个无中间 lookup 的被取代 load 测试——lookup(开始扫描)→ invalidate() → 旧 load resolve → 下一次 lookup 必须为 'scan'

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +127 to +131
const lookup = cache.lookup(SCOPE, () => reload.promise);
expect(lookup.status).toBe('scan');

reload.resolve(snapshot());
await lookup.promise;

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] This read-path TTL test ('rejects an installed value when its read-path age reaches the TTL') pins only the expiry decision, not the teardown that accompanies it: dropping the read-path this.removeValue(slot) call and proceeding straight to rescan leaves all 11 tests green — the reload's installValue overwrites slot.value and increments retainedSummaries without the matching decrement, and the trailing cache.clear() masks the residual state. — Failure scenario: probe-verified — under the mutant a mechanism probe (cap 9, three scopes, one read-path expiry+reinstall) shows the leaked count persists (evictFor evicts a healthy in-TTL scope) while shipped code keeps it; on a long-lived daemon listing a hot workspace at >2 s intervals each expired→reinstall cycle leaks its session count permanently, and once drift exceeds the 50,000 cap every install evicts all other scopes' healthy snapshots, collapsing the cache into a full rescan per list request. Production code is correct; the suite cannot catch a teardown-removing regression. Suggested fix: use a small maxRetainedSummaries, install a second scope after the expiry/reinstall, and assert both scopes are still cache_hit — the inflated counter under the mutant would evict one of them.

中文说明

这个读路径 TTL 测试('rejects an installed value when its read-path age reaches the TTL')只固定了过期判定,没有固定伴随的清理动作:删除读路径上的 this.removeValue(slot) 调用并直接重新扫描,全部 11 个测试仍为绿——reload 的 installValue 会覆盖 slot.value 并在没有对应扣减的情况下增加 retainedSummaries,结尾的 cache.clear() 又掩盖了残留状态。——失败场景:已用探针验证——在该突变体下,机制探针(容量 9、三个 scope、一次读路径过期+重装)显示泄漏计数持续存在(evictFor 会逐出一个仍在 TTL 内的健康 scope),而现有代码不会;在长期运行、以超过 2 秒间隔重复列出同一热点 workspace 的守护进程中,每次过期→重装都会永久泄漏其会话计数,漂移超过 50,000 容量后每次安装都会逐出所有其他 scope 的健康快照,缓存退化为每次 list 都全量重扫。生产代码本身是正确的,问题在于测试套件无法捕获「删除清理逻辑」这类回归。建议修复:使用较小的 maxRetainedSummaries,在过期/重装后再安装第二个 scope,并断言两个 scope 仍为 cache_hit——突变体下膨胀的计数会逐出其中之一。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +197 to +199
it('evicts the oldest retained snapshot to honor the global cap', async () => {
vi.useFakeTimers();
const cache = new PersistedSessionListCache(10_000, 2);

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 eviction tests never hold more than one valued snapshot at the moment eviction runs, so neither the oldest-first victim-selection rule nor the multi-round while loop in evictFor is discriminated — any arbitrary single-eviction policy passes. — Failure scenario: probe-verified — inverting the selection comparison to newest-first leaves all 11 tests green, and replacing the while with an if (single-pass eviction) also leaves all 11 green (liveness check: disabling eviction entirely fails the cap test, so the file can fail — it just cannot discriminate these mutants). The production singleton runs cap 50,000, unreachable by integration tests; a newest-first or single-pass regression degrades hot workspaces to full disk scans per list request once retained summaries exceed the cap. Suggested fix: restructure so ≥2 valued snapshots coexist when eviction fires and assert which one survives; note a cap-3/three-install shape catches only the selection mutant — discriminating the while loop additionally requires forcing two eviction rounds in one install (e.g. cap 5: A(2), B(2), C(1), then D(4)).

中文说明

逐出(eviction)测试在触发逐出的时刻从未同时持有多于一个带值快照,因此「最旧优先」的选择规则和 evictFor 的多轮 while 循环都没有被区分——任意单轮逐出策略都能通过。——失败场景:已用探针验证——把选择比较反转为「最新优先」后 11 个测试仍全绿;把 while 换成 if(单轮逐出)同样全绿(活性检查:完全禁用逐出会让容量测试失败,说明该文件可以失败——只是无法区分这两个突变体)。生产环境的单例容量为 50,000,集成测试无法触及;一旦保留摘要数超过容量,「最新优先」或单轮逐出回归会使热点 workspace 退化为每次 list 都全量磁盘扫描。建议修复:重构测试使逐出触发时至少有 2 个带值快照并存,并断言哪一个存活;注意 cap=3/三次安装的形态只能捕获选择突变体——要区分 while 循环还需迫使一次安装触发两轮逐出(例如 cap 5:A(2)、B(2)、C(1),再安装 D(4))。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +104 to +106
it('uses a non-sliding TTL measured from load completion', async () => {
vi.useFakeTimers();
const cache = new PersistedSessionListCache(2_000, 50_000);

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] This test is named 'uses a non-sliding TTL measured from load completion', but it cannot distinguish a scan-start TTL baseline from a load-completion baseline: every loader in this file resolves within microseconds of scan start, so both baselines produce identical cacheAgeMs values in every test. — Failure scenario: probe-verified — a mutant recording the TTL epoch at lookup/scan start instead of completion passes all 11 tests; a scan taking longer than the 2 s TTL would then install an already-expired snapshot (read-path age check fails immediately, expiry timer scheduled with delay 0), so every session-list request rescans and the cache silently never caches. The discriminating probe (5 s load vs 2 s TTL, then expect cache_hit) passes on real code and fails on the mutant. Suggested fix: use a deferred loader, advance time past half the TTL while the load is still in flight, then resolve it and assert cache_hit still holds afterward — under a scan-start baseline the remaining TTL would be exhausted and the assertion would fail.

中文说明

该测试名为 'uses a non-sliding TTL measured from load completion'(TTL 从加载完成起算、不滑动),但它无法区分「从扫描开始起算」与「从加载完成起算」两种 TTL 基准:文件中所有 loader 都在扫描开始后微秒级内 resolve,因此两种基准在每个测试中产生完全相同的 cacheAgeMs。——失败场景:已用探针验证——把 TTL 纪元改为在 lookup/扫描开始时记录的突变体可通过全部 11 个测试;此时一次耗时超过 2 秒 TTL 的扫描会安装一个已过期的快照(读路径年龄检查立即失败、过期定时器以 0 延迟调度),导致每次 session list 都重新扫描,缓存实际上永远不生效。判别探针(5 秒 load 对比 2 秒 TTL,随后期望 cache_hit)在真实代码上通过、在突变体上失败。建议修复:使用延迟 loader,在 load 仍在进行时推进时间超过 TTL 的一半,再 resolve 并断言其后仍为 cache_hit——若基准是扫描开始,则剩余 TTL 已耗尽,断言会失败。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +456 to +460
try {
return await mutation();
} finally {
invalidateSessionLists(runtime, archiveStates);
}

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 invalidate-even-when-the-mutation-rejects (try/finally) semantics of runWithSessionListInvalidation (mirrored in acp-http/dispatch.ts) are untested — no test makes a mutation actually reject; the closest tests throw SessionNotFoundError, which the flows treat as benign and still resolve 200. — Failure scenario: probe-verified — moving invalidation to the success path only leaves all 1315 tests green (server.test.ts 915 + multi-workspace + transport 299), while a comparator mutant removing invalidation entirely fails 3 of the 4 new invalidation tests, so the harness is alive. Real rejection paths exist (409 gate races, DaemonDrainingError, non-benign close failures on the single-session close route) and carry no invalidation assertion; a failed batch would return 500 and leave the warm catalog serving already-removed sessions for up to 2 s. Suggested fix: add a test where the wrapped mutation rejects with a non-benign error, then assert the subsequent organized list rescans instead of hitting the warm snapshot (e.g. via a SessionService.prototype.listSessions spy count).

中文说明

runWithSessionListInvalidation(acp-http/dispatch.ts 中有镜像实现)的「mutation 失败时也同样失效缓存」(try/finally)语义没有被测试——没有任何测试让 mutation 真正 reject;最接近的测试抛出 SessionNotFoundError,而流程把它视为良性错误,仍然返回 200。——失败场景:已用探针验证——把失效逻辑移到仅成功路径后,全部 1315 个测试仍为绿(server.test.ts 915 + multi-workspace + transport 299);而完全移除失效逻辑的对照突变体会使 4 个新失效测试中的 3 个失败,说明测试装置是有效的。真实的 rejection 路径确实存在(409 门控竞态、DaemonDrainingError、单会话 close 路由上的非良性 close 失败),且都没有失效断言;失败的批量操作会返回 500,并使热缓存在最长 2 秒内继续返回已被移除的会话。建议修复:新增一个让被包裹 mutation 以非良性错误 reject 的测试,然后断言随后的 organized 列表是重新扫描而非命中热快照(例如通过 SessionService.prototype.listSessions 的 spy 计数)。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +13578 to +13581
expect(setAttribute).toHaveBeenCalledWith(
'qwen-code.daemon.session_list.scan_duration_ms',
expect.any(Number),
);

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 leader/waiter telemetry test mocks trace.getSpan to return ONE shared {setAttribute} spy for all four requests, so only the union of attributes across requests is observable; consequently the if (lookup.status === 'scan') guard around scan_duration_ms in listAllPersistedSummaries (session-list.ts:499-504) is pinned by no test. — Failure scenario: probe-verified — deleting the status guard so scan_duration_ms is recorded on every lookup leaves this test green (the genuine rescan satisfies the shared spy), while an emission-count assertion fails on the mutant (length of 1 but got 2) — a cache_hit request emits the cached snapshot's original scan duration as its own, polluting scan-latency metrics with hit traffic. Distinct from R1-3 (attribute presence, fixed) and the rejected round-2 failure-phase attribution claim. Suggested fix: mock getSpan to return a fresh setAttribute spy per call (captured in order) and assert per-request attribute sets — the scan request's spy received scan_duration_ms while the cache-hit request's spy did not.

中文说明

leader/waiter 遥测测试把 trace.getSpan mock 为对全部四个请求返回同一个共享的 {setAttribute} spy,因此只能观察到跨请求的属性并集;于是 listAllPersistedSummaries 中围绕 scan_duration_msif (lookup.status === 'scan') 守卫(session-list.ts:499-504)没有任何测试固定。——失败场景:已用探针验证——删除该状态守卫使每次 lookup 都记录 scan_duration_ms 后,本测试仍为绿(真正的重新扫描满足了共享 spy);而发射次数断言在突变体上失败(length of 1 but got 2)——cache_hit 请求会把缓存快照原始的扫描耗时当作自己的发出,用命中流量污染扫描延迟指标。与 R1-3(属性存在性,已修复)及第二轮被驳回的失败阶段归属问题均不相同。建议修复:把 getSpan mock 为每次调用返回一个新的 setAttribute spy(按顺序捕获),并按请求断言属性集合——scan 请求的 spy 收到 scan_duration_ms,而 cache_hit 请求的 spy 没有。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines 2078 to +2079
conn.closingSessions.delete(sessionId);
this.invalidateSessionLists(['active']);

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] R1-1 (round-1 finding, re-verified this round, still standing): the ACP session/close and _qwen/session/update_metadata invalidation call sites have no over-the-wire test — the new transport test covers only _qwen/sessions/{archive,unarchive,delete}. The round-1 deferral is acknowledged; re-reporting because the gap is unchanged at this commit. — Failure scenario: re-verified by probe this round — deleting both invalidateSessionLists(['active']) call sites leaves all 346 ACP tests green (transport.test.ts + workspace-qualified-acp.test.ts). An ACP client that closes a session (or updates its metadata) and re-lists within two seconds would then be served the stale pre-mutation persisted snapshot — the regression the REST-side close/metadata tests were written to catch. Suggested fix: extend the transport test (or add a sibling) to warm an organized session/list, run session/close and _qwen/session/update_metadata, and assert the immediate re-list reflects the post-mutation state without asserting synchronous title durability.

中文说明

R1-1(第一轮发现,本轮重新验证后仍然存在):ACP session/close_qwen/session/update_metadata 的失效调用点没有 over-the-wire 测试——新的 transport 测试只覆盖了 _qwen/sessions/{archive,unarchive,delete}。已知悉第一轮的延期决定;因该缺口在本提交仍未变化,故再次报告。——失败场景:本轮探针重新验证——删除两处 invalidateSessionLists(['active']) 调用点后,全部 346 个 ACP 测试仍为绿(transport.test.ts + workspace-qualified-acp.test.ts)。ACP 客户端关闭会话(或更新其 metadata)后两秒内重新 list,将得到变更前的过期持久化快照——这正是 REST 侧 close/metadata 测试要防范的回归。建议修复:扩展 transport 测试(或新增同级测试),先预热 organized session/list,再执行 session/close_qwen/session/update_metadata,断言紧随其后的重新 list 反映变更后状态,但不要求标题同步落盘。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +467 to +470
addDaemonRequestAttribute(
'qwen-code.daemon.session_list.cache_status',
lookup.status,
);

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] R1-11 (round-1 finding, still standing): the span attributes emitted from listAllPersistedSummaries use setAttribute (overwrite semantics via addDaemonRequestAttributetrace.getSpan(...)?.setAttribute, daemon-tracing.ts:189), so a request performing multiple catalog lookups records only the last lookup's values. The round-1 deferral is acknowledged — correcting it requires an observability design (events or child spans with aggregation semantics) outside this cache PR; re-reporting only to keep the thread open for that follow-up. — Failure scenario: a daemon request that hits several catalog scopes (multiple archive states or workspaces in one handler) emits span attributes describing only the final lookup — metrics/traces attributed to the request misrepresent the earlier lookups' cache status, age, and scan statistics.

中文说明

R1-11(第一轮发现,仍然存在):listAllPersistedSummaries 发出的 span 属性使用 setAttribute(经由 addDaemonRequestAttributetrace.getSpan(...)?.setAttribute 的覆盖语义,daemon-tracing.ts:189),因此执行多次目录 lookup 的请求只会记录最后一次 lookup 的值。已知悉第一轮的延期决定——修正需要本缓存 PR 之外的可观测性设计(事件或带聚合语义的子 span);再次报告仅为保留该后续事项的线程。——失败场景:命中多个目录 scope 的守护进程请求(单个处理器中涉及多个归档状态或 workspace)发出的 span 属性只描述最后一次 lookup——归属于该请求的指标/链路会错误呈现先前 lookup 的缓存状态、年龄与扫描统计。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +108 to +110
} else if (current.value === undefined) {
this.slots.delete(key);
}

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] R1-5 (round-1 finding, re-measured this round, still standing): test-efficacy probe (harness validated) — forcing the current.value === undefined guard to true leaves every test green; no test pins the case where the guard must NOT fire. The round-1 pushback was checked against the code this round and is architecturally sound: installs happen only through the identity-checked success handler, so a superseded load cannot reach this branch after a newer value installs — the guard is defensive cleanup, not a reachable public state transition. Re-reporting the probe fact so the thread can be closed deliberately. — Failure scenario: as filed, with the guard broken an old-generation or oversized load resolving while a newer value is installed would delete the slot and its valid snapshot, orphaning the expiry timer and skewing retained-summary accounting — with no failing test; the transition is unreachable today, so practical risk is limited to future refactors of the install path. Suggested fix: either add the round-1 suggested test (install a value, then resolve a superseded/oversized in-flight load against the same key and assert the lookup is still cache_hit), or accept the guard as defensive and close the thread.

中文说明

R1-5(第一轮发现,本轮重新测量后仍然存在):测试有效性探针(harness 已验证)——把 current.value === undefined 守卫强制为 true 后所有测试仍全绿;没有测试固定该守卫「不应触发」的情形。本轮对照代码核查了第一轮的反馈,其架构论证成立:安装只发生在经过身份检查的成功处理路径中,因此在更新的值已安装后,被取代的 load 不可能进入该分支——该守卫是防御性清理,而非可达的公开状态转换。重新报告探针事实,以便有意识地关闭该线程。——失败场景:如原报告所述,守卫被破坏时,旧一代或超大 load 在更新值已安装时 resolve 会删除 slot 及其有效快照,使过期定时器成为孤儿、污染 retained-summary 计数——且没有测试失败;该转换目前不可达,实际风险仅限于未来对安装路径的重构。建议修复:要么新增第一轮建议的测试(先安装一个值,再让同 key 上被取代/超大的在飞 load resolve,断言 lookup 仍为 cache_hit),要么接受该守卫为防御性代码并关闭线程。

— qwen3.8-max via Qwen Code /review (v0.21.9)

Comment on lines +109 to +112
this.slots.delete(key);
}
}
return snapshot;

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] R1-6 (round-1 finding, re-measured this round, still standing): test-efficacy probe (harness validated) — deleting the added this.slots.delete(key); cleanup leaves every test green; no test asserts that empty slots are removed from the slots map after a non-retained result. The round-1 pushback stands: a follow-up lookup is scan in both cases, so there is no public behavioral discriminator without exposing private Map state. — Failure scenario: with the delete removed, every scope whose load completed without installing (oversized snapshot, generation bumped mid-flight) leaves a permanently empty slot; across many distinct scopes the map grows without bound — a slow leak no test in this diff detects. Cost is bounded by the distinct-scope space (workspaces × 2 archive states) and each entry is tiny, so this is retention hygiene rather than a correctness risk. Suggested fix: either expose a minimal test seam for slot-map size, or accept the bounded hygiene cost and close the thread.

中文说明

R1-6(第一轮发现,本轮重新测量后仍然存在):测试有效性探针(harness 已验证)——删除新增的 this.slots.delete(key); 清理语句后所有测试仍全绿;没有测试断言未保留结果的 load 完成后空 slot 会从 slots map 中移除。第一轮的反馈成立:后续 lookup 在两种情况下都是 scan,若不暴露私有 Map 状态就不存在公开的行为判别依据。——失败场景:若删除该语句,任何 load 完成但未安装值的 scope(超大快照、飞行中被提升 generation)都会留下永久为空的 slot;在大量不同 scope 下 map 无限增长——这是本 diff 中没有任何测试能检测的缓慢泄漏。成本受限于不同 scope 的数量空间(workspace 数 × 2 种归档状态)且每个条目极小,因此属于保留卫生问题而非正确性风险。建议修复:要么为 slot-map 大小暴露一个最小测试接缝,要么接受这一有界的卫生成本并关闭线程。

— qwen3.8-max via Qwen Code /review (v0.21.9)

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

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 11, 2026
Merged via the queue into QwenLM:main with commit 63fbf1b Aug 11, 2026
517 of 530 checks passed
@doudouOUC
doudouOUC deleted the agent/session-list-catalog-cache branch August 11, 2026 09:35
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR is not open for verification (state=MERGED, draft=false).

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR is not open for verification (state=MERGED, draft=false)。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.10.

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.

3 participants