Skip to content

fix(webui): Make same-session refresh transactional - #8939

Merged
doudouOUC merged 1 commit into
mainfrom
fix/transactional-same-session-refresh
Aug 12, 2026
Merged

fix(webui): Make same-session refresh transactional#8939
doudouOUC merged 1 commit into
mainfrom
fix/transactional-same-session-refresh

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR makes same-logical-session load, configured reload, resume, and explicit client-ID rebind transactional on daemons that advertise client attachment identity. It keeps the current attachment, transcript, event stream, prompt state, metadata, and controls active while a candidate restore runs; validates the candidate epoch, cursor, replay completeness, ownership, and bounded live tail; then commits the replacement atomically. Failed, timed-out, malformed, superseded, or stale candidates are retired without changing the visible source session.

The existing restore coordinator now distinguishes exact replay shapes and serializes ordinary restore RPCs across cross-session and same-session intents. Full loads stage replay plus a contiguous source tail without duplicating replay arrays, while resume/client-ID rebind preserves the visible transcript and invalidates stale pagination ownership. The TypeScript SDK also exposes the existing restore epoch and partial-replay diagnostics needed for fail-closed validation.

Why it's needed

Before this change, refreshing or rebinding the current session used the destructive runner path: it could abort the source SSE, detach the source, clear prompt state, or replace the transcript before the target restore completed. A slow, failed, partial, or stale restore could therefore interrupt an otherwise healthy session and lose live events. Transactional same-session refresh preserves the working source until the replacement is proven complete and current.

Reviewer Test Plan

How to verify

  1. Connect the WebUI to a modern daemon, start a same-session reload, and hold the completed restore response before it reaches the client. While held, send work through the source attachment and confirm its SSE event appears while the connection and transcript remain owned by the source. Release the response and confirm the refreshed replay plus live tail appears exactly once.
  2. Return a structured 504, malformed/partial replay, epoch mismatch, cursor gap, id-less tail frame, or bounded-tail overflow from the candidate. Confirm the public action fails with a recoverable transition error while the source attachment, transcript, prompt, and controls remain usable.
  3. Change only the explicit client ID for the same session and workspace. Confirm the client uses resume, preserves the transcript, commits the requested attachment, invalidates stale pagination work, and can send another prompt. On a daemon that explicitly lacks attachment identity, confirm the existing full-load legacy behavior remains.
  4. Start a prompt, shell command, create, or controlled cross-session transition before requesting refresh. Confirm refresh waits or fails with an explicit terminal transition according to ownership priority, never silently cancelling the other operation.

Evidence (Before & After)

Before: five isolated baseline probes on 86a474ba62a50d25e65deac1a0d5111c4a5bb90d showed same-session load/reload aborting the source event stream, cancelling an admitted prompt, and resume/client-ID rebind detaching or clearing the source before the candidate settled.

After: the same five probes pass, 330 WebUI session tests and 57 SDK session-client tests pass, and the real-daemon JSDOM integration passes all three held-response, structured-504, and client-ID-rebind scenarios. N/A for screenshots because the change is ownership/error-path behavior rather than a visual redesign.

Tested on

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

Environment (optional)

macOS 26.4.1, Node.js 22.22.3, npm 10.9.8, no sandbox for the focused real-daemon integration.

Risk & Scope

  • Main risk or tradeoff: During staging, memory temporarily includes the visible source transcript, the candidate replay, and a bounded source tail of up to 8 MiB. Source and stale-candidate detach are single-attempt best effort, so an invisible attachment may remain until the existing reaper runs if cleanup fails.
  • Not validated / out of scope: The approximately 80 MiB manual memory trace, Windows/Linux local runs, cross-epoch and ring resync, live-journal repair, branch adoption, selective JSONL reading, global attachment scheduling, cleanup registries, and SDK self-heal scheduling are out of scope.
  • Breaking changes / migration notes: None. Daemons that explicitly lack client_identity retain the existing destructive behavior; unknown or malformed modern capability/ownership state fails closed and preserves the source.

Linked Issues

Refs #8678

中文说明

本 PR 做了什么

本 PR 为明确支持客户端 attachment 身份的 daemon,将同逻辑会话的 load、configured reload、resume 和显式 client ID rebind 改为事务化流程。候选 restore 运行期间,当前 attachment、transcript、事件流、prompt 状态、metadata 和控制能力保持可用;候选通过 epoch、cursor、replay 完整性、owner 和有界 live tail 校验后,才原子提交 replacement。失败、超时、malformed、被 supersede 或 stale 的候选只会被回收,不会改变当前可见 source session。

现有 restore coordinator 现在按精确 replay shape 区分请求,并在跨会话和同会话 intent 之间串行化普通 restore RPC。完整 load 会在不复制 replay arrays 的前提下 staging replay 与连续 source tail;resume/client-ID rebind 保留可见 transcript,并使旧 pagination owner 失效。TypeScript SDK 同时暴露现有 restore epoch 与 partial-replay 诊断,供 fail-closed 完整性校验使用。

为什么需要

改动前,刷新或 rebind 当前 session 会使用 destructive runner 路径:target restore 完成前就可能 abort source SSE、detach source、清理 prompt 状态或替换 transcript。慢速、失败、partial 或 stale restore 因此会中断原本健康的 session,并可能丢失 live events。事务化同会话刷新会一直保留可用 source,直到 replacement 被证明完整且仍是当前目标。

Reviewer 测试计划

如何验证

  1. 将 WebUI 连接到现代 daemon,发起 same-session reload,并在完成的 restore response 交给客户端前暂扣它。暂扣期间通过 source attachment 发送工作,确认 source SSE event 仍能显示,connection 和 transcript 仍归 source 所有。释放 response 后,确认 refreshed replay 与 live tail 各出现一次且没有重复。
  2. 让候选返回 structured 504、malformed/partial replay、epoch mismatch、cursor gap、id-less tail frame 或有界 tail overflow。确认公开 action 以 recoverable transition error 失败,同时 source attachment、transcript、prompt 和 controls 仍可用。
  3. 只修改同一 session/workspace 的显式 client ID。确认客户端使用 resume、保留 transcript、提交指定 attachment、使旧 pagination work 失效,并可继续发送 prompt。对于明确缺少 attachment identity 的 daemon,确认仍保留现有 full-load legacy 行为。
  4. 在请求 refresh 前启动 prompt、shell command、create 或 controlled cross-session transition。确认 refresh 根据 owner priority 等待或以明确 terminal transition 失败,绝不会静默取消其他 operation。

证据(前后对比)

改动前:在 86a474ba62a50d25e65deac1a0d5111c4a5bb90d 上的五个隔离 baseline probe 显示,same-session load/reload 会 abort source event stream、取消已 admitted prompt,而 resume/client-ID rebind 会在候选 settle 前 detach 或清理 source。

改动后:相同五个 probe 全部通过,330 个 WebUI session tests 与 57 个 SDK session-client tests 通过,真实 daemon JSDOM integration 的 held-response、structured-504 和 client-ID-rebind 三个场景全部通过。由于本改动是 ownership/error-path 行为而非视觉重设计,截图为 N/A。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS 26.4.1、Node.js 22.22.3、npm 10.9.8;focused real-daemon integration 使用 no sandbox。

风险与范围

  • 主要风险或取舍:staging 期间内存会暂时同时包含可见 source transcript、candidate replay,以及最多 8 MiB 的有界 source tail。source 与 stale candidate detach 均为单次 best-effort;cleanup 失败时,不可见 attachment 可能保留到现有 reaper 回收。
  • 未验证或超出范围:约 80 MiB 的手工内存 trace、Windows/Linux 本地运行、跨 epoch 与 ring resync、live-journal repair、branch adoption、selective JSONL reading、全局 attachment scheduling、cleanup registry 和 SDK self-heal scheduling 均不在本 PR 范围。
  • Breaking changes / 迁移说明:无。明确缺少 client_identity 的 daemon 继续使用现有 destructive 行为;未知或 malformed 的现代 capability/owner 状态会 fail closed 并保留 source。

关联 Issue

Refs #8678

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E test report

Validated the focused real-daemon/JSDOM scenarios on macOS 26.4.1 with Node.js 22.22.3:

QWEN_SANDBOX=false NODE_OPTIONS=--no-experimental-webstorage npx vitest run --root ./integration-tests cli/qwen-serve-webui-same-session-refresh.test.ts --reporter=dot

Result: 3/3 passed in 21.26s.

  • Held a completed same-session load response, sent a prompt through the still-active source attachment, observed its live SSE tail, then released the response and verified replay plus tail committed exactly once.
  • Returned a structured 504 and verified the public error shape while the source session, transcript, client ID, and connection remained intact.
  • Rebound only the client ID through resume, verified the transcript remained visible, then successfully sent another prompt through the replacement attachment.

