Skip to content

fix(core): isolate fork cache readers by session - #9471

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
tomsen02:fix/cache-safe-session-ownership
Aug 24, 2026
Merged

fix(core): isolate fork cache readers by session#9471
wenshao merged 4 commits into
QwenLM:mainfrom
tomsen02:fix/cache-safe-session-ownership

Conversation

@tomsen02

@tomsen02 tomsen02 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This change makes cached fork snapshots session-owned at every background read boundary. A reader now receives a snapshot only when its session ID matches the session that last saved it; otherwise the optional background operation fails closed without creating a forked chat or invoking an agent. Same-session suggestion, speculative generation, pipelined speculation, and automatic memory extraction keep their existing behavior.

Why it's needed

An ACP channel can multiplex multiple sessions in one process, while the cached fork snapshot is held in one process-global slot. If session B saves after session A, an unscoped background reader for A can consume B's history. This can generate follow-up work from the wrong conversation and can send another session's transcript to a model. Fixes #9470.

Reviewer Test Plan

How to verify

Save a cached snapshot for session B, then invoke speculative generation or automatic memory extraction using a configuration for session A. Confirm that the lookup returns no snapshot and that neither a forked chat nor a forked agent is invoked. Repeat with session B as the reader and confirm that initial speculation, pipelined speculation, and memory extraction still receive B's history. The focused unit tests also assert that every production background reader supplies its active session ID.

Evidence (Before & After)

Before: a function-level dynamic probe saved B's history and then invoked readers configured for A. Initial speculation created a forked chat from B's history, pipelined speculation reused B's history, and automatic memory extraction passed B's history to its agent.

After: the same probe reports no snapshot for B-to-A reads and zero downstream forked-chat or agent calls. B-to-B reads continue successfully for initial speculation, pipelined speculation, and memory extraction. The post-fix probe passed 3/3 scenarios, and the focused unit suite passed 153/153 tests. Review follow-up coverage also confirms both speculation readers fail closed, records a foreign auto-memory cache owner as a benign skip without advancing the extraction cursor, and short-circuits an existing mismatch before scaffold and cursor I/O while retaining a second check for ownership changes during that asynchronous work.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Node.js 22.22.3 on macOS. Verified with a function-level dynamic integration probe, focused unit tests, the core lint task, the repository build, and TypeScript type checking. No provider credentials or model network calls were used.

Risk & Scope

  • Main risk or tradeoff: if another session overwrites the single cached slot before optional background work reads it, that work is skipped instead of using a foreign snapshot.
  • Not validated / out of scope: full ACP daemon and live-model end-to-end testing; replacing the single slot with a per-session cache; unrelated cache-prefix behavior.
  • Breaking changes / migration notes: none. Same-session reads are unchanged.

Linked Issues

Fixes #9470

中文说明

本 PR 的改动

本改动让所有后台读取边界都按 session 校验缓存的 fork 快照。只有读取方的 session ID 与最近一次保存快照的 session 一致时,读取方才能获得快照;否则可选的后台操作会安全终止,不会创建 fork chat,也不会调用 agent。同一 session 内的后续建议、推测生成、流水线推测和自动记忆提取保持原有行为。

为什么需要

一个 ACP channel 进程可以复用来承载多个 session,但缓存的 fork 快照只保存在一个进程级全局槽中。如果 session B 在 session A 之后保存,A 的未限定后台读取就可能消费 B 的历史。这会基于错误会话生成后续任务,也可能把另一 session 的对话记录发送给模型。修复 #9470

Reviewer 测试计划

如何验证

先为 session B 保存缓存快照,再用 session A 的配置调用推测生成或自动记忆提取。确认读取不到快照,而且 fork chat 和 fork agent 都没有被调用。然后改用 session B 读取,确认初次推测、流水线推测和记忆提取仍能正常收到 B 的历史。聚焦单元测试还会断言每个生产后台读取方都传入其当前 session ID。

证据(修复前后)

修复前:函数级动态探针保存 B 的历史后,调用配置为 A 的读取方。初次推测使用 B 的历史创建了 fork chat,流水线推测复用了 B 的历史,自动记忆提取也把 B 的历史传给了 agent。

修复后:同一个探针显示 B 到 A 的读取无法获得快照,下游 fork chat 和 agent 调用数均为零。B 到 B 的读取在初次推测、流水线推测和记忆提取中继续成功。修复后探针的 3/3 个场景通过,聚焦单元测试 153/153 通过。审查跟进测试还确认两条推测读取路径都会安全失败,将外来 auto-memory 缓存所有者记录为良性跳过且不推进提取游标,并在脚手架和游标 IO 前快速跳过已存在的不匹配,同时保留第二次检查以捕获异步工作期间的所有权变化。

已测试平台

系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS 上的 Node.js 22.22.3。已通过函数级动态集成探针、聚焦单元测试、core lint、仓库构建和 TypeScript 类型检查验证。未使用 provider 凭据或模型网络调用。

风险与范围

  • 主要风险或取舍:如果可选后台任务读取前,单一缓存槽已被另一 session 覆盖,该后台任务会被跳过,而不会使用外来快照。
  • 未验证 / 范围外:完整 ACP daemon 加真实模型的端到端测试;把单一槽改造成按 session 存储的缓存;无关的缓存前缀行为。
  • 破坏性变更 / 迁移说明:无。同一 session 的读取行为不变。

关联 Issue

修复 #9470

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 19, 2026
@tomsen02

tomsen02 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Verification report

Evidence level: function-level dynamic integration test against production readers; this was not a full ACP daemon or live-model E2E run.

Baseline reproduction

  • Saved a real cache snapshot owned by session B.
  • Invoked initial speculation, pipelined speculation, and automatic memory extraction with a configuration owned by session A.
  • Before the fix, all three readers consumed B's history; the initial speculation path created a forked chat from that foreign history, and the memory path passed it to the forked agent.

