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

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.

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

1 similar comment
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

2 participants