Artifacts: .integration-tests/1786455089687.

The held-response case prints existing React act(...) warnings because an independent daemon client drives source events outside React; all assertions and the process exit are clean. The approximately 80 MiB manual memory trace remains intentionally pending and is not a CI acceptance threshold.

@doudouOUC
doudouOUC requested a review from wenshao August 11, 2026 16:11
@doudouOUC doudouOUC self-assigned this Aug 11, 2026
@doudouOUC
doudouOUC requested a review from yiliang114 August 11, 2026 16:11
@doudouOUC
doudouOUC marked this pull request as ready for review August 11, 2026 16:12
@doudouOUC
doudouOUC marked this pull request as draft August 11, 2026 16:12
@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 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.

Not explored to full depth (tool budget reached): chunk 9: could not execute packages/webui/src/daemon/session/actions.test.ts — no node_modules in the review worktree or parent checkout, and a full monorepo npm in…; You are review agent reverse-audit — Reverse audit agen...: none — all planned checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; Context: PR #8939 makes same-session WebUI refresh/rebind...: I did not run builds/tests for empirical verification (my dimension is static cross-file tracing). Also did not exhaustively walk all 15k lines of DaemonSession…; Context: PR #8939 makes same-session WebUI refresh/rebind...: none material — I did not execute builds/tests (static trace only) and did not page through the untouched bulk of DaemonSessionProvider.test.tsx beyond the di…, and 13 more.

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

中文说明

未探索到全部深度(达到工具调用预算):chunk 9:could not execute packages/webui/src/daemon/session/actions.test.ts — no node_modules in the review worktree or parent checkout, and a full monorepo npm in…;You are review agent reverse-audit — Reverse audit agen...:none — all planned checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;Context: PR #8939 makes same-session WebUI refresh/rebind...:I did not run builds/tests for empirical verification (my dimension is static cross-file tracing). Also did not exhaustively walk all 15k lines of DaemonSession…;Context: PR #8939 makes same-session WebUI refresh/rebind...:none material — I did not execute builds/tests (static trace only) and did not page through the untouched bulk of DaemonSessionProvider.test.tsx beyond the di…,另有 13 条。

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

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