Post-fix verification

  • Foreign B-to-A reads returned no snapshot.
  • Initial speculation made 0 forked-chat calls and 0 forked-agent calls.
  • Automatic memory extraction made 0 forked-agent calls.
  • Same-session B-to-B reads remained successful for initial speculation, pipelined speculation, and automatic memory extraction.
  • Dynamic probe: 3/3 scenarios passed.
  • Focused unit suite after review follow-up: 152/152 tests passed across 6 files.
  • Both speculation readers now have explicit null fail-closed coverage.
  • A foreign auto-memory cache owner is recorded as skipped with skipped_reason=session_mismatch; the extraction cursor is not advanced and the planner is not called.
  • An existing mismatch exits before scaffold/cursor I/O; a second pre-planner check catches ownership changes during the asynchronous setup work.
  • Core lint, repository build, TypeScript typecheck, formatting check, and git diff --check all exited 0.

Environment: macOS, Node.js 22.22.3. No provider credentials or model network calls were used.

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

⚠️ Deferred approval withheld — 1 PR CI workflow run(s) on 7055fc3 did not finish green; see the updated table in the Stage 2 comment. Re-run @qwen-code /triage after fixes. finalize run

⚠️ 延迟审批已搁置 —— 7055fc3 有 1 个 PR CI workflow 未以绿色完成,详见 Stage 2 评论中已更新的表格。修复后可重新运行 @qwen-code /triage查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

  • Template ✓ — all sections present, including the bilingual summary.
  • Problem: real and confirmed, not theoretical. bug(core): process-global cache params cross session boundaries in background readers #9470 carries a deterministic reproduction against main@577f719, and I verified it in the current code: speculation.ts (both the initial and the pipelined path) and extractionAgentPlanner.ts read the process-global CacheSafeParams slot without comparing sessionId, while suggestionGenerator.ts has compared it since fix(cli): honour the declared enableCacheSharing default in both suggestion gates #9233. In a multi-session ACP process, a snapshot saved by session B can genuinely flow into session A's speculative generation or memory extraction.
  • Direction: aligned — this is a cross-session content leak (a foreign transcript reaching a background model call, or contaminating another session's memory), i.e. the exact class of bug fix(cli): honour the declared enableCacheSharing default in both suggestion gates #9233 only partially closed. No CHANGELOG reference, but the area is plainly relevant.
  • Size: core paths, 42 production-logic lines (forkedAgent getter + three readers + two comment updates) and 43 test lines. Well under any escalation threshold.
  • Approach: right-sized and minimal — the ownership check moves into getCacheSafeParams(sessionId) itself (fail closed on mismatch), each production reader passes its own session id, and the now-redundant double-check in the suggestion generator is dropped. The larger alternative (a per-session cache map) is explicitly out of scope and would need eviction logic this bug doesn't justify. No drive-by changes in the diff.
  • Risk: no high-risk-path matches; no elevated risk signals.

Moving on to code review. 🔍

中文说明

感谢贡献!

  • 模板 ✓ —— 各部分齐全,包含中文说明。
  • 问题:真实存在且已确认,不是理论性问题。bug(core): process-global cache params cross session boundaries in background readers #9470 提供了在 main@577f719 上的确定性复现,且我在当前代码中核实:speculation.ts(初次推测与流水线推测两条路径)和 extractionAgentPlanner.ts 读取进程级全局的 CacheSafeParams 槽时并不比对 sessionId,而 suggestionGenerator.tsfix(cli): honour the declared enableCacheSharing default in both suggestion gates #9233 起已有比对。在多 session 的 ACP 进程中,session B 保存的快照确实可能流入 session A 的推测生成或记忆提取。
  • 方向:对齐 —— 这是跨 session 内容泄漏(外来对话记录进入后台模型调用,或污染另一 session 的记忆),正是 fix(cli): honour the declared enableCacheSharing default in both suggestion gates #9233 只部分解决的那类 bug。CHANGELOG 无直接提及,但该领域显然相关。
  • 规模:核心路径,42 行生产逻辑(forkedAgent getter + 三个读取方 + 两处注释更新)、43 行测试,远低于任何升级阈值。
  • 方案:范围恰当且最小化 —— 所有权校验下沉到 getCacheSafeParams(sessionId) 内部(不匹配即 fail closed),每个生产读取方传入自身 session id,suggestion generator 中冗余的双重校验随之移除。更大的替代方案(按 session 的缓存 map)被明确列为范围外,且需要本 bug 并不必要的淘汰逻辑。diff 中无夹带改动。
  • 风险:未命中高风险路径;无升级风险信号。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 7055fc38ddcf68b0225a43a04f37e5f2d007ea31 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

The approach is the one I'd have picked independently: enforce ownership in the getter, fail closed, and pass every production reader's own session id. Comparing against the base:

  • Completeness — the important part. I grepped every getCacheSafeParams( call site in the tree: the three production readers this PR updates (initial speculation, pipelined speculation, memory-extraction planner) plus the suggestion generator are the only non-test readers; remaining argument-less calls are all tests, which is the documented escape hatch. Config.getSessionId() returns a plain string, so production readers always activate the guard.
  • Fail-closed semantics check out. A slot with a missing/foreign sessionId returns null. Downstream, that's handled: AppContainer already treats a rejected startSpeculation() as non-blocking (.catch(() => {})), the memory manager catches and logs the extraction throw, and the pipelined path returns null. These paths all existed for the empty-slot case, so the mismatch adds no new failure mode — the optional background work is simply skipped.
  • The suggestion-generator simplification is sound. The old double-check (cacheSafeSessionId === sessionId at read, then cacheSafe.sessionId === config.getSessionId() at use) collapses into the getter; the session_mismatch telemetry label is preserved via getCacheSafeParamsSessionId(). Behavior is equivalent for every defined/undefined combination.
  • Tests pin the change: a direct cross-session rejection test on the getter, and toHaveBeenCalledWith('<session>') assertions on every production reader — so the suite fails both if the guard is removed and if a reader stops passing its session id. The speculation.test.ts mock move to vi.hoisted() follows the repo convention for mocks consumed by vi.mock().

No blockers, no convention violations.

CI test evidence

The unit suite for this commit is still running; this table reflects one fetch and is updated in place by the finalize job once CI settles. No failures so far — security checks, desktop shell builds, and the PR precheck are green, with the ubuntu unit test job still in flight and the macOS/Windows/integration legs skipped by CI classification.

Final CI results for 7055fc3 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
web-shell E2E Smoke (ubuntu-latest, Node 22.x) 🚫 cancelled
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

What the suite can and cannot settle: the new unit tests pin the mechanism (getter guard + per-reader wiring), but multi-session daemon behavior — an ACP process actually interleaving two sessions — is covered only by the author's self-reported function-level probe (author's claim, not independently re-run). Sandboxed verification would settle this: @qwen-code /verify as a sponsored run (the author is a fork contributor without write access) — it would A/B-prove that a foreign snapshot really does fail closed for speculation and memory extraction while same-session reads keep working. Note for the maintainer: a sponsored run screens the PR before executing it and wipes its workspace, but the code under verification is still adversarial input — read the resulting report with the same skepticism as the fork's own CI logs.

中文说明

代码审查

方案与我独立想到的做法一致:把所有权校验下沉到 getter,不匹配即 fail closed,每个生产读取方传入自己的 session id。对照基础代码:

  • 完整性(关键点):全仓搜索了所有 getCacheSafeParams( 调用点——本 PR 更新的三个生产读取方(初次推测、流水线推测、记忆提取 planner)加上 suggestion generator 就是全部非测试读取方;其余无参调用均为测试,属于文档中保留的逃生口。Config.getSessionId() 返回普通 string,生产读取方必然触发守卫。
  • fail-closed 语义成立sessionId 缺失或外来时返回 null。下游均能处理:AppContainer 本来就把 startSpeculation() 失败当作非阻塞(.catch(() => {})),memory manager 会捕获并记录提取抛错,流水线路径返回 null。这些路径在空槽场景下早已存在,不匹配只是新增一种触发方式,可选后台任务被安全跳过。
  • suggestion generator 的简化正确:旧的双重校验合并进 getter,session_mismatch 遥测标签经 getCacheSafeParamsSessionId() 保留,各种 defined/undefined 组合下行为等价。
  • 测试钉住了改动:getter 有直接的跨 session 拒绝测试,每个生产读取方都有 toHaveBeenCalledWith('<session>') 断言——移除守卫或读取方不再传 session id 都会挂测试。speculation.test.ts 把 mock 移入 vi.hoisted() 符合仓库对 vi.mock() 消费 mock 的约定。

无阻塞问题,无规范违反。

CI 测试证据

该提交的单元测试仍在运行;上表来自一次抓取,CI 结束后由 finalize 任务原地更新。目前无失败——安全扫描、桌面壳构建与 PR 预检均通过,ubuntu 单元测试仍在进行,macOS/Windows/集成测试腿被 CI 分类跳过。

单元测试能钉住机制(getter 守卫 + 读取方接线),但多 session 守护进程层面的行为目前只有作者自述的函数级探针(作者声明,未独立复跑)。如需沙箱验证:@qwen-code /verify 可作为赞助运行(作者是 fork 贡献者,无写权限)——以 A/B 方式证明外来快照在推测与记忆提取下确实 fail closed,且同 session 读取不受影响。请维护者注意:赞助运行会先做执行前风险筛查并清空工作区,但被验证的代码仍是对抗性输入——阅读报告时请保持与阅读 fork CI 日志同等的审慎。

Qwen Code · qwen3.8-max

Reviewed at 7055fc38ddcf68b0225a43a04f37e5f2d007ea31 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — a clean, minimal fix for a reproduced cross-session leak; the one open item is daemon-level evidence, which the unit suite pins only indirectly.

Stepping back: the bug is real (verified in the base code, not just taken from the PR's framing), the fix is exactly the shape I'd have chosen — ownership enforced once in the getter, every production reader passing its own session id, fail closed on mismatch — and it goes slightly further than required by collapsing the suggestion generator's now-redundant double-check. All 42 production lines serve the stated goal; the two comment updates keep documentation truthful. The failure mode is the right one: optional background work gets skipped instead of consuming a foreign transcript.

Why not 5/5: the multi-session ACP scenario end-to-end rests on unit tests plus the author's probe — nothing I can re-run here. That's a reason for the /verify lane suggested above, not a reason to hold the PR.

Approval deferred until CI lands green on 7055fc38ddcf68b0225a43a04f37e5f2d007ea31 — the unit suite is still running, and approving now would attest to a result that doesn't exist yet. If the suite lands green on that commit, the approval follows automatically; if anything lands red or the head moves, it is withheld.

中文说明

置信度:4/5 —— 针对一个已复现的跨 session 泄漏,修复干净且最小化;唯一未决项是守护进程层面的证据,单元测试只能间接钉住。

退一步看:bug 真实存在(已在基础代码中核实,而非照单全收 PR 的描述),修复形态与我独立选择的一致——所有权校验集中在 getter 一处,每个生产读取方传入自身 session id,不匹配即 fail closed——并且还顺手收敛了 suggestion generator 中冗余的双重校验。全部 42 行生产代码都服务于既定目标;两处注释更新保持了文档的准确性。失败模式也是正确的那种:可选后台任务被跳过,而不是消费外来对话记录。

为何不是 5/5:多 session ACP 场景的端到端证据依赖单元测试加作者的探针——我在这里无法复跑。这是建议走 /verify 通道的原因,而不是扣住 PR 的理由。

审批暂缓,等待 CI 在该提交上变绿——单元测试仍在运行,此刻批准等于为一个尚不存在的结果背书。若该提交的测试全绿,审批将自动跟进;若有失败或提交变动,则不予批准。

Qwen Code · qwen3.8-max

Reviewed at 7055fc38ddcf68b0225a43a04f37e5f2d007ea31 · re-run with @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.

中文说明

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

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

options?: { model?: string },
): Promise<SpeculationState> {
const cacheSafe = getCacheSafeParams();
const cacheSafe = getCacheSafeParams(config.getSessionId());

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 fail-closed outcome of the session-scoped lookup is untested for both speculation readers. Every test in this file uses a mock that returns params unconditionally, and the new assertions only pin that the session id is passed — there is no case where getCacheSafeParams returns null, so neither the throw in startSpeculation nor the return null in generatePipelinedSuggestion is exercised. The sibling readers pin the equivalent path (extractionAgentPlanner.test.ts "throws when getCacheSafeParams returns null", suggestionGenerator.test.ts foreign-slot fallback), but speculation does not.

This matters in exactly the multi-session scenario this PR fixes: a future edit relaxing the null guard — for example falling back to an unscoped read to "fix" a speculation-not-starting report — would pass every test in this file green and silently reintroduce the cross-session fork. A mutation probe confirmed it: applying getCacheSafeParams(config.getSessionId()) ?? getCacheSafeParams() here kept the suite 18/18 green while a contention probe showed the mutated code consuming the foreign snapshot past the guard; the same probe passes on the unmodified source.

Add a test setting the mock to return null and asserting startSpeculation rejects with 'CacheSafeParams not available for speculation' and neither createForkedChat nor runForkedAgent is called, plus the analogous null case for the pipelined suggestion path:

it('does not start speculation when the session-scoped lookup returns null', async () => {
  forkedAgentMocks.getCacheSafeParams.mockReturnValue(null);
  await expect(startSpeculation(config, 'read a.ts')).rejects.toThrow(
    'CacheSafeParams not available for speculation',
  );
  expect(createForkedChat).not.toHaveBeenCalled();
  expect(forkedAgentMocks.runForkedAgent).not.toHaveBeenCalled();
});
中文说明

两个推测(speculation)读取方的 fail-closed(会话校验失败返回 null)结果都没有测试覆盖。本文件中所有测试都使用无条件返回 params 的 mock,新增断言也只钉住了「传入了 session id」这一行为——没有任何用例让 getCacheSafeParams 返回 null,因此 startSpeculation 的 throw 和 generatePipelinedSuggestionreturn null 都不会被执行到。相邻的读取方钉住了等价路径(extractionAgentPlanner.test.ts 的 "throws when getCacheSafeParams returns null"、suggestionGenerator.test.ts 的外部 slot 回退),唯独 speculation 没有。

这正是本 PR 修复的多 session 场景下的风险所在:未来若有人放宽 null 守卫——例如为了「修复」推测不启动的问题而回退到无 session 限定的读取——本文件的所有测试仍会全绿,跨 session fork 会被悄悄重新引入。变异探针证实了这一点:在此处应用 getCacheSafeParams(config.getSessionId()) ?? getCacheSafeParams() 后,测试套件仍 18/18 全绿,而竞争探针显示变异后的代码越过守卫消费了外来快照;同一探针在未修改的源码上通过。

建议新增一个测试:将 mock 设为返回 null,断言 startSpeculation 以 'CacheSafeParams not available for speculation' 拒绝,且 createForkedChatrunForkedAgent 均未被调用;流水线推测路径也加类似的 null 用例。

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bd1babe. Added fail-closed coverage for both speculation readers: the initial null lookup now asserts rejection with zero forked-chat/agent calls, and a null second lookup completes speculation without producing a pipelined suggestion or calling the forked agent.

projectRoot: string,
): Promise<AutoMemoryExtractionExecutionResult> {
const cacheSafe = getCacheSafeParams();
const cacheSafe = getCacheSafeParams(config.getSessionId());

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] With this change, a session-mismatched extraction now fails closed by throwing — but the throw is recorded downstream as a failure, not the benign skip the PR description ("that work is skipped") and the linked issue's fix direction ("extraction: skip the run") describe. The error message also names only the empty-slot cause, so it misdiagnoses the mismatch case.

Concrete shape: in the issue's own multi-session scenario, session B's completed turn overwrites the single slot before session A's queued extraction reads it (extraction is serialized per projectRoot in memory/manager.ts, so A's run can lag well behind A's turn). The planner throws, and runExtract in manager.ts catches and unconditionally records status: 'failed' plus MemoryExtractEvent({status:'failed'}) — for an expected, benign race that recurs whenever two sessions interleave turns, over-counting failure telemetry and pointing oncall at the turn-completion path instead of slot contention. A probe driving the real planner gate showed mismatch-slot and empty-slot errors are byte-identical:

PROBE mismatch-slot: runAutoMemoryExtractionByAgent: no cache-safe params available; extraction must run after a completed main turn.
PROBE empty-slot: runAutoMemoryExtractionByAgent: no cache-safe params available; extraction must run after a completed main turn.
PROBE matching-slot: resolved, touchedTopics=[]

The codebase already models benign no-runs as status: 'skipped' with a skippedReason — the memory-pressure gate in the very same runExtract — and MemoryExtractEvent already carries a skipped_reason union. Distinguish the foreign-slot case from the empty-slot case (e.g. when getCacheSafeParamsSessionId() differs from config.getSessionId(), surface a skip with skippedReason: 'session_mismatch'), or at minimum extend the message to name both causes.

中文说明

本次改动后,session 不匹配的记忆提取会以抛错的方式 fail closed——但该抛错在下游被记录为失败(failed),而不是 PR 描述("该后台任务会被跳过")和关联 issue 修复方向("extraction: skip the run")所说的良性跳过。错误信息也只描述了空槽这一种原因,会误导不匹配场景的排查。

具体场景:在 issue 本身的多 session 场景下,session B 完成的回合可能在 session A 排队的提取读取之前覆盖唯一槽位(提取在 memory/manager.ts 中按 projectRoot 串行化,A 的执行可能远滞后于 A 的回合)。planner 抛错后,manager.ts 的 runExtract 捕获并无条件记录 status: 'failed' 并发出 MemoryExtractEvent({status:'failed'})——而这其实是两个 session 交替回合时必然反复出现的良性竞争:失败遥测被抬高,值班排查也会被引向回合完成/保存路径,而非真正的槽位竞争。驱动真实 planner 守卫的探针显示,不匹配槽与空槽的报错逐字节相同(见上方代码块)。

代码库已经把良性的「不执行」建模为 status: 'skipped'skippedReason——就在同一个 runExtract 里的内存压力守卫——且 MemoryExtractEvent 已有 skipped_reason 联合类型。建议把外来槽场景与空槽场景区分开(例如当 getCacheSafeParamsSessionId()config.getSessionId() 不一致时,以 skippedReason: 'session_mismatch' 记为跳过),至少也应扩展错误信息,把两种原因都写明。

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

@tomsen02 tomsen02 Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bd1babe. Auto-memory extraction now recognizes a foreign cache owner immediately before entering the planner and returns skippedReason=session_mismatch without advancing the cursor. The manager records the task and telemetry event as skipped rather than failed, including the structured skipped reason. Added extract and manager tests for cursor preservation, zero planner calls, task state, and telemetry.

Comment on lines +151 to +156
if (
expectedSessionId !== undefined &&
currentCacheSafeParams.sessionId !== expectedSessionId
) {
return null;
}

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 mismatch path returns null identically to the never-captured path with no log, so cross-session slot contention — the exact condition this PR is about — is invisible: a repo-wide grep finds session_mismatch logged in exactly one place (the suggestion generator's debug line). In a multi-session daemon, session A's readers intermittently get null whenever session B owns the slot — startSpeculation throws into AppContainer's no-op .catch, the pipelined suggestion disappears bare, and the planner throws with a message that does not name mismatch — leaving an oncall debugging "background speculation/extraction randomly stops working" nothing to grep for to tell contention apart from "no turn completed yet".

Emit a debug log before returning null (debugLogger already exists in this module):

Suggested change
if (
expectedSessionId !== undefined &&
currentCacheSafeParams.sessionId !== expectedSessionId
) {
return null;
}
if (
expectedSessionId !== undefined &&
currentCacheSafeParams.sessionId !== expectedSessionId
) {
debugLogger.debug(
`CacheSafeParams session mismatch: slot owned by ${currentCacheSafeParams.sessionId}, requested ${expectedSessionId}`,
);
return null;
}
中文说明

不匹配分支返回 null 的方式与「尚未捕获」路径完全相同,且没有任何日志,因此跨 session 槽位竞争——正是本 PR 针对的状况——是不可见的:全仓 grep session_mismatch 只有一处命中(suggestionGenerator 的 debug 日志)。在多 session 守护进程中,只要槽位属于 session B,session A 的读取方就会间歇性拿到 null——startSpeculation 抛错后被 AppContainer 的空 .catch 吞掉,流水线推测无声消失,planner 抛出的信息又不提不匹配——值班排查「后台推测/提取随机罢工」时,没有任何可 grep 的线索来区分槽位竞争与「尚未完成回合」。

建议在返回 null 前输出一条 debug 日志(本模块已有 debugLogger)。

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

@tomsen02 tomsen02 Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bd1babe. The scoped getter now emits a DEBUG session_mismatch diagnostic with requested and cached session IDs before returning null. Auto-memory extraction also exposes the condition structurally through skipped_reason=session_mismatch.

@tomsen02

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

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

Partially reviewed — gaps disclosed.

Not explored to full depth (tool budget reached): "agent 6b": NO Budget gap: line — all checks completed within budget.; "agent 4": NO; "agent 1a": NO; "agent 3a": .

Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.

中文说明

仅完成部分审查,审查缺口已披露。

未探索到全部深度(达到工具调用预算):"agent 6b"NO Budget gap: line — all checks completed within budget."agent 4"NO"agent 1a"NO"agent 3a"

未审查:反向审计——没有审计 agent 是用本 skill 构建的 prompt 启动的——负责搜寻评审其余部分遗漏问题的这道工序,即便运行过,也缺失了 brief 承载的方法。

— Qwen3-235B-A22B via Qwen Code /review (v0.21.13)

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

PR head 已漂移:审查锁定的 head 为 7055fc38ddcf68b0225a43a04f37e5f2d007ea31,当前 head 为 bd1babe52661d474aba7fb19ef6372ff3b022d5a。原 inline 评论可能因行号失效,此处仅保留汇总。

结论

ISSUES_FOUND(一轮 deepseek-v4-flash 审查发现建议项)

要点

  • R1-1 / R1-2speculation.ts:145extractionAgentPlanner.ts:251 的错误信息把「没有保存 params」和「params 属于另一 session」混为一谈,多 session daemon 调试时易误导。建议:调用 getCacheSafeParamsSessionId() 区分并在日志中带上 owning session ID。
  • R1-3generatePipelinedSuggestionspeculation.ts:721)在 session 不匹配时静默返回 null,缺少诊断信息。建议:增加 debugLogger.debug 日志。

验证结果

  • packages/core 构建成功(exit 0)
  • 4 个相关测试文件共 81/81 通过
  • 根本原因定位为客户端 scope gap,修复方案与 issue triage 建议一致

原始审查输出摘要

Review complete: pr-9471 — COMMENT posted (0 Critical, 0 Suggestion inline)
Posted: #9471 (review)

audio-capture 构建失败(缺少 Python/node-gyp)为预先存在的环境问题,与本次 PR 无关。

@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 — no blockers. Suggestions are inline.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/core/src/utils/forkedAgent.ts:147 — [review] the session-ownership gate is opt-in: getCacheSafeParams() without an id returns whatever the process-global slot holds, so a future reader can silently reopen the #9470 leak
中文说明

已审查——无阻断问题。 建议见行内评论。

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

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

Comment thread packages/core/src/memory/extract.ts Outdated
Comment on lines +164 to +168
const cachedSessionId = getCacheSafeParamsSessionId();
if (
cachedSessionId !== undefined &&
cachedSessionId !== params.config.getSessionId()
) {

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 O(1) session-mismatch gate runs only after ensureAutoMemoryScaffold (mkdir + three writeFileIfMissing round-trips), ensureUserAutoMemoryScaffold, readExtractCursor (readFile + JSON.parse), and the params.history.slice(startOffset).some(...) scan that allocates a copy of the entire unprocessed tail. Because the skip deliberately does not advance the cursor, every subsequent user turn of a mismatched session repeats all of that IO over a slice that grew since the last skip — cumulative wasted work is quadratic in the number of consecutive mismatched turns. This is not a rare path: in the multi-session daemons this PR targets, a probe racing saves into the pre-gate window resolved 15/15 iterations as session_mismatch, so the ordering is paid routinely under contention.

Hoist the gate to the top of runAutoMemoryExtract, immediately after the !params.config guard — it needs only the slot id and params.config.getSessionId(), so moving it is semantics-preserving (cachedSessionId === undefined still falls through to the existing empty-cache path) — and delete this block from its current position:

const cachedSessionId = getCacheSafeParamsSessionId();
if (
  cachedSessionId !== undefined &&
  cachedSessionId !== params.config.getSessionId()
) {
  debugLogger.debug('Skipping auto-memory extract: session_mismatch.');
  return {
    touchedTopics: [],
    skippedReason: 'session_mismatch',
    cursor: {
      sessionId: params.sessionId,
      updatedAt: now.toISOString(),
    },
  };
}
中文说明

这个 O(1) 的 session 不匹配检查在 ensureAutoMemoryScaffold(mkdir + 三次 writeFileIfMissing 往返)、ensureUserAutoMemoryScaffoldreadExtractCursor(readFile + JSON.parse)以及 params.history.slice(startOffset).some(...) 扫描(会为整个未处理的尾部历史分配一份拷贝)之后才执行。由于 mismatch 跳过故意不推进游标,处于 mismatch 状态的 session 每个后续 user turn 都会在比上次更大的切片上重复上述全部 IO —— 累计浪费的工作量随连续 mismatch turn 数呈二次方增长。这并非罕见路径:在本 PR 面向的多 session daemon 场景中,一个向 gate 前窗口注入并发保存的探针实验 15/15 次迭代都得到 session_mismatch,说明该顺序开销在槽竞争下是常规代价。

建议把该检查上移到 runAutoMemoryExtract 开头、!params.config 守卫之后 —— 它只需要槽的 session id 和 params.config.getSessionId(),移动后语义不变(cachedSessionId === undefined 仍会落入既有的空缓存路径),并删除当前位置的这段代码块。

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 20bd886 with an early fast-path plus the existing pre-planner check. The early check avoids scaffold, cursor IO, and history scanning when the slot is already foreign. The second check is intentionally retained because those operations await: the slot can be replaced after the early check, and deleting the late check would turn that benign race back into a failed extraction. Tests cover both cases: an existing mismatch leaves an absent project root uncreated, while an owner sequence of session-1 then session-2 is caught before the planner without advancing the cursor. The focused suite passes 152/152.

@tomsen02

Copy link
Copy Markdown
Contributor 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.

No blocking issues. LGTM! ✅

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • packages/core/src/utils/forkedAgent.ts:147 — [review] the session-ownership gate is opt-in: getCacheSafeParams() without an id returns the raw process-global slot, so a future reader can silently reopen the #9470 leak
  • packages/core/src/followup/speculation.ts:144 — [review] speculation's mismatch failure is indistinguishable from an empty cache and is swallowed by its only caller — no telemetry, unlike the extract boundary
中文说明

无阻断问题。LGTM!✅

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

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

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real-stack A/B on macOS

I rebuilt this PR locally and ran it against a real CLI bundle and a real (local, recording) model endpoint rather than re-running the author's unit probe. Verdict: the fix does what it claims, with no regression on the same-session path, and every guard it adds is covered by a test. One reachability caveat and two small follow-ups are noted at the end.

Environment

OS / runtime macOS (Darwin 25.6.0), Node v24.18.1, npm 11.16.0
BASE aa0178df29 — the PR's merge-base on main
HEAD 955231f5cd — PR 9471 tip
Build two separate worktrees, each npm ci && npm run build && npm run bundle → real dist/cli.js
Model local recording OpenAI-compatible server. No provider credentials, no outbound model calls.

Sanity check on the two bundles: CacheSafeParams session_mismatch is present in HEAD's dist/chunks/*.js and absent from BASE's, so the arms really do differ by this PR.

How the two sessions were produced (please read this part)

The host matters here. saveCacheSafeParams() has exactly one caller — GeminiClient.sendMessageStream() — and that method has exactly two production callers repo-wide:

packages/cli/src/ui/hooks/useGeminiStream.ts:3712
packages/cli/src/nonInteractiveCli.ts:2317, 2643

Both are one-session-per-process hosts, so I used the headless one (qwen -p …), which is the shipped host that actually schedules managed auto-memory extraction. To supply the second session I compiled a 12-line, byte-identical probe (+623 bytes) into both bundles. It is a no-op unless QWEN_PROBE_FOREIGN_SESSION is set; when set, at the end of the turn it calls the production saveCacheSafeParams() with a different session id — exactly what another session in the same process does when its own turn ends. Identical patch on both arms, so any difference is attributable to the PR alone.

Proof the probe is inert: I re-ran the control scenario on the unpatched HEAD bundle and compared the extraction request that hit the wire. Byte-identical, sha256 = 64dcb45de760c91be6e70d5f73bffd5c829796c3982782908bf2eb5336a598b7 for both.

Result — 2×2 matrix

Build Scenario Extraction requests on the wire What reached the model
BASE control (only this session) 1 this session's own transcript
HEAD control (only this session) 1 this session's own transcript
BASE another session owns the slot 1 the OTHER session's transcript
HEAD another session owns the slot 0 nothing — failed closed

A/B matrix

The BASE leak is not inferred from a mock — it is the literal body of POST /v1/chat/completions:

role=system    : "You are now acting as the managed memory extraction subagent…"
role=user      : "CONFIDENTIAL to the other session only: PINEAPPLE-ALPHA-7734"     <-- leaked
role=assistant : "Understood, keeping PINEAPPLE-ALPHA-7734 private."                <-- leaked
role=user      : "Managed memory has TWO directories…"  (task prompt)

On HEAD the same run produces 1 model request instead of 2 (the main turn only), and two independent signals confirm the intended path was taken:

  • debug log — [AUTO_MEMORY_EXTRACT] Skipping auto-memory extract: session_mismatch.
  • telemetry qwen-code.memory.extractstatus=skipped, skipped_reason=session_mismatch on HEAD vs status=completed on BASE for the identical scenario. That also exercises the new skipped_reason plumbing in manager.ts / telemetry/types.ts.

Wire evidence

Do the tests actually gate the fix? — 8/8

I reverted one guard at a time on the PR branch and re-ran the 153-test focused suite. Every mutation was caught, so no guard in this PR is untested:

Mutation matrix

Static checks

Check Result
Focused suite (7 files from this PR) 153 / 153 passed
Full packages/core 20 610 passed, 28 skipped, 0 failed (594 files)
npm run typecheck pass
eslint --max-warnings 0 on touched dirs pass
Merge with current origin/main conflict-free (git merge-tree → 0 conflicts; branch is 163 commits behind)

main has since touched forkedAgent.ts (deep-copy in copyHistoryContainers) and extractionAgentPlanner.ts (dropped ToolNames.LS); neither overlaps the guard this PR adds.

Review notes

1. Reachability today is narrower than #9470's "Impact" section implies. The slot is only written by GeminiClient.sendMessageStream(), i.e. only by the TUI and the headless CLI — both single-session processes. The multi-session host (ACP / daemon Session) runs its own turn loop on GeminiChat and never writes the slot; it only reads it, via generatePromptSuggestion, which #9233 already scoped. So on main today the extraction/speculation cross-session read is latent, not live — which is exactly why my harness has to introduce the second session id itself. This is not an argument against merging: the guard is one line per reader, it closes the class of bug for whichever host lands next, and it matches the precedent set by #9233. It is worth correcting in the issue/changelog wording, though, so the severity isn't overstated.

2. The race window still exists, but it fails closed noisily rather than benignly. (Code reading — not reproduced.) The late check in runAutoMemoryExtract and the hard guard inside runAutoMemoryExtractionByAgent are not atomic. If ownership flips between them, the planner throws no cache-safe params available, MemoryManager.runExtract records the task as failed and emits memory_extract status=failed — instead of the benign skipped / session_mismatch this PR introduces for the same underlying condition. Consider catching that specific throw and mapping it onto the session_mismatch skip so the two orderings report the same thing.

3. An unowned slot now hard-fails where it previously worked. getSessionMismatchResult() treats getCacheSafeParamsSessionId() === undefined as "no mismatch", but getCacheSafeParams(expectedSessionId) rejects a slot whose sessionId is undefined, so such a slot reaches the throw path. Production always passes a session id, and the new preserves the empty-cache failure path test pins this deliberately — flagging it only so the sharp edge is a known one for any future writer of that slot.

Reproducing

Harness, machine-readable per-arm summaries, the captured wire requests and the mutation results are on pr-assets/9471-verify (harness/, evidence/).

中文版

维护者验证 —— macOS 上的真实链路 A/B

我在本地重新构建了这个 PR,用真实的 CLI 打包产物 + 真实(本地、记账型)模型端点跑了一遍,而不是复跑作者的单元探针。结论:修复确实达成了它声称的效果,同一 session 的正常路径没有回归,而且它新增的每一处防护都有测试兜住。 末尾有一条可达性说明和两个小的后续建议。

环境

系统 / 运行时 macOS(Darwin 25.6.0),Node v24.18.1,npm 11.16.0
BASE aa0178df29 —— 本 PR 在 main 上的 merge-base
HEAD 955231f5cd —— PR 9471 顶端
构建 两个独立 worktree,各自 npm ci && npm run build && npm run bundle → 真实 dist/cli.js
模型 本地记账型 OpenAI 兼容服务。未使用任何 provider 凭据,无外网模型调用。

两个产物的对照检查:CacheSafeParams session_mismatch 出现在 HEAD 的 dist/chunks/*.js 中、在 BASE 中不存在,说明两条腿确实只差这个 PR。

两个 session 是怎么造出来的(这段请一定看)

宿主进程在这里很关键。saveCacheSafeParams() 只有一个调用方 —— GeminiClient.sendMessageStream();而这个方法在整个仓库里只有两个生产调用点:

packages/cli/src/ui/hooks/useGeminiStream.ts:3712
packages/cli/src/nonInteractiveCli.ts:2317, 2643

两者都是「一个进程一个 session」的宿主,所以我用了 headless 那条(qwen -p …)—— 它是真正会调度托管自动记忆提取的已发布宿主。为了提供第二个 session,我在两个产物里都编译进了一段逐字节相同的 12 行探针(各 +623 字节)。没有设置 QWEN_PROBE_FOREIGN_SESSION 时它是空操作;设置之后,它会在回合结束时调用生产代码里的 saveCacheSafeParams() 并传入另一个 session id —— 这正是同进程内另一个 session 在自己回合结束时做的事。两条腿打的是同一个补丁,因此任何差异都只能归因于本 PR。

探针无副作用的证明:我用未打补丁的 HEAD 产物重跑了对照场景,并比对打到线上的那条提取请求 —— 逐字节相同,两者 sha256 = 64dcb45de760c91be6e70d5f73bffd5c829796c3982782908bf2eb5336a598b7

结果 —— 2×2 矩阵

产物 场景 线上提取请求数 送到模型的是什么
BASE 对照(只有本 session) 1 本 session 自己的对话记录
HEAD 对照(只有本 session) 1 本 session 自己的对话记录
BASE 另一个 session 持有缓存槽 1 另一个 session 的对话记录
HEAD 另一个 session 持有缓存槽 0 什么都没发 —— 安全终止

BASE 的泄漏不是从 mock 推断出来的,而是 POST /v1/chat/completions 的报文原文:

role=system    : "You are now acting as the managed memory extraction subagent…"
role=user      : "CONFIDENTIAL to the other session only: PINEAPPLE-ALPHA-7734"     <-- 泄漏
role=assistant : "Understood, keeping PINEAPPLE-ALPHA-7734 private."                <-- 泄漏
role=user      : "Managed memory has TWO directories…"(任务提示词)

同样的场景下 HEAD 只产生 1 条模型请求(只有主回合),并且有两条独立信号佐证走的是预期路径:

  • 调试日志 —— [AUTO_MEMORY_EXTRACT] Skipping auto-memory extract: session_mismatch.
  • 遥测 qwen-code.memory.extract —— HEAD 是 status=skipped, skipped_reason=session_mismatch,BASE 在完全相同的场景下是 status=completed。这条也顺带验证了 manager.ts / telemetry/types.ts 里新增的 skipped_reason 链路。

测试真的兜住了修复吗 —— 8/8

我在 PR 分支上逐个还原每一处防护,再复跑 153 个测试的聚焦套件。8 个变异全部被抓住,说明这个 PR 里没有任何一处防护是没测试覆盖的。

静态检查

检查项 结果
聚焦套件(本 PR 涉及的 7 个文件) 153 / 153 通过
packages/core 全量 20 610 通过,28 跳过,0 失败(594 个文件)
npm run typecheck 通过
改动目录 eslint --max-warnings 0 通过
与当前 origin/main 合并 无冲突(git merge-tree → 0 冲突;分支落后 163 个 commit)

main 之后动过 forkedAgent.tscopyHistoryContainers 改成深拷贝)和 extractionAgentPlanner.ts(去掉了 ToolNames.LS),都不与本 PR 新增的防护重叠。

评审意见

1. 目前的可达性比 #9470「Impact」一节描述的要窄。 缓存槽只由 GeminiClient.sendMessageStream() 写入,也就是只由 TUI 和 headless CLI 写 —— 两者都是单 session 进程。多 session 宿主(ACP / daemon 的 Session)跑的是它自己那套基于 GeminiChat 的回合循环,从不写这个槽;它只通过 generatePromptSuggestion ,而那条路 #9233 已经限定过了。所以在今天的 main 上,提取 / 推测这两条跨 session 读取是潜在的而非已经发生的 —— 这也正是我的验证脚手架必须自己造出第二个 session id 的原因。这不构成反对合入的理由:防护每个读取方只加一行,能把这一类缺陷对后续落地的宿主一次性关掉,而且与 #9233 的先例一致。不过 issue / changelog 的措辞值得修正一下,免得把严重程度说过头。

2. 竞态窗口仍然存在,只是失败得比较吵,而不是良性跳过。(源码阅读得出,未实测复现。)runAutoMemoryExtract 里的第二次检查,和 runAutoMemoryExtractionByAgent 内部的硬防护,两者之间不是原子的。如果所有权正好在这个间隙翻转,planner 会抛 no cache-safe params availableMemoryManager.runExtract 会把任务记为 failed 并发出 memory_extract status=failed —— 而不是本 PR 为同一个底层原因引入的良性 skipped / session_mismatch。建议捕获这个特定异常并映射到 session_mismatch 跳过,让两种时序上报同一件事。

3. 「无主」缓存槽现在会硬失败,而以前是可用的。 getSessionMismatchResult()getCacheSafeParamsSessionId() === undefined 当作「不算不匹配」,但 getCacheSafeParams(expectedSessionId) 会拒绝 sessionIdundefined 的槽,于是这种槽会一路走到抛异常。生产代码总是会传 session id,而且新增的 preserves the empty-cache failure path 测试是刻意钉住这个行为的 —— 这里只是把这个尖角标出来,让将来往这个槽里写数据的人心里有数。

复现方式

脚手架、每条腿的机器可读汇总、抓到的线上报文以及变异测试结果都在 pr-assets/9471-verifyharness/evidence/)。

@wenshao
wenshao added this pull request to the merge queue Aug 24, 2026
Merged via the queue into QwenLM:main with commit a7128d3 Aug 24, 2026
43 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(core): process-global cache params cross session boundaries in background readers

4 participants