Comment on lines +1854 to +1855
activePromptsRef.current.has(activeSession.sessionId) ||
activePromptsRef.current.has(`${activeSession.sessionId}:shell`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-1: The shell-aware hasSessionActivePrompt() folds the ${sessionId}:shell entry into the runnerActiveTurn latch at the turn-terminal re-evaluation, and nothing re-evaluates the latch when the shell command finishes afterwards (sendShellCommand's finally only deletes the map entry and sets the prompt status; user_shell_result is not a terminal event). snapshot().activeTurn stays stale-true on an idle session, and any same-session refresh parks in the queued branch — which arms no watchdog — forever. Probe-reproduced on the unmodified PR: shell in flight → turn_complete → shell resolves → loadSession hangs in queued with the restore RPC never issued; re-evaluating the turn state without the shell entry flips it to a committed refresh while the existing wait-for-shell test still passes.

Failure scenario: user runs a shell command during a turn → both finish → reloadSession() never settles and the UI shows a pending transition indefinitely until a page reload, a prop-driven session change, or another turn's terminal event.

Suggested fix (make the state self-correcting):

// in snapshot():
activeTurn:
  runnerActiveTurn ||
  hasCurrentSessionActivePromptRef.current(),

or re-evaluate runnerActiveTurn and queue a transition pump from the shell-completion path.

中文说明

shell 感知的 hasSessionActivePrompt() 会在 turn 终止事件的重新评估中把 ${sessionId}:shell 条目折叠进 runnerActiveTurn 锁存值,而 shell 命令随后结束时没有任何路径重新评估该锁存值(sendShellCommandfinally 只删除 map 条目并设置 prompt 状态;user_shell_result 不是终止事件)。空闲会话上 snapshot().activeTurn 会一直保持过期的 true,任何同会话刷新都会停在不设置 watchdog 的 queued 分支,永远无法推进。已在未修改的 PR 上用探针复现:shell 执行中 → turn_complete → shell 结束 → loadSession 卡在 queued,restore RPC 始终未发出;把 shell 条目从终止重估中去掉后探针变为成功提交,且现有的等待 shell 测试仍然通过。

失败场景:用户在一个 turn 中执行 shell 命令 → 两者都结束 → reloadSession() 永不 settle,UI 无限期显示进行中的转场,直到页面重载、prop 驱动的会话变更或另一个 turn 的终止事件。

建议修复(让状态可自我纠正):在 snapshot() 中改为 activeTurn: runnerActiveTurn || hasCurrentSessionActivePromptRef.current()(实时读取,shell 条目删除后自动失效),或在 shell 完成路径重新评估 runnerActiveTurn 并触发一次 transition pump。

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

Comment on lines +3453 to +3455
intent.timeout = setTimeout(() => {
if (intent.candidate) {
retireAttachment(intent.candidate, intent);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-2: A settle path retires neither the staged candidate nor the armed capture. The pump-top deadline check (~3782-3787; the environment check at ~3771-3781 has the same hole) settles the intent via exposeCrossSessionFailure, which does not retire intent.candidate and does not clear runnerControlRef.current.capture — and settleCrossSessionIntent then cancels this watchdog, the only timeout path that retires the candidate first. All other settle sites added in this diff retire the candidate before settling; this shared failure function is the exception. Newly introduced by this diff: pre-PR, intent.candidate did not exist.

Failure scenario: the deadline (default 75s) expires while the staged candidate waits for the source tail to drain — reliably so in throttled/background tabs, where the next event-driven pump lands before the watchdog macrotask → the daemon attachment leaks until the reaper, and the still-armed capture serializes every subsequent live event into an orphan buffer (up to 1024 events / 8 MiB).

Suggested fix — make exposeCrossSessionFailure the single cleanup sink: before settling, retire intent.candidate and clear runnerControlRef.current.capture when it equals intent.capture, mirroring this watchdog callback body.

中文说明

存在一条 settle 路径既不回收已暂存的 candidate、也不清除已挂载的 capture。pump 顶部的 deadline 检查(约 3782-3787 行;约 3771-3781 行的 environment 检查有同样的漏洞)通过 exposeCrossSessionFailure 结束 intent,但该函数既不回收 intent.candidate,也不清除 runnerControlRef.current.capture —— 随后 settleCrossSessionIntent 取消了这个 watchdog(唯一会先回收 candidate 的超时路径)。本 diff 新增的所有其他 settle 点都会先回收 candidate,只有这个共享失败函数例外。这是本 diff 新引入的问题:PR 之前不存在 intent.candidate

失败场景:默认 75s 的 deadline 在 staged candidate 等待 source tail 排空期间到期(在被节流/后台的标签页中必然发生,因为事件驱动的 pump 会先于 watchdog 宏任务到达)→ daemon 端 attachment 泄漏直到 reaper 回收,且仍然挂载的 capture 会把之后每个 live 事件序列化进一个无人消费的孤儿缓冲区(最多 1024 个事件 / 8 MiB)。

建议修复:让 exposeCrossSessionFailure 成为统一的清理出口 —— 在 settle 之前回收 intent.candidate,并在 runnerControlRef.current.capture === intent.capture 时清除它,与本 watchdog 回调体保持一致。

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

Comment on lines +3962 to +3964
if (
intent.sameLogical &&
(candidate.eventEpoch === undefined ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-12: The integrity check hard-requires candidate.eventEpoch, but the daemon's bounded-refresh load path (refreshedReplayFieldsFor in packages/acp-bridge/src/bridge.ts, taken whenever the load carries historyPageSize — which the Web Shell always sends, default 100) returns no eventEpoch in its idle-session success branch. Probe-verified: refreshed.eventEpoch is undefined while loaded.eventEpoch is a string, and adding the epoch to that branch flips the probe to pass. A busy-session refresh succeeds because it falls back to replayFieldsFor, which carries the epoch. Neither test layer catches this: the unit mocks always supply eventEpoch: 'epoch-1', and no integration test sets historyPageSize.

Failure scenario: refreshing an idle session under the default Web Shell configuration fails with 'Session refresh returned an incomplete snapshot' on every retry — the headline feature of this PR regresses on its happy path.

Suggested fix (packages/acp-bridge/src/bridge.ts, refreshedReplayFieldsFor success branch): add eventEpoch: entry.events.epoch (mirroring replayFieldsFor, which includes it in every branch), plus a regression test for bounded-refresh loads carrying eventEpoch.

中文说明

完整性检查硬性要求 candidate.eventEpoch,但 daemon 的有界刷新 load 路径(packages/acp-bridge/src/bridge.ts 中的 refreshedReplayFieldsFor,凡是 load 携带 historyPageSize 就会走该路径 —— Web Shell 总是会发送,默认 100)在其空闲会话成功分支中不返回 eventEpoch。已用探针验证:refreshed.eventEpochundefinedloaded.eventEpoch 是字符串;在该分支补上 epoch 后探针即通过。忙碌会话的刷新能成功,是因为回落到 replayFieldsFor(其每个分支都携带 epoch)。两层测试都抓不到该问题:单测 mock 总是提供 eventEpoch: 'epoch-1',集成测试没有任何一处设置 historyPageSize

失败场景:在默认 Web Shell 配置下刷新空闲会话,每次都会以 'Session refresh returned an incomplete snapshot' 失败 —— 本 PR 的核心特性在其正常路径(happy path)上发生回归。

建议修复:在 packages/acp-bridge/src/bridge.tsrefreshedReplayFieldsFor 成功分支中加入 eventEpoch: entry.events.epoch(与 replayFieldsFor 保持一致),并补充针对有界刷新 load 携带 eventEpoch 的回归测试。

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

Comment on lines +3845 to +3849
retireAttachment(candidate, intent);
exposeCrossSessionFailure(
intent,
new Error('Session refresh failed integrity validation'),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-19: The three settle sites in this candidate branch (capture-invalid/attachment-change at ~3803-3814, deadline at ~3817-3823, and this commit-integrity failure) retire the candidate but never clear runnerControlRef.current.capture. The capture is installed at staging and deliberately preserved by the RPC .finally hand-off; settleCrossSessionIntent/exposeCrossSessionFailure don't clear it, and the settle's clearTimeout cancels the watchdog that would have. Same leak class as the pump-top settle path, but these triggers need no deadline: staged.repair or a daemon.replay_event_malformed notice fires the integrity site directly, and an event-driven pump commonly lands before the watchdog macrotask.

Failure scenario: after any of these recoverable refresh failures, captureSourceEvent keeps JSON-serializing and buffering every live event (≤1024 events / 8 MiB) into a buffer nobody consumes — pinned until the next refresh staging, session switch, or unmount — plus per-event serialization overhead on the live session.

Suggested fix — at each site, or centrally in exposeCrossSessionFailure, mirror the guarded clear used by cancelCrossSessionTransition:

const control = runnerControlRef.current;
if (control && intent.capture && control.capture === intent.capture) {
  control.capture = undefined;
}
中文说明

该 candidate 分支中的三个 settle 点(约 3803-3814 行的 capture 失效/attachment 变更、约 3817-3823 行的 deadline,以及此处的 commit 完整性失败)都会回收 candidate,但从不清除 runnerControlRef.current.capture。capture 在 staging 时挂载,并被 RPC 的 .finally 交接逻辑有意保留;settleCrossSessionIntent/exposeCrossSessionFailure 都不清除它,而 settle 的 clearTimeout 又会取消本应清除它的 watchdog。与 pump 顶部 settle 路径属于同一泄漏类别,但这些触发不需要 deadline:staged.repairdaemon.replay_event_malformed 通知会直接触发完整性失败点,且事件驱动的 pump 通常先于 watchdog 宏任务到达。

失败场景:上述任一可恢复的刷新失败之后,captureSourceEvent 会继续把每个 live 事件 JSON 序列化并缓冲(≤1024 个事件 / 8 MiB)进一个无人消费的缓冲区 —— 直到下一次 refresh staging、会话切换或卸载才释放 —— 并给 live 会话带来逐事件的序列化开销。

建议修复:在每个失败点(或集中在 exposeCrossSessionFailure 中)仿照 cancelCrossSessionTransition 的带保护清除逻辑清除 capture。

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

Comment on lines +474 to +476
if (
sourceBoundOperationInFlight &&
(current === undefined ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R1-20: A controlled (prop-driven) session switch rejected by this early-reject is dropped permanently. The provider's controlled effect stamps lastHandledSessionIdRef/lastHandledClientIdRef before the async outcome and swallows the rejection (void request.catch(console.warn)), so nothing retries and no notice or transition is published. The source-bound window during createSession (trackCreate) is new in this diff; pre-PR this interleaving took the legacy path and the request was processed. Probe-flipped both arms: on the PR tree, a prop set during the create window produces no load attempt and no published transition failure, and after the create commits the provider permanently shows the created session while the prop says session B; deleting the early-reject makes the same probe handle the request via the legacy path.

Failure scenario: the Web Shell first-prompt bootstrap opens the up-to-30s source-bound create window; the host sets the sessionId prop inside it (deep-link/back navigation) → silent drop; after the create commits, a permanent created-session-vs-prop divergence with zero user-visible feedback.

Suggested fix: don't stamp the lastHandled* refs until the request settles (or clear them on rejection so the effect can re-fire), or retry once on the source-bound InvalidStateError; additionally route the legacy-path rejection through addNotice for parity with rejectPreflight.

中文说明

被这个 early-reject 拒绝的受控(prop 驱动)会话切换会被永久丢弃。provider 的受控 effect 在异步结果返回之前就记录了 lastHandledSessionIdRef/lastHandledClientIdRef,并且用 void request.catch(console.warn) 吞掉拒绝 —— 因此没有任何重试,也不会发布任何通知或转场状态。createSessiontrackCreate)引入的 source-bound 窗口是本 diff 新增的;PR 之前同样的时序会走 legacy 路径,请求会被正常处理。探针双向往返验证:在 PR 代码上,create 窗口内设置 prop 不会产生任何 load 调用、也不发布转场失败,create 提交后 provider 永久显示新建的会话而 prop 指向会话 B;删除该 early-reject 后同一探针会通过 legacy 路径正常处理请求。

失败场景:Web Shell 首个 prompt 的引导流程会打开最长 30s 的 source-bound create 窗口;宿主在此期间设置 sessionId prop(深链/返回导航)→ 切换被静默丢弃;create 提交后,provider 状态与 prop 永久不一致,且用户看不到任何反馈。

建议修复:在请求 settle 之前不要记录 lastHandled* ref(或在被拒绝时清除它们以便 effect 可以重新触发),或对 source-bound 的 InvalidStateError 重试一次;同时把 legacy 路径的拒绝也通过 addNotice 发布,与 rejectPreflight 保持一致。

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

Comment on lines +14864 to +14866
it.each([
['epoch mismatch', { eventEpoch: 'epoch-2' }],
['incomplete replay', { replaySnapshotComplete: false }],

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-17: The same-session integrity gate applies its epoch/partial/error/degraded clauses to both load and resume intents, but all five rejection rows run via loadSession; the only same-session resumeSession test fails at the RPC level, and the cross-session resume tests skip the gate (sameLogical: false) — so resume-mode integrity has zero coverage.

Concrete cost: a refactor dropping or misplacing the intent.mode === 'load' guard (or splitting the check and dropping a mode-agnostic clause — epoch, replayPartial, replayError, replayDegraded — from the resume side) would make healthy same-session resumes reject with 'Session refresh returned an incomplete snapshot', breaking the transactional resume this PR introduces, while the suite stays green.

Suggested fix: add a resume-mode case — resumeSession('session-a') with a candidate carrying eventEpoch: 'epoch-2' (or replayPartial: true), asserting the rejection message, the intact source transcript, and the candidate detached.

中文说明

同会话完整性门对 load 与 resume intent 都应用 epoch/partial/error/degraded 条款,但五个拒绝用例全部通过 loadSession 运行;唯一的同会话 resumeSession 测试在 RPC 层就失败了,跨会话 resume 测试则跳过该门(sameLogical: false)—— 因此 resume 模式的完整性路径零覆盖。

具体代价:如果重构丢弃或错放了 intent.mode === 'load' 守卫(或拆分检查并把模式无关条款 —— epoch、replayPartialreplayErrorreplayDegraded —— 从 resume 一侧丢掉),健康的同会话 resume 会被 'Session refresh returned an incomplete snapshot' 拒绝,破坏本 PR 引入的事务化 resume,而测试套件保持绿色。

建议修复:新增 resume 模式用例 —— 用携带 eventEpoch: 'epoch-2'(或 replayPartial: true)的 candidate 调用 resumeSession('session-a'),断言拒绝消息、source transcript 完整以及 candidate 被 detach。

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

Comment on lines +147 to +149
readonly replaySnapshotComplete: boolean;
readonly replayPartial: boolean;
readonly replayError: string | undefined;

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-23: The new replayError diagnostic is read only as a boolean (candidate.replayError !== undefined) at its sole consumer (the provider's same-session candidate gate); the string it exists to carry is discarded, and the refresh failure publishes the hardcoded 'Session refresh returned an incomplete snapshot'. The same provider file's transcript-pagination path folds this exact field into its error (page.replayError ?? 'Earlier session history was only partially read') — the author surfaces the field elsewhere; the refresh path drops it.

Concrete cost: when persisted-journal reconstruction fails during load — the exact case partial/replayError were added for — the user/operator sees a generic failure with no way to tell a transient transcript-page read error from real corruption without daemon logs.

Suggested fix: at the gate's exposure site, fold in the available detail — append ': ' + candidate.replayError when present, or ' (partial replay)' / ' (degraded replay)' markers otherwise.

中文说明

新增的 replayError 诊断信息在其唯一消费点(provider 的同会话 candidate 门)只被当作布尔值读取(candidate.replayError !== undefined);它本应携带的字符串被丢弃,刷新失败对外发布的是硬编码的 'Session refresh returned an incomplete snapshot'。同一个 provider 文件的 transcript 分页路径却把同一字段拼进了错误消息(page.replayError ?? 'Earlier session history was only partially read')—— 作者在别处展示了该字段,刷新路径却丢掉了它。

具体代价:当 load 期间持久化 journal 重建失败 —— 正是 partial/replayError 被加入的场景 —— 用户/运维只能看到泛化失败,不查 daemon 日志无法区分瞬时的 transcript 分页读取错误和真正的数据损坏。

建议修复:在门对外暴露错误处拼入可用的细节,例如把 candidate.replayError(或 '(partial replay)' 等标记)附加到错误消息中。

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

Comment on lines +3410 to +3411
? `Could not refresh session ${target.sessionId}. The current attachment is still active.`
: `Could not open session ${target.sessionId}. The current session is still 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-26: Every same-session refresh failure surfaces as 'The current attachment is still active' with recoverable: true, regardless of the cause carried in debugMessagepublishCrossSessionFailure is the single notice sink for transition timeout, integrity-validation failure, incomplete snapshot, invalid owner identity, attachment change, and capture invalidation alike.

Concrete cost: on an idle session whose refresh failed integrity validation (e.g. the incomplete-snapshot path above), the notice asserts a busy-attachment condition that does not exist and invites retries that keep failing deterministically, with no hint of the real cause.

Suggested fix: keep the 'still active' wording only for activity-class rejections; for transition-time failures use a cause-neutral same-session message or derive it from the error class.

中文说明

无论 debugMessage 携带的真实原因是什么,所有同会话刷新失败都对外显示 'The current attachment is still active' 且 recoverable: true —— publishCrossSessionFailure 是转场超时、完整性校验失败、快照不完整、owner 身份无效、attachment 变更、capture 失效等所有失败的统一通知出口。

具体代价:在一个空闲会话上,如果刷新因完整性校验失败(例如上面的快照不完整路径),通知却声称存在并不存在的 attachment 忙碌状态,并诱导用户不断重试一个必然继续失败的请求,且看不到任何真实原因的提示。

建议修复:'still active' 措辞只保留给活动类拒绝;对转场期失败使用原因中性的同会话消息,或根据错误类别生成消息。

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

Comment on lines +923 to +927
() => {
rawCreateSettled = true;
setSourceBoundOperationInFlight(false);
},
);

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-27: This rejection handler — the only code that releases the source-bound flag when a raw create request fails — is exercised by zero tests: the two new timeout tests only ever resolve the deferred create, and no test rejects createOrAttachSession/createDetachedSession. Mutation-verified: deleting setSourceBoundOperationInFlight(false) here keeps all 330 tests green; a probe rejecting the deferred create flips ([[true]] stuck under the mutation vs [[true],[false]] on the PR code).

Concrete cost: a future edit breaking this branch leaves sourceBoundOperationCountRef stuck at 1 after a routine failed create (daemon 5xx / network error) — every requireStableSession() action and every load/resume/reload throws 'Another session operation is already in progress' until the provider remounts.

Suggested fix: add a test mirroring the timeout tests but rejecting the deferred create — assert the flag sequence [[true],[false]], no retire/detach issued, and a follow-up loadSession/createSession not blocked; ideally also a reject-before-timeout case.

中文说明

这个拒绝处理器 —— 原始 create 请求失败时唯一释放 source-bound 标志的代码 —— 零测试覆盖:两个新的超时测试只会 resolve 延迟的 create,没有任何测试 reject createOrAttachSession/createDetachedSession。已通过变异验证:删除此处的 setSourceBoundOperationInFlight(false) 后全部 330 个测试保持绿色;用 reject 延迟 create 的探针则发生翻转(变异下标志停在 [[true]],PR 代码下为 [[true],[false]])。

具体代价:未来破坏该分支的改动会让一次普通的 create 失败(daemon 5xx / 网络错误)后 sourceBoundOperationCountRef 永久停在 1 —— 每个 requireStableSession() 动作和每次 load/resume/reload 都会抛出 'Another session operation is already in progress',直到 provider 重新挂载。

建议修复:仿照超时测试新增一个 reject 延迟 create 的测试 —— 断言标志序列 [[true],[false]]、不触发 retire/detach、后续 loadSession/createSession 不被阻塞;最好再加一个超时前 reject 的用例。

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

Comment on lines +15591 to 15593
replayError: opts.replayError,
eventEpoch: opts.eventEpoch ?? 'epoch-1',
lastEventId: opts.lastEventId,

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-28: opts.eventEpoch ?? 'epoch-1' silently coerces an explicit eventEpoch: undefined, so the suite cannot construct an epoch-less session through the standard factory spelling (the spelling compiles — the project does not enable exactOptionalPropertyTypes) — precisely the session class whose refresh regression is confirmed above (virtual/epoch-less sessions). Today's only epoch overrides are 'epoch-1'/'epoch-2'; no test can reach the epoch-less gate behavior.

Concrete cost: when the epoch-less refresh regression is fixed, the natural regression test createMockSession({ eventEpoch: undefined }) silently exercises the epoch-having happy path, stays green, and blesses the regression it was written to catch.

Suggested change
replayError: opts.replayError,
eventEpoch: opts.eventEpoch ?? 'epoch-1',
lastEventId: opts.lastEventId,
replayError: opts.replayError,
eventEpoch: Object.hasOwn(opts, 'eventEpoch') ? opts.eventEpoch : 'epoch-1',
lastEventId: opts.lastEventId,
中文说明

opts.eventEpoch ?? 'epoch-1' 会把显式传入的 eventEpoch: undefined 静默强制转换为默认值,因此测试套件无法通过标准工厂写法构造无 epoch 的会话(该写法可以编译 —— 项目未启用 exactOptionalPropertyTypes)—— 而这正是上面已确认存在刷新回归的会话类别(虚拟/无 epoch 会话)。目前仅有的 epoch 覆盖值是 'epoch-1'/'epoch-2';没有任何测试能触达无 epoch 的门行为。

具体代价:当无 epoch 刷新回归被修复时,最自然的回归测试 createMockSession({ eventEpoch: undefined }) 会静默走到有 epoch 的正常路径并保持绿色,从而认可了它本应捕获的回归。

建议修复:区分'未提供'与'显式 undefined',例如 eventEpoch: Object.hasOwn(opts, 'eventEpoch') ? opts.eventEpoch : 'epoch-1'

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

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the fix/transactional-same-session-refresh branch from 1e03e90 to 59fb669 Compare August 12, 2026 02:27
@doudouOUC
doudouOUC changed the base branch from fix/restore-request-shape-coalescing to main August 12, 2026 02:27
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@doudouOUC
doudouOUC marked this pull request as ready for review August 12, 2026 03:08
@doudouOUC
doudouOUC enabled auto-merge August 12, 2026 03:08
@doudouOUC
doudouOUC requested a review from ytahdn August 12, 2026 03:08
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run note: re-gated the same head (59fb669) after the maintainer approval landed. Gate outcome is unchanged; the verdict movement is in the Stage 2/3 comments.

Thanks for the PR — this is the next slice of the #8678 session-restore safety series (#8691 and #8833 already merged).

  • Template: complete ✓ — all sections present, bilingual, and the reviewer test plan is concrete.
  • Problem: observed, not theoretical. Linked P1 issue fix(serve): Preserve the current session when a large restore times out #8678 documents same-session refresh/reload/resume tearing down the live source before the target restore settles, and the PR body shows five baseline probes on 86a474b reproducing the destructive behavior (source SSE aborted, admitted prompt cancelled, source detached early).
  • Direction: aligned. This is the planned transactional-refresh slice of the tracking issue. The claude-code CHANGELOG has no direct analog for transactional same-session refresh, but session-restore robustness is clearly a live theme across agents, and our own P1 already settled the direction.
  • Size: touches core infrastructure under the cross-package clause (packages/sdk-typescript + packages/webui): 1,445 production logic lines (SDK 31, webui provider/actions 1,414) vs 1,851 test lines and 47 docs lines — above both the 500-line maintainer-awareness bar and the 1,000-line large-PR advisory. Naming it directly: the title says fix, but the diff is a structural rework of the session-transition state machine inside a ~5.4k-line provider. That depth is what the escalation is about; it is not a block by itself.
  • Approach: the direction is right and it reuses the existing coordinator machinery (retireAttachment, resolveSessionRestoreTimeouts, stageCrossSession) rather than adding parallel plumbing; fail-closed validation (epoch, cursor, replay completeness, ownership) plus legacy capability gating matches the design doc. I don't see a materially simpler path — capture-and-merge of the live tail is intrinsic if events between the candidate watermark and commit must not be lost.
  • Risk: no elevated revert-history path signals. The risk here is ordinary complexity risk, concentrated in one very large file.

Flagging size/depth for maintainer awareness, and moving on to code review. 🔍

中文说明

Re-run 说明:在维护者批准之后,对同一个头(59fb669)重新执行了门禁。门禁结论不变;结论变化见 Stage 2/3 评论。

感谢贡献——这是 #8678 会话恢复安全系列的下一个切片(#8691#8833 已合并)。

  • 模板:完整 ✓——各节齐全,双语,reviewer 测试计划具体可执行。
  • 问题:已观测到,不是理论问题。关联的 P1 issue fix(serve): Preserve the current session when a large restore times out #8678 记录了同会话 refresh/reload/resume 在目标 restore 落定前就拆掉活跃 source 的问题;PR 正文给出了在 86a474b 上的五个基线探针复现。
  • 方向:对齐。这是跟踪 issue 中规划好的事务化刷新切片。
  • 规模:按跨包条款属于核心基础设施(packages/sdk-typescript + packages/webui):1445 行生产逻辑(SDK 31 行,webui provider/actions 1414 行),另有 1851 行测试 与 47 行文档——同时超过 500 行维护者关注线与 1000 行大 PR 提示线。直说:标题是 fix,但 diff 是对约 5.4 千行 provider 中会话转换状态机的结构性重构。升级评审针对的是这种深度,本身不是阻断项。
  • 方案:方向正确,复用了现有协调器机制(retireAttachmentresolveSessionRestoreTimeoutsstageCrossSession),没有另起炉灶;fail-closed 校验加 legacy 能力门控,与设计文档一致。没有看到明显更简的路径。
  • 风险:无 revert 历史高风险路径信号。风险是常规复杂度风险,集中在一个超大文件里。

规模/深度已提请维护者关注,继续进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 59fb669232979d92dad451f5b55ff39297d2a5d1 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot added daemon scope/session-management Session state and persistence labels Aug 12, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review (re-run)

The design doc and the implementation still line up on this head, and I can corroborate the commit-path description in @yiliang114's approval: commitSameSession is a synchronous double-checked validation (two runner snapshots, no await between them), the tail applied at commit is strictly (watermark, processedEventId], the candidate cursor is advanced to the source's final processed cursor before the new runner starts, and every failure path retires the candidate and preserves the source.

What this re-run adds: I independently re-checked the six Critical findings from the earlier multi-agent review pass (raised on the pre-force-push head, in the standing CHANGES_REQUESTED review) against this head's code. All six are still present — the force-push did not address them. Verified statically, line numbers at this head:

  1. Idle-session refresh fails in the default Web Shell configuration (epoch gap). The PR's integrity gate hard-requires candidate.eventEpoch (DaemonSessionProvider.tsx:3966), but the daemon's bounded-refresh load path — refreshedReplayFieldsFor in packages/acp-bridge/src/bridge.ts, taken whenever the load carries historyPageSize, which Web Shell always sends (default WEB_SHELL_HISTORY_PAGE_SIZE = 100) — returns no eventEpoch in its idle-session success branch (bridge.ts:5442-5453 on current main), while replayFieldsFor includes the epoch in every branch (bridge.ts:5279-5313, with a comment explaining why the epoch must travel with the cursor). Net effect: refreshing an idle session fails with "Session refresh returned an incomplete snapshot" on every retry in the production Web Shell configuration — the headline feature regresses on its happy path. Neither test layer can see it: the unit mocks always supply eventEpoch, and this PR's real-daemon integration test never sets historyPageSize (verified). The PR doesn't touch acp-bridge, so the gap stands on current main too. The review pass's suggested fix stands: add eventEpoch: entry.events.epoch to that branch plus a bounded-refresh regression test.
  2. Shell × turn latch hang. snapshot().activeTurn reads the latched runnerActiveTurn (:1205), re-evaluated on turn-terminal events with the :shell entry folded in (:2728). Shell completion (sendShellCommand finally in actions.ts) deletes the map entry and sets the prompt status but never re-evaluates the latch. Sequence: shell in flight during a turn → turn_complete → shell resolves → the latch stays stale-true → any same-session refresh parks in the queued branch (:3877, which arms no watchdog) until the next turn terminal event or a page reload.
  3. Settle paths leak the staged candidate and the armed capture. exposeCrossSessionFailure (:3421-3429) retires neither intent.candidate nor clears runnerControlRef.capture. The settle sites that route through it: the pump-top environment/deadline checks (:3771-3790), and the three candidate-branch sites (:3809, :3818, :3845) retire the candidate but leave the capture armed. The watchdog callback does clean up — but settleCrossSessionIntent cancels that very watchdog. Result: orphaned capture buffer serializing subsequent live events (up to 1024 events / 8 MiB) plus a daemon attachment lingering until the reaper.
  4. Same leak class, no deadline needed. staged.repair or a daemon.replay_event_malformed notice reaches the integrity site directly; an event-driven pump commonly lands before the watchdog macrotask, so the leak triggers on ordinary recoverable failures, not just timeouts.
  5. Prop-driven switches dropped during the create window. The actions.ts:475 early-reject plus the controlled effect stamping lastHandled* refs before the async outcome (:4671-4673) and swallowing the rejection (:4736) means a sessionId prop set during the source-bound createSession window produces no load attempt, no notice, no retry — a permanent prop-vs-provider divergence after the create commits.
  6. Undefined-watermark sessions can't refresh until their first id-bearing event (seeded without a ?? 0 fallback at :1190 and :1828). @yiliang114's approval accepts this as a tolerable rough edge for "brand-new zero-event sessions"; noting the scope is broader — any pure re-attach session before its first event, which is the ordinary new-chat bootstrap.

None of the six has regression-test coverage. Findings 2-6 were probe-reproduced by the review pass on the pre-force-push head; this re-run confirms the relevant code is unchanged on the current head, and finding 1 is additionally verified against current main's daemon code — it is not a judgment call, the bounded-refresh branch simply doesn't copy the epoch.

To be explicit about where the approval and this review diverge: the maintainer's verification of the commit CAS, exactly-once tail merge, fail-closed gates, and scheduling is accurate as far as it goes — these findings sit outside the commit path, in the daemon response shape, the shell latch, the settle cleanup, and the controlled bootstrap, and none of them is observable from the commit path itself.

sequenceDiagram
    participant P1 as WebUI action
    participant P2 as Transition coordinator
    participant P3 as Source runner
    participant P4 as Daemon
    participant P5 as Candidate session
    P1->>P2: request same-session refresh
    P2->>P3: wait until ready and idle
    P2->>P3: arm bounded event capture
    P2->>P4: load or resume with client id
    P4-->>P2: candidate with epoch and watermark
    P2->>P2: validate epoch, cursor, replay completeness
    P3-->>P2: source processed cursor catches up
    P2->>P5: stage replay plus bounded tail
    P2->>P5: commit, stop source, detach best effort
Loading
Files changed (9)
File What changed
docs/design/2026-08-11-transactional-same-session-refresh.md New design doc covering scope, scheduling, cursor integrity, staging/commit, and failure behavior
integration-tests/cli/qwen-serve-webui-same-session-refresh.test.ts New real-daemon integration test: held-response tail merge, structured timeout source preservation, client-ID rebind transcript continuity — never sets historyPageSize, so the bounded-refresh daemon path is untested
packages/sdk-typescript/src/daemon/DaemonSessionClient.ts Surfaces replaySnapshotComplete / replayPartial / replayError from load responses and adds an eventEpoch getter
packages/sdk-typescript/src/daemon/types.ts Adds optional partial and replayError fields to the restored-session type
packages/sdk-typescript/test/unit/DaemonSessionClient.test.ts Unit tests for the new replay-integrity surfacing (mocks always supply eventEpoch)
packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx 14 new scenario tests pinning the transactional same-session paths
packages/webui/src/daemon/session/DaemonSessionProvider.tsx The core change: runner snapshot/capture, candidate validation, commitSameSession, deadline and abort handling, client-ID rebind routing
packages/webui/src/daemon/session/actions.test.ts Tests for create tracking, restore blocking, and bootstrap-path behavior
packages/webui/src/daemon/session/actions.ts Routes same-session switches through the coordinator, tracks raw create lifetime, retires late create results

Testing evidence (PR's own CI)

This unattended run does not execute PR code. Check-runs fetched once on the head commit, not polled:

The main Qwen Code CI workflow has never run on this branch — on any head of this PR. Verified through the workflow-runs API filtered by branch (zero runs) and independently through the commit's check-suites: there are no build / lint / typecheck / unit / integration check-runs on either the current or the pre-force-push head. What did run on this head: Live Host (macos-latest) ✅, the bot orchestration jobs ✅, one label run cancelled, review-pr still in progress; the other 48 checks on the commit are skipped bot-gate jobs. So "no failures" is true as far as it goes, but the repo's main suite has never executed this code. The approval's "42 checks green" does not match the main-CI record for this branch — flagging respectfully so @yiliang114 can double-check which view that read. Why the pull_request trigger never fired here may itself deserve a look.

Checks on 59fb669 (fetched once; skipped bot-gate checks omitted):

Check Conclusion
label ❌ cancelled
Live Host (macos-latest) ✅ success
route / triage / authorize / Remind on force-push ✅ success
review-pr ⏳ in progress
Qwen Code CI (build / lint / typecheck / unit / integration) ⚠️ never triggered on this branch

Sandboxed verification: a @qwen-code /verify run is already in flight as part of this triage run — its report will be posted in this thread when it completes, and will speak to the runtime fail-closed claims. Note that finding 1 above is a static wire-shape gap in current main's daemon code; a verification harness that doesn't send historyPageSize would share the existing tests' blind spot, so read the report against these findings. Real-scenario (tmux) testing is N/A for this unattended run.

中文说明

代码审查(re-run)

设计文档与实现在该头上仍然一致;@yiliang114 批准中对提交路径的描述我可以印证:commitSameSession 是同步双重校验(两次 runner 快照、中间无 await),提交时应用的 tail 严格为 (watermark, processedEventId],候选 cursor 在新 runner 启动前推进到 source 的最终 processed cursor,所有失败路径都回收候选并保留 source。

本次 re-run 的增量:独立复核了早前多代理评审(在强推前的头上提出、即仍挂着的 CHANGES_REQUESTED 评审)中的六项 Critical 发现——六项在当前头上全部仍然存在,强推并未处理它们。静态核实,行号为当前头:

  1. 默认 Web Shell 配置下空闲会话刷新必然失败(epoch 缺口):PR 的完整性门硬性要求 candidate.eventEpochDaemonSessionProvider.tsx:3966),但 daemon 的有界刷新 load 路径——packages/acp-bridge/src/bridge.tsrefreshedReplayFieldsFor,凡 load 携带 historyPageSize 必走(Web Shell 总是发送,默认 WEB_SHELL_HISTORY_PAGE_SIZE = 100)——其空闲会话成功分支不返回 eventEpoch(当前 mainbridge.ts:5442-5453),而 replayFieldsFor 每个分支都携带 epoch。结果:生产 Web Shell 配置下刷新空闲会话每次都以 "Session refresh returned an incomplete snapshot" 失败——核心特性在正常路径上回归。两层测试都看不到:单测 mock 总是提供 eventEpoch,本 PR 的真实 daemon 集成测试从未设置 historyPageSize(已核实)。PR 未触碰 acp-bridge,该缺口在当前 main 上同样存在。
  2. shell × turn 锁存挂起snapshot().activeTurn 读取锁存值 runnerActiveTurn:1205),turn 终止事件重估时把 :shell 条目折叠在内(:2728);shell 完成路径只删 map 条目并设置 prompt 状态,从不重估锁存。时序:turn 中 shell 执行 → turn_complete → shell 结束 → 锁存过期为 true → 同会话刷新停在不设 watchdog 的 queued 分支(:3877),直到下一个 turn 终止事件或页面重载。
  3. settle 路径泄漏候选与已挂载 captureexposeCrossSessionFailure:3421-3429)既不回收 intent.candidate 也不清除 runnerControlRef.capture;经由它 settle 的站点(pump 顶部 environment/deadline 检查 :3771-3790,candidate 分支三处 :3809:3818:3845)回收候选却不清 capture。watchdog 回调确实会清理——但 settleCrossSessionIntent 恰好取消了该 watchdog。结果:孤儿缓冲区继续序列化后续 live 事件(上限 1024 个 / 8 MiB),daemon attachment 滞留到 reaper。
  4. 同类泄漏无需 deadlinestaged.repairdaemon.replay_event_malformed 通知直达完整性失败点;事件驱动的 pump 常先于 watchdog 宏任务到达,普通可恢复失败即可触发。
  5. create 窗口内的受控切换被静默丢弃actions.ts:475 的 early-reject,加上受控 effect 在异步结果前记录 lastHandled* 引用(:4671-4673)并吞掉拒绝(:4736)——source-bound createSession 窗口内设置 sessionId prop 不会产生任何 load 调用、通知或重试;create 提交后 prop 与 provider 永久不一致。
  6. watermark 为 undefined 的会话在首个带 id 事件前无法刷新:1190:1828 播种时未加 ?? 0 兜底)。@yiliang114 的批准将其视为"全新零事件会话"的可接受粗糙点;需要说明适用范围更宽——任何纯 re-attach 会话的首个事件之前,即普通新建聊天引导。

六项均无回归测试覆盖。发现 2-6 已由评审探针在强推前的头上复现;本次 re-run 确认相关代码在当前头未变;发现 1 另在当前 main 的 daemon 代码上核实——这不是判断分歧,有界刷新分支就是没有拷贝 epoch。

批准与本次评审的分歧点说明:维护者对提交 CAS、恰好一次 tail 合并、fail-closed 门控与调度的核实是准确的;上述发现位于提交路径之外——daemon 响应形状、shell 锁存、settle 清理、受控引导——从提交路径本身不可见。

测试证据(PR 自身 CI)

本次无人值守运行不执行 PR 代码。检查只拉取一次、不轮询:

Qwen Code CI 工作流从未在本分支运行——本 PR 的任何头上都没有。已通过按分支过滤的工作流运行 API 核实(零运行),并经提交的 check-suites 独立确认:当前头与强推前的头上都不存在 build / lint / typecheck / unit / integration 检查。该头上实际运行的只有:Live Host (macos-latest) ✅、bot 编排任务 ✅、一次 label 运行被取消、review-pr 仍在进行;其余 48 项为跳过的 bot 门禁检查。所以"0 失败"在已运行范围内成立,但仓库主套件从未执行过本代码。批准中的"42 checks green"与该分支的主 CI 记录不符——谨此提醒 @yiliang114 复核当时所看的视图;pull_request 触发为何从未在此分支生效,本身也值得看一眼。

沙箱验证:@qwen-code /verify 运行已作为本次 triage 运行的一部分在进行中——完成后报告会发布在本帖,用于验证运行时 fail-closed 论断。注意发现 1 是当前 main daemon 代码中的静态线上形状缺口;不发送 historyPageSize 的验证 harness 会与现有测试共享同一盲区,请对照上述发现阅读报告。真实场景(tmux)测试:本次无人值守运行 N/A。

Qwen Code · qwen3.8-max

Reviewed at 59fb669232979d92dad451f5b55ff39297d2a5d1 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 2/5 — significant concerns, leaning against as-is: the six Critical findings from the multi-agent review pass are all still present on this head (independently re-verified), including a daemon-side epoch gap that breaks the headline feature on its happy path in the default Web Shell configuration, and the repo's main CI has never run on this branch.

Stepping back: the problem is real and observed, the direction is right, the design is good, and the parts of the previous defer that @yiliang114 was asked to settle have been settled seriously — the commit-path CAS, exactly-once tail merge, and scheduling verification in the approval are accurate, and I corroborated them against the code. That is not where this re-run parts company.

It parts company on what the previous run also flagged: a static read cannot attest to every transition of this state machine — and when you probe it, the earlier review pass's findings reproduce against this head one by one. The decisive one is not a judgment call: refreshedReplayFieldsFor on current main does not copy entry.events.epoch into the bounded-refresh load response, while this PR's integrity gate hard-requires the epoch. Web Shell sends historyPageSize=100 on every load. Therefore refreshing an idle session — the ordinary reload case — fails on every retry in the production configuration. Neither test layer can see it, which is exactly why it survived a thorough suite. The other five (shell latch hang, candidate/capture leak on settle paths, silently dropped prop-driven switches during the create window, undefined-watermark refresh rejection) are lesser but real, and none has regression coverage. An approval that does not engage those findings does not settle them. The standing /review CHANGES_REQUESTED remains the gating state on this PR, and this verdict aligns with it rather than overriding the maintainer's vote — main asks for two approvals, and this is the second reviewer's substantiated objection. (For transparency: this run attempted to submit its own request-changes review pinned to the reviewed commit, but the review bot still holds a pending draft review on this PR, and GitHub allows only one per account — the draft belongs to the in-flight review run and was left untouched.)

Two more things before merge could be reconsidered:

  • CI: the main Qwen Code CI workflow never triggered on this branch, on any head. Approving would attest to code the repo's own suite has never executed. Why the trigger never fired may deserve its own look.
  • Sandboxed verification is in flight as part of this run; its report lands in this thread and should be read against the Stage 2 findings.

@doudouOUC the minimum merge-blocking fix is finding 1 — one line in the daemon's bounded-refresh branch plus a regression test — but please address or rebut each of the six with probe evidence; the review thread has concrete suggested fixes for all of them. @yiliang114 requesting a re-review after the author responds — your commit-path verification stands, my objection is scoped to what it did not cover.

中文说明

置信度:2/5 —— 重大顾虑,倾向不合并当前版本:多代理评审的六项 Critical 发现在该头上全部仍然存在(已独立复核),其中包括 daemon 侧 epoch 缺口——它在默认 Web Shell 配置下让核心特性的正常路径直接失败;且仓库主 CI 从未在本分支运行。

退一步看:问题真实且已观测,方向正确,设计良好;上次缓议请 @yiliang114 定夺的部分已得到认真定夺——批准中对提交 CAS、恰好一次 tail 合并与调度的核实是准确的,我也在代码上印证了。本次 re-run 的分歧不在这里。

分歧在于上次运行同样指出的那点:静态阅读无法为该状态机的每个转换背书——而一旦去探针验证,早前评审的发现在这颗头逐个复现。决定性的一项不是判断分歧:当前 mainrefreshedReplayFieldsFor 没有把 entry.events.epoch 拷贝进有界刷新的 load 响应,而本 PR 的完整性门硬性要求 epoch;Web Shell 每次 load 都发送 historyPageSize=100。因此刷新空闲会话——最普通的 reload 场景——在生产配置下每次重试都失败。两层测试都看不到它,这正是它能在一份扎实的测试套件下存活的原因。其余五项(shell 锁存挂起、settle 路径候选/capture 泄漏、create 窗口内受控切换被静默丢弃、undefined watermark 拒绝刷新)严重度较低但都是真实缺陷,且均无回归覆盖。未触及这些发现的批准不能将其了结。仍挂着的 /review CHANGES_REQUESTED 继续作为本 PR 的合并门禁,本判决与其一致而非推翻维护者的投票——main 需要两个批准,这是第二评审人基于证据的异议。(透明说明:本次运行曾尝试提交自己的、锚定被审提交的 request-changes 评审,但评审机器人仍持有一个未提交的草稿评审,GitHub 每账户只允许一个——该草稿属于仍在进行的评审运行,未做改动。)

重新考虑合并前的另外两点:

  • CI:主 Qwen Code CI 从未在本分支任何头上触发。批准将意味着为一段仓库自身套件从未执行过的代码背书;触发为何从未生效本身值得查看。
  • 沙箱验证作为本次运行的一部分正在进行,报告会发布在本帖,请对照 Stage 2 的发现阅读。

@doudouOUC 最小的合并阻断修复是发现 1——daemon 有界刷新分支的一行修复加一个回归测试——但请逐项处理或以探针证据反驳六项发现;评审线程中都有具体修复建议。@yiliang114 请在作者回应后复审——你对提交路径的核实成立,我的异议仅限于其未覆盖的部分。

Qwen Code · qwen3.8-max

Reviewed at 59fb669232979d92dad451f5b55ff39297d2a5d1 · re-run with @qwen-code /triage

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

Approved. Same-session refresh now goes through the restore coordinator with the same transactional guarantees as cross-session switching, and the commit math checks out.

What I verified

  • Commit path is a synchronous double-checked CAS (commitSameSession): intent/lifecycle/env-generation, exact source object + clientId, epoch, deadline, runner readiness, turn state, and processed cursor are re-checked before and after staging, with no await in between — no interleaving window.
  • No loss/duplication: capture is contiguous-id and bounded (maxQueued + 8 MiB); tail applied at commit is strictly (watermark, processedEventId]; candidate cursor is advanced to the final processed cursor before the new SSE runner starts, so events beyond it are delivered exactly once. captureSourceEvent runs pre-processing and markSourceEventProcessed in finally, so captured-but-unprocessed events are correctly excluded from the tail and re-delivered to the new runner.
  • Fail-closed everywhere: epoch mismatch, incomplete/partial/degraded replay, capture gaps/id-less frames/overflow, in-place clientId self-heal, and missing cursor/epoch all reject the candidate, detach it best-effort once, and preserve the live source with one recoverable transition failure (never rewriting it as disconnected).
  • Scheduling: the watchdog deadline starts with the raw RPC (queue wait excluded), identical signal-free requests coalesce, different same-session intents are latest-wins, cross-session targets supersede a prepared refresh, and a timed-out retry can adopt the still-in-flight raw result while keeping capture armed (.finally clears rawTransitionRef on both success and error paths and re-pumps — no deadlock on abort-during-RPC).
  • createSession source-bound exclusion now tracks the raw request rather than the outer action timeout, and a late successful create after timeout is detached once — matching the design doc.
  • API contract is additive only (optional partial/replayError, client-derived replaySnapshotComplete, eventEpoch getter); no daemon changes required.
  • Test coverage is thorough: prompt/shell/observer gating, completeness matrix, tail-gap/overflow cases, rebind modern+legacy+failure, timeout-retry adoption, cross-session supersede, plus real-daemon JSDOM tests for atomic replay+tail merge, structured 504 preservation, and transcript continuity across rebind.

CI: green on head sha (42 checks, 0 failures; only the review-bot itself in progress and a cancelled label job).

Non-blocking nits (P2/P3)

  1. clientIdRef render-time reset uses initialClientIdDependencyRef, which is frozen at first render. With an explicit clientId prop on a modern daemon, a committed rebind to a new id gets reset back to the first-render prop on the next render; a later SSE reconnect then requests the stale id. Daemon registerClient mints a fresh id for unknown requests (load never rejects), so this self-heals with mild attach churn rather than breakage — but worth a follow-up to reconcile the ref with the committed owner.
  2. Refresh on a session with no cursor/epoch yet (e.g. a brand-new zero-event session) fails fail-closed with "cursor is unavailable; session was preserved" — correct direction, slight UX rough edge.
  3. If the processed cursor advances during staging, commit surfaces as "integrity validation" failure instead of re-waiting; recoverable by retry, acceptable.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (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: 12 passed · 0 failed · 12 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

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

脚本断言:12 通过 · 0 失败 · 12 总计

Verification report

PR #8939 — fix(webui): Make same-session refresh transactional

Verdict: merge-ready — 12/12 scripted assertions passed, 0 unexpected failures. Verified head: 59fb669232979d92dad451f5b55ff39297d2a5d1 (merge-ref checkout f1f602a8, base tip ac78acd3). Single-commit PR; the snapshot's commits array matches the locally reachable commit.

中文摘要
  • 结论: merge-ready。12/12 脚本化断言通过,0 个意外失败。
  • A/B 结论: 中心主张(同会话 refresh 事务化)被证明 load-bearing。真实 daemon 集成场景在 head 3/3 通过;把 webui 两个改动文件回退到 base 后,同样场景 2/3 失败且失败是行为性的(held-response 期间 source 变 disconnected;structured 504 时公开 action 挂死 30s×3 而非可恢复失败)。单元级 A/B:base 源码下 332 个 webui 测试恰好失败 27 个(26 个新增 + 1 个被 PR 改写),其余 305 个通过;SDK 57 个中恰好失败 3 个。
  • Wire oracle: 失败的 refresh 在 wire 上 1 次 load、0 次 detach,source 之后仍能跑真实 prompt;成功的 refresh 提交后 retirement detach 指向与已提交 attachment 相同的 clientId,真实 daemon 上会话存活且后续 prompt 成功(daemon 的 clientId 引用计数账本吸收了这次 detach,packages/acp-bridge/src/bridge.tsregisterClient/unregisterClient)。
  • Findings: 1 条 Suggestion 级观察(见下),非阻塞。
  • 未覆盖: Windows/Linux 本地运行、跨 epoch/ring resync、live-journal repair、branch adoption(均为 PR 声明的 out-of-scope);base 侧 wire harness 未单独运行(A/B base 臂用的是 PR 自带集成场景)。

Central claim and A/B proof

Central claim: on a daemon advertising client_identity, same-session load/reload/resume/client-ID rebind is transactional — the visible source (attachment, transcript, SSE, prompt state, controls) stays usable until the candidate restore is validated and committed; failed/timed-out/stale candidates are retired with one recoverable failure and never disturb the source.

A/B design: head arm = built head webui dist + head SDK dist; base arm = a control dist built from head webui with only the two PR-changed source files (DaemonSessionProvider.tsx, actions.ts — the only non-test webui files in the diff) reverted to HEAD^1, pinned to a base-built SDK via a local node_modules/@qwen-code/sdk symlink (realpath asserted to point into the base tree; base SDK dist asserted to lack replaySnapshotComplete, base webui dist asserted to lack commitSameSession/the incomplete-snapshot error). The daemon (packages/cli/dist/index.js, untouched by this PR) is identical in both arms. Same three real-daemon scenarios (spawned qwen serve + mock ACP echo child, real SDK, real provider dist in JSDOM) run against each arm:

Scenario (real daemon, wire-level) head base control
Held load response: source stays connected + preparing, live tail merged, replay+tail exactly once ✓ 5.0 s ✗ source goes status: 'disconnected' while the candidate is still held (01-integration-head-3-of-3.png, 02-integration-base-2-of-3-fail.png)
Structured 504 on same-session reload: action fails recoverably, source preserved ✓ 4.9 s, DaemonHttpError{504} ✗ action never settles; 30 s vitest timeout ×3 retries (102 s) — the destructive path wedges instead of failing
Controlled clientId rebind: resume keeps transcript, prompt works after ✓ 4.9 s ✓ 4.9 s (legacy full load also restores the transcript after the fact; the destruction is mid-flight, invisible to the final-state assertions)

The two load-bearing cells flip broken→fixed; the third is a boundary the PR deliberately preserves, and it passes on both arms.

Unit-level A/B / vacuity (same test files, sources reverted to base in the head tree): webui 27 failed / 305 passed (332); SDK 3 failed / 54 passed (57). The 27 webui failures are exactly the 26 PR-added tests plus the one PR-modified pre-existing test (does not restart the current session while a target switch is preparing, rewired to the new isDifferentLogicalTransitionPending option; it times out on base because base proceeds with the legacy switch instead of rejecting). All failures are behavioral assertion mismatches/timeouts, no import or collection errors — so every new test pins the new behavior and no test is vacuous. The two added tests that pass on base are regression guards for behavior base already had (keeps an empty-owner load on the bootstrap path, consumes a controlled origin when a source-bound operation blocks restore) — pinned-but-not-new, which is what they assert. See 03-unit-ab-base-fails-27.png.

Gates at head: webui DaemonSessionProvider.test.tsx + actions.test.ts 332/332; SDK DaemonSessionClient.test.ts 57/57; tsc --noEmit clean for both packages.

Reviewer Test Plan walkthrough: step 1 (held response + live work during hold + exactly-once commit) = integration cell 1, head ✓ / base ✗. Step 2 (504 / malformed / partial / epoch / gap / id-less / overflow) = integration cell 2 plus the unit matrix (rejects a same-session load with … ×5, …captured source tail has … ×3), all green at head and red on base. Step 3 (rebind via resume, transcript preserved, prompt after; legacy daemon full load) = integration cell 3 + uses a full load for a legacy controlled clientId rebind. Step 4 (refresh waits/fails around in-flight prompt, shell command, create, cross-session target) = waits for an admitted prompt…, waits for an in-flight shell command…, keeps a remote observer turn live…, blocks a restore while active-session creation is in flight, keeps restore blocked after create times out…, retires a prepared same-session candidate when a cross-session target supersedes it — all red on base, green at head.

Wire oracle (independent harness, real daemon)

Harness harness/pr8939-wire-harness.test.ts records every HTTP request the client sends while driving the built provider against a real daemon (04-wire-oracle-w1-w2.png):

  • W1 (failed refresh): exactly 1 /load, 0 /detach; connection stays connected with the source clientId; afterwards a real prompt through the source succeeds — the source is not merely cosmetically preserved, it is live.
  • W2 (successful refresh): exactly 1 /load; after commit the provider sends one /detach whose X-Qwen-Client-Id equals the committed attachment's clientId (same as the source's — the refresh reuses it by design); the session nonetheless survives and a prompt through the committed attachment works, connection still connected 500 ms later.

Findings

S1 (suggestion, non-blocking) — post-commit retirement detach targets the committed attachment's own clientId; safety rests on the daemon's refcounted client ledger. commitSameSession ends with retireAttachment(intent.source, intent), and detachDaemonClient POSTs /session/:id/detach with the source clientId — which is identical to the candidate's (beginCrossSessionTransition sets targetClientId = source.clientId for same-logical requests, and the provider rejects a candidate whose clientId differs). On the current daemon this is safe: /load with an existing clientId increments a per-clientId refcount (registerClient, packages/acp-bridge/src/bridge.ts:2622), so the retirement detach only decrements 2→1 (unregisterClient, never reaching the clientIds.size === 0 close path), and W2 measured the session surviving with a working prompt. The coupling is real, though: the provider's correctness here depends on daemon-side ledger semantics it does not control, and the PR's own unit test models the detach with an unconditional-204 fetch stub that would accept a daemon that kills the session. If a future daemon change alters load/detach refcounting (e.g. load stops re-registering an adopted clientId), a successful refresh would close the session it just committed — and the unit suite would stay green. Recommend either (a) a daemon-side integration test that sends a prompt after a same-session commit (this round's W2 does exactly that; consider landing it), or (b) skipping the retirement detach when candidate.clientId === source.clientId. Not blocking: measured safe on the shipped daemon, and the ledger behavior is documented in the bridge (BkwQP comment).

Not covered

  • Windows/Linux local runs, cross-epoch/ring resync, live-journal repair, branch adoption, selective JSONL reading, global attachment scheduling — all declared out of scope by the PR and not probed.
  • The ~80 MiB manual memory-trace claim in the description was not reproduced; the 8 MiB capture bound is covered only by the PR's unit tests (overflow/id-less/gap cells, red on base).
  • The base arm of my independent wire harness was not run; the base arm of the A/B uses the PR's own integration scenarios instead (whose base failures are behavioral and quoted above).
  • Repo-wide gates were not run; only the two affected workspaces were typechecked/tested. The PR's CI covers the rest.
  • One early evidence capture attempt was invalid (cross-env not on PATH → 127 masked by a pipe); it was discarded and every cell re-run or re-rendered from the real run logs. Raw logs for every cell ship in this artifact dir.
  • Per-commit attribution: single-commit PR, snapshot commits matches; nothing to split.

Methodology

Environment: CI verify container (node 22, merge-ref checkout at depth 2; npm ci + npm run build pre-run). Unit gates via per-package vitest at head. Wire-level A/B: real qwen serve daemon spawned from packages/cli/dist/index.js (untouched by the PR) with the mock ACP echo child; the built @qwen-code/webui/daemon-react-sdk dist rendered in JSDOM with the real @qwen-code/sdk over loopback HTTP/SSE. Base control: head webui source with only the two changed files reverted, built against a base-built SDK (local symlink; realpath asserted; marker greps on both dists asserted, checks 10–12 of check-assertions.mjs). Unit A/B: same two files (plus the two SDK files for the SDK suite) reverted in-tree, full test files run, failures classified against the diff. All expectations — including "base fails exactly these N tests" — are encoded as scripted assertions in check-assertions.mjs (12/12 pass, 05-scripted-assertions-12-of-12.png); vitest totals are parsed from the raw logs shipped alongside. Evidence captures render the actual run logs; raw logs (integration-head-full.log, integration-base-full.log, webui-reverted-run.log, sdk-reverted-run.log, wire-harness-head.log, gate logs) are in this directory.

Evidence images

01-integration-head-3-of-3

02-integration-base-2-of-3-fail

03-unit-ab-base-fails-27

04-wire-oracle-w1-w2

05-scripted-assertions-12-of-12

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

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 59fb669232979d92dad451f5b55ff39297d2a5d1 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 59fb669232979d92dad451f5b55ff39297d2a5d1既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@ytahdn ytahdn 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. No critical issues found. Key suggestions: (1) legacyClientIdDependency is misleadingly named — consider effectiveClientIdDependency and add a comment explaining why modern daemons use the initial prop value; (2) commitSameSession pre-staging and final-snapshot guards are ~30 lines each and nearly identical — extract a shared helper; (3) CrossSessionIntent now covers same-session refresh via sameLogical — rename to SessionTransitionIntent or add a comment; (4) structured matchers preferred over JSON.stringify(...).includes() in tests. Questions: confirm exposeCrossSessionFailure clears desiredTransitionRef.current; document best-effort detach failure semantics in commitSameSession.

@doudouOUC
doudouOUC dismissed qwen-code-ci-bot’s stale review August 12, 2026 06:20

already have 2 approves, 3ks.

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 5f29e7f Aug 12, 2026
75 of 77 checks passed
@doudouOUC
doudouOUC deleted the fix/transactional-same-session-refresh branch August 12, 2026 06:21
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

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

Labels

daemon scope/session-management Session state and persistence

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants