Skip to content

feat(serve): Add live-state session activity watermark - #9396

Merged
doudouOUC merged 9 commits into
QwenLM:mainfrom
doudouOUC:agent/workspace-session-live-state-updated-at-design
Aug 19, 2026
Merged

feat(serve): Add live-state session activity watermark#9396
doudouOUC merged 9 commits into
QwenLM:mainfrom
doudouOUC:agent/workspace-session-live-state-updated-at-design

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR implements the workspace session live-state activity watermark contract designed earlier on this branch. The bridge now advances a strictly monotonic per-session watermark once when a prompt that reached running publishes its formal terminal, projects it as the already-typed optional BridgeSessionSummary.updatedAt, and returns it from GET /workspaces/:workspace/sessions/live-state. Full workspace session lists now pick the later valid timestamp between the live watermark and the persisted transcript mtime so an async transcript write can never move a row backward. The TypeScript SDK type and the developer protocol docs describe the new optional field. The design document that opened the branch remains, followed by the implementation commit that realizes it. Web Shell consumption stays out of scope by design.

Why it's needed

The workspace live-state protocol removed high-frequency full session-list polling, but the current Web Shell consumer still schedules a rate-limited catalog reconciliation after every completed turn because live-state does not carry session recency. Populating the already-typed activity watermark closes that gap on the server side, keeps the catalog version stable across ordinary turns so the two-second live-state poll does not observe a mismatch, and gives the later consumer PR a deployed compatibility target while old clients continue exercising the unchanged path.

Reviewer Test Plan

How to verify

  • Confirm updatedAt is an optional additive field under response v: 1 and the existing workspace_session_live_state capability, with no new route, feature gate, or readiness feature.
  • Confirm the watermark advances exactly once only when a prompt that reached running wins its formal terminal latch, is written before the terminal event is published, remains strictly increasing under same-millisecond terminals, clock rollback, or a corrected forward jump, and is never driven by admission, streaming, queue-only cancellation, heartbeat, or interaction waits.
  • Confirm ordinary turn activity leaves generation + revision, persisted-list cache invalidation, selected-runtime ownership, trust checks, status codes, and the route's memory-only behavior unchanged.
  • Confirm live/persisted summary merging keeps the later valid timestamp so an async transcript write cannot move a row backward, absent or unparseable candidates preserve the other valid value, and the workspace live-state route still projects only the bridge-local value.
  • Confirm the TypeScript SDK exports the optional field, GET /session/:id/status returns the bridge-local watermark directly without merge, and the compatibility, downstream consumer, test, and rollout coverage described in the design document remain accurate for the shipped implementation.

Evidence (Before & After)

N/A — server protocol change with no user-visible TUI or Web Shell surface. Behavior is exercised through the bridge, route, list-merge, and SDK unit tests added in this PR.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node 22, local npm run typecheck, npm run lint, npx prettier --check, and targeted vitest run against the bridge, CLI serve, and SDK test files touched by this change.

Risk & Scope

  • Main risk or tradeoff: A downstream consumer of the already-public BridgeSessionSummary.updatedAt field could depend on the current live-first merge policy or on the field remaining unpopulated from the live path. The design's downstream consumer audit maps every read site: workspace live-state, full session lists, live-only lists and cursors, GET /session/:id/status, Live Task cursors, goals, owner resolution, and admission all read compatibly, and the merge helper now takes the later valid timestamp so a stale mtime cannot displace a fresh terminal and a delayed mtime cannot mask a live regression.
  • Not validated / out of scope: Web Shell request-count reduction, real-daemon E2E fault injection, performance under sustained turn churn, and the follow-up consumer's completion-sequence handshake. Those belong to the later Web Shell PR and its own reviewer plan.
  • Breaking changes / migration notes: None. The field is optional and additive, response v: 1 and the existing workspace_session_live_state capability are unchanged, no configuration or stored-data migration is required, and old clients decode the response by ignoring the extra property.

Linked Issues

Follow-up to #9261 and #9366. Web Shell consumption ships in a separate PR.

中文说明

本 PR 做了什么

在本分支之前提交的设计文档基础上,实现 workspace session live-state 活动水位契约。Bridge 在到达 running 的 prompt 发布正式终态时严格单调地推进每会话水位,投影到已经声明但未填充的 BridgeSessionSummary.updatedAt,并由 GET /workspaces/:workspace/sessions/live-state 返回。整包 workspace session list 合并时改为取 live 水位与持久化 mtime 中较晚的那个有效值,避免异步 transcript 写入让活动排序回退。TypeScript SDK 类型和开发者协议文档同步更新。设计 PR 打开时的文档保留,后跟本次实现 commit。Web Shell 消费端仍是后续独立改动。

为什么需要

Workspace live-state 协议取消了高频 session-list 轮询,但当前 Web Shell 在每次 turn 完成后仍安排一次限频 catalog reconciliation,因为 live-state 不携带会话最近活动时间。填充这个已声明的活动水位在服务端关闭该缺口;catalog 版本在普通 turn 下不变,所以两秒轮询不会因此触发全量扫描;后续消费端 PR 也能对着一个已经部署的兼容目标写,而老客户端继续走原路径。

Reviewer 测试方案

如何验证

  • 确认 updatedAt 是 response v: 1 与既有 workspace_session_live_state capability 下的可选增量字段,不新增路由、feature gate 或 readiness feature。
  • 确认仅当到达 running 的 prompt 赢下正式终态 latch 时水位才恰好推进一次;水位写入发生在终态事件发布之前;同毫秒多个终态、时钟回拨或修正后的向前跳跃下水位仍严格递增;admission、streaming、仅排队取消、heartbeat 和交互等待均不推进它。
  • 确认普通 turn 活动不改变 generation + revision、persisted-list cache 失效、selected-runtime 所有权、trust 检查、状态码以及路由纯内存行为。
  • 确认 live/持久化合并取较晚有效值,异步 transcript 写入不再让行回退;两端值缺失或无法解析时优先保留仍有效的一方;workspace live-state 路由仍只投影 bridge-local 值。
  • 确认 TypeScript SDK 暴露该可选字段;GET /session/:id/status 直接返回 bridge-local 水位(无合并);设计文档描述的兼容性、下游消费者、测试与发布覆盖在实现中依然成立。

证据(改动前后)

N/A — 服务端协议改动,无用户可见 TUI 或 Web Shell 表面。行为通过本 PR 新增的 bridge、route、list-merge、SDK 单测覆盖。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

Node 22,本地 npm run typechecknpm run lintnpx prettier --check 与针对本次改动文件的 vitest run 全部通过。

风险与范围

  • 主要风险或权衡:BridgeSessionSummary.updatedAt 是一个此前已声明但未由 live 路径填充的已公开字段。设计文档的下游消费者审计已覆盖所有读点(workspace live-state、全量 session lists、live-only 列表与游标、GET /session/:id/status、Live Task 游标、goals、owner resolution、admission),且合并规则改为取较晚有效值,既阻止已过时的 mtime 顶替新终态,也阻止延迟的 mtime 掩盖 live 回退。
  • 未验证 / 范围外:Web Shell 请求数下降、真实 daemon 的 E2E 故障注入、持续 turn 压力下的性能,以及后续消费端的 completion-sequence 握手。这些属于后续 Web Shell PR 及其自己的 reviewer 测试方案。
  • 破坏性变更 / 迁移说明:无。字段为可选增量;response v: 1 与既有 capability 均保持不变;不需要配置、存储数据迁移或 feature flag;老客户端忽略新增属性即可正确解码。

关联 Issue

#9261#9366 的后续工作。Web Shell 消费端在独立 PR 中提交。

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC doudouOUC self-assigned this Aug 18, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Design review, verified against the code at the PR base (2af4f903de, the #9366 merge commit). Every factual claim in the document checks out: the declared-but-unpopulated BridgeSessionSummary.updatedAt, mtime-derived persisted summaries, the live.updatedAt ?? existing.updatedAt merge, the publishPromptTerminal latch + running-state gate ordering, heartbeat-driven sessionLastSeenAt, the memory-only route with version-fenced cache invalidation, the SDK single-REST/no-probe contract, the serialized async recording queue, and the current Web Shell 2s poll + rate-limited post-turn reconcile. An independent grep of all getSessionSummary / listWorkspaceSessions read sites found no consumer missing from the audit table — the ACP-transport session/list dispatch shares listWorkspaceSessionsForResponse, so it is functionally covered by the "Full workspace session lists" row.

Direction is sound: terminal-latch authority, the catalog-version boundary, the later-valid-timestamp merge correction, and the two-PR split are the right calls. Four inline suggestions below — all wording/completeness level, none blocking.

CI note: Test (ubuntu-latest, Node 22.x) failed with ENOSPC: no space left on device during npm install — runner infrastructure flake, unrelated to this docs-only change; a rerun should clear it.

Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
doudouOUC and others added 2 commits August 18, 2026 16:38
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Advance a bridge-local per-session activity watermark once when a prompt
that reached the running state publishes its formal terminal, project it
as the existing optional BridgeSessionSummary.updatedAt, and expose it on
the workspace live-state route. The advance is written before the
terminal is published so a client that observes the terminal cannot read
a stale value, and the extra millisecond keeps the watermark strictly
increasing when several terminals share a wall-clock millisecond or the
clock moves backward. A queued-only terminal, heartbeat, attach/detach,
or streamed update never advances it, and turn activity does not change
the session catalog version.

Populating the already-typed summary field lets full workspace session
lists merge live and persisted timestamps. Because the mtime and the
running-turn watermark are different authorities and the recorder writes
asynchronously, the merge picks the later valid timestamp instead of
blindly preferring the live value, so a row cannot move backward when an
async transcript write lands after the terminal.

Extend the response schema documentation for the live-state route and
GET /session/:id/status, and add the optional field on the TypeScript
SDK DaemonSessionLiveState type so consumers can pre-flight the tag once
and read the recency directly.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC doudouOUC changed the title docs(serve): Design live-state session activity timestamps feat(serve): Add live-state session activity watermark Aug 18, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

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

Qwen Code · serve A/B

@doudouOUC
doudouOUC marked this pull request as ready for review August 18, 2026 10:12
@doudouOUC
doudouOUC enabled auto-merge August 18, 2026 10:12
@doudouOUC
doudouOUC requested review from qwen-code-ci-bot, wenshao, yiliang114 and ytahdn and removed request for qwen-code-ci-bot August 18, 2026 10:13
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run at an unchanged head (731d9b0c) — no commits have landed since the last pass. What changed is downstream of this gate: the maintainer sign-off this class of PR ships on is now on record. Gate picture unchanged; the numbers are carried over, not re-derived.

Template ✓ — all sections present, bilingual, reviewer test plan included.

Problem: real, and evidenced in already-merged code. The Web Shell consumer shipped in #9366 still carries its workaround comment — "the live-state overlay carries no updatedAt — flag a rate-limited reconcile so activity stamps keep refreshing." This PR fills that gap server-side. Not theoretical.

Direction: aligned. It completes the typed-but-unpopulated BridgeSessionSummary.updatedAt contract #9261 left behind, and touches public contract surface (protocol docs, SDK exported type, activity-cursor shape). The prior pass said this class ships on a maintainer's sign-off rather than a bot approval alone; since then @ytahdn approved on this exact head after a point-by-point static review, and @wenshao — whose change request stood from ba3f9e2e — re-reviewed (round 5: zero new findings) and posted a runtime-verification LGTM on the same head. The sign-off the gate asked for exists.

Size: 2,506 changed lines break down as 377 production logic lines (bridge watermark, session-list cursor/merge, the activity-timestamp.ts helper, Live Task read paths, route projection, SDK type), 1,417 test lines, and 712 documentation lines (695 of which is the design doc). No thresholds tripped.

Approach: every commit maps to a named finding. 81ff5998 carried emitted identities in the activity cursor (answering the round-1 cursor-key regression), be744e47 closed the three drop paths round 3 probed, and 731d9b0c put the createdAt floor back into the design doc's normative advance formula. No drive-by changes spotted; nothing has landed since.

Risk: no high-risk path matches.

Moving on to code review. 🔍

中文说明

在不变的 head(731d9b0c)上重跑——上一轮之后没有新 commit。变化发生在本门槛之外:此类 PR 合入所需的 maintainer 签核现已在案。门槛判断不变,数字沿用、未重新推导。

模板 ✓ —— 各节齐全、中英双语、附 reviewer 测试方案。

问题:真实存在,且有已合入代码佐证。 #9366 合入的 Web Shell 消费端仍带着那条绕行注释——"the live-state overlay carries no updatedAt — flag a rate-limited reconcile so activity stamps keep refreshing"。本 PR 在服务端填上该缺口。不是理论性问题。

方向:对齐。 补全 #9261 留下的"已声明未填充"契约 BridgeSessionSummary.updatedAt,并触及公开契约面(协议文档、SDK 导出类型、activity-cursor 形状)。上一轮曾表示此类改动需 maintainer 签核而非仅机器人批准;此后 @ytahdn 已在本 head 上逐点静态审查后批准,@wenshao——其修改要求立于 ba3f9e2e——也已复审(round 5:零新发现)并在同一 head 上发布运行时验证 LGTM。门槛所要求的签核已经存在。

规模: 2,506 行改动分为:377 行生产逻辑(bridge 水位、session-list 游标/合并、activity-timestamp.ts helper、Live Task 读路径、路由投影、SDK 类型)、1,417 行测试712 行文档(其中 695 行为设计文档)。未触及任何阈值。

方案:每个 commit 都对应一条指名发现。 81ff5998 让 activity cursor 携带已发出身份(回应 round-1 的游标键回归),be744e47 关闭 round 3 探出的三条丢失路径,731d9b0c 在规范推进公式中补回 createdAt 地板。未发现夹带改动;此后亦无新增。

风险: 未命中高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 731d9b0c36a1a803d8d420c20513fb944f937849 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run at an unchanged head (731d9b0c). No commits landed since the last pass, so the diff is identical to what was read line by line then; this pass re-walked every production hunk independently rather than trusting the prior notes, and found nothing new. What changed since is the evidence around the diff, covered under testing.

Code review

The implementation is the shape an independent proposal lands on: advance a strictly monotonic per-session watermark (max(Date.now(), previous + 1)) inside the single terminal funnel publishPromptTerminal, only for prompts that reached running, written before the terminal event is published; floor the first advance at createdAt so a wall-clock rollback cannot key a row behind the createdAt it was already listed at; project it as the already-typed optional BridgeSessionSummary.updatedAt with absent-key semantics. Where it exceeds the proposal is the failure-mode discipline around pagination.

The two round-1 Critical findings remain closed, and this pass re-verified the mechanisms rather than taking the prior pass's word:

  • Pagination divergence (merge on later pages). mergeLiveSessionSummary now runs on every page of the organized list, not just the first, so a page-1 cursor encoded from merged keys cannot re-admit a row a later page keyed by persisted mtime alone; live-only rows stay first-page insertions because they have no persisted key to page by. The later-valid rule lives in one shared helper (laterActivityTimestamp) used by the default, organized, metadata, Live Task, and cursor paths — a session that is both live and persisted reports one recency on every route.
  • Cursor-key regression on live-entry retirement. Activity cursors carry emitted identities (bounded at 64 for URL safety); an identity is retained while transiently absent from the page's collection (-Infinity floor — over-retention is the safe direction), while it has no persisted floor yet, or while either pin state could re-admit it; overflow drops the highest persisted keys first and degrades to at-most-once duplicates rather than failing the pass. Cursors minted before the field existed parse with an empty list; a malformed list is an invalid cursor.

The docs tell the truth about the guarantee — the protocol doc states the residual duplication modes (persisted-only rows are never carried; callers should key by sessionId) instead of hiding them, and the SDK type documents updatedAt as ordering-only, not a persistence acknowledgement. The design doc's normative formula matches the shipped helper in all three branches.

Residual observations, all previously deferred as Suggestion-class and unchanged: the teardown-flush advance is untestable through the public interface; GET /session/:id/status has no test supplying updatedAt (the docs state the verbatim projection); the write-before-publish happens-before is not pinned by a synchronous-subscriber test — @ytahdn's one non-blocking suggestion, which the author confirmed accurate at this head and recorded as a follow-up rather than voiding the existing sign-offs with another push.

Test evidence — the PR's own CI (unattended run)

Unattended CI run: no code from this PR was built or executed by the triage agent. Evidence below is the PR's own check-runs on 731d9b0c, re-fetched through the API for this pass. The full pull_request suite on the reviewed head remains complete and green — all five PR CI workflow runs (Qwen Code CI, Qwen Live Host CI, Security Checks, SDK Java, Serve A/B) completed successfully; the only in-flight check is review-pr, bot orchestration on pull_request_target, not PR CI. macOS/Windows Node jobs are skipped by fork-PR policy.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Live Host (macos-latest) ✅ success
Real daemon E2E / Java 11 ✅ success
ubuntu-latest / Java 11, 17, 21 ✅ success
macos-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success
Secret scan (TruffleHog) ✅ success
Dependency CVE audit ✅ success
precheck-pr / precheck ✅ success
label, Classify PR ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (fork-PR policy)
Test (windows-latest, Node 22.x) ⏭️ skipped (fork-PR policy)
Integration Tests (CLI, No Sandbox) ⏭️ skipped

Serve A/B at this head (4 scenarios, no response changes against the PR base) confirms the additive wire surface; the watermark behaviour itself is the unit suite's job above.

Sandboxed verification (@qwen-code /verify) was triggered for this head three times and died on infrastructure all three times — the lane is named, and it never produced a report. The behavioural gap it existed to close is now covered by stronger, maintainer-run evidence on this exact head, so this comment states that evidence with its provenance instead of asking for a fourth infra retry: @wenshao's runtime verification (comment in this thread, 2026-08-19) built both arms from source — PR head 731d9b0c vs merge-base — ran two real qwen serve daemons with real qwen --acp children against a mock upstream, and reports 59/59 runtime assertions pass, the PR's own suites green (1,923 tests), and 9/9 source mutations killed, i.e. every production hunk is load-bearing. @ytahdn's approval independently verified the contract point-by-point at the same head (exactly-once advance through all five terminal call sites, write-before-publish ordering, monotonicity formula, uniform later-valid merge, and closure of the four earlier blockers). Both are maintainer-run, not author-reported; read the report itself for the per-item breakdown.

Real-scenario testing

N/A for this run — unattended CI never executes PR code, and the change has no user-visible TUI surface (server-side protocol field and cursor internals). Not verified by this agent: watermark and cursor-dedup behaviour under a real daemon — covered instead by the maintainer-run runtime verification cited above.

中文说明

在不变的 head(731d9b0c)上重跑。上一轮之后无新 commit,diff 与上次逐行读过的完全相同;本轮未采信上次笔记,而是独立重走了每个生产 hunk,无新发现。变化在 diff 之外的证据,见测试部分。

代码审查

实现即是独立方案会落到的形态:在唯一终态漏斗 publishPromptTerminal 内、仅对到达过 running 的 prompt 推进严格单调的每会话水位(max(Date.now(), previous + 1)),先写后发布;首次推进以 createdAt 为地板,墙钟回拨无法把行键控到其已列出的 createdAt 之后;投影为已声明的可选 BridgeSessionSummary.updatedAt,采用键缺失语义。超出独立方案之处在分页的失败模式纪律。

round-1 的两条 Critical 发现仍然关闭,本轮复核了机制本身而非转述上次结论:

  • 分页分歧(后续页合并)。 organized 列表现在每一页都执行 mergeLiveSessionSummary 而非仅第一页,第 1 页以合并键编码的 cursor 不会因后续页单独用 mtime 键控而重新接纳同一行;live-only 行因无持久化键可分页,仍为第一页插入。later-valid 规则收在一个共享 helper(laterActivityTimestamp)中,default、organized、metadata、Live Task 与 cursor 各路径共用——同时 live 且持久化的会话在所有路由上报告同一个最近活动时间。
  • live 条目退役时的游标键回归。 activity cursor 携带已发出身份(URL 安全上限 64);身份在页面集合中瞬时缺失时保留(-Infinity 地板——过度保留是安全方向)、在无持久化地板时保留、在任一 pin 状态可能重新接纳时保留;超限优先丢弃持久化键最高者,降级为至多一次重复而非取页失败。字段出现前铸造的 cursor 按空列表解析;列表畸形按非法 cursor 处理。

文档如实描述保证范围——协议文档写明残余重复模式(persisted-only 行从不被携带;调用方应按 sessionId 键控),而不是藏起来;SDK 类型注明 updatedAt 仅作排序信号、不是持久化确认。设计文档的规范公式与实现在三个分支上一致。

残余观察,均为先前已按建议级延后且无变化:teardown-flush 推进无法经公开接口测试;GET /session/:id/status 仍无提供 updatedAt 的测试(文档已写明逐字投影);先写后发布的 happens-before 尚无同步订阅者测试钉住——即 @ytahdn 唯一的非阻塞建议,作者已在本 head 上核实其准确,并选择记录为后续跟进而非再推一次 commit 使现有签核作废。

测试证据 —— PR 自己的 CI(无人值守运行)

无人值守 CI 运行:triage agent 未构建、未执行本 PR 的任何代码。以下证据为本轮通过 API 重新获取的 731d9b0c 上 PR 自己的 check 结果。审查 head 上的 pull_request 全套 CI 保持完成且全绿——五个 PR CI 工作流(Qwen Code CI、Qwen Live Host CI、Security Checks、SDK Java、Serve A/B)全部成功完成;唯一在途的 review-prpull_request_target 事件的机器人编排,不是 PR CI。macOS/Windows Node 任务按 fork PR 策略跳过。CI 表格见上方机器可读区域。

Serve A/B 在本 head 为绿(4 场景、相对 PR base 无响应变化),确认增量线协议面;水位行为本身由上方单测套件覆盖。

沙箱验证(@qwen-code /verify)针对本 head 触发过三次,均因基础设施失败——车道已指名,但始终未产出报告。它要关闭的行为缺口现由更强的、maintainer 亲自运行的证据在同一 head 上覆盖,故本评论直接陈述该证据及其出处,不再请求第四次重试:@wenshao 的运行时验证(本线程 2026-08-19 评论)从源码构建双臂——PR head 731d9b0c 对 merge-base——以两个真实 qwen serve daemon、真实 qwen --acp 子进程加 mock 上游驱动,报告 59/59 运行时断言通过、PR 自带套件全绿(1,923 个测试)、9/9 源码变异体被杀(即每个生产 hunk 都是 load-bearing)。@ytahdn 的批准在同一 head 上独立逐点核验了契约(五个终态调用点的恰好一次推进、先写后发布顺序、单调性公式、一致的 later-valid 合并、四条先前阻塞项的关闭)。两者均为 maintainer 亲自运行而非作者自述;逐条明细请阅读报告本身。

真实场景测试

本次运行不适用——无人值守 CI 从不执行 PR 代码,且改动无用户可见 TUI 面(服务端协议字段与 cursor 内部结构)。本 agent 未验证:真实 daemon 下的水位与游标去重行为——改由上文引用的 maintainer 运行时验证覆盖。

Qwen Code · qwen3.8-max

Reviewed at 731d9b0c36a1a803d8d420c20513fb944f937849 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean across every stage on this head; the only remaining items are the recorded Suggestion-class deferrals (test-pinning probes from review rounds 4–5 and @ytahdn's synchronous-subscriber assertion), all confirmed non-blocking and routed to a follow-up.

The prior pass deferred for one concrete reason: the maintainer who requested changes had not seen the three fix commits, and the end-to-end signal was unsettled. Both are now resolved on this exact head. @wenshao re-reviewed (round 5: zero new findings — six probes, all deferred as non-blocking) and went further than the gate could ask: a runtime verification with both arms built from source, 59/59 assertions against the reviewer test plan, the PR's own 1,923-test suite green, and 9/9 source mutants killed. @ytahdn independently approved after verifying the contract point by point, including closure of all four earlier blockers.

Stepping back: my independent proposal before reading the diff was the shape this PR implements — one strictly-monotonic watermark advanced in the single terminal funnel, later-valid merge, identity-carrying cursors. Where the PR exceeds it is still the failure-mode discipline: each review round got a named commit instead of an argument, the carry degrades to at-most-once duplicates instead of errors, and the docs state the residual duplication modes instead of hiding them. When the author chose to record @ytahdn's last suggestion as a follow-up rather than push again and void two sign-offs, that was the right call for the same reason. If I had to maintain this in six months, the shared laterActivityTimestamp helper and the scoped protocol-doc guarantees are the parts I'd thank the author for.

I am approving because the evidence is affirmative, not because I ran out of objections: the problem is real and evidenced in merged code, the diff is minimal and matches its stated goal, CI is fully green, and the two humans best placed to judge it have signed off on exactly this commit. One mechanics note for the merge: @wenshao's change request from ba3f9e2e still formally stands in the review state even though his round-5 pass found nothing new — only he can retire it. With that dismissed, this approval and @ytahdn's make the two the branch requires.

中文说明

置信度:4/5 —— 本 head 上各阶段均干净;剩余项仅为已记录的 Suggestion 级延后项(review round 4–5 的测试钉住探针与 @ytahdn 的同步订阅者断言),均已确认非阻塞并转入后续跟进。

上一轮 defer 出于一个具体原因:提出修改要求的 maintainer 还没有看到三个 fix commit,端到端信号未落定。两者如今都在本 head 上解决。@wenshao 已复审(round 5:零新发现——六条探针全部按非阻塞延后),并做到了门槛无法要求的地步:双臂均从源码构建的运行时验证,59/59 断言对照 reviewer 测试方案,PR 自带 1,923 测试套件全绿,9/9 源码变异体被杀。@ytahdn 在逐点核验契约(含四条先前阻塞项的关闭)后独立批准。

退一步看:我在读 diff 前的独立方案就是本 PR 实现的形态——在唯一终态漏斗推进一个严格单调水位、later-valid 合并、cursor 携带身份。PR 超出独立方案之处仍是失败模式纪律:每一轮 review 得到的是以其命名的 commit 而不是辩解,携带机制超限时降级为至多一次重复而非报错,文档写明残余重复模式而不是藏起来。作者选择把 @ytahdn 的最后一条建议记录为后续跟进、而不是再推一次 commit 使两份签核作废——出于同样的理由,这是对的。若六个月后由我维护,共享的 laterActivityTimestamp helper 与限定范围的协议文档保证是我会感谢作者的部分。

我批准是因为证据是肯定的,而不是因为提不出反对意见:问题真实存在且有已合入代码佐证,diff 最小且与其目标一致,CI 全绿,最有资格判断它的两个人类都已在同一 commit 上签核。合入机制上的一条提醒:@wenshao 立于 ba3f9e2e 的修改要求在评审状态上仍然正式存在,尽管他的 round-5 审查已无新发现——只有他能将其撤销。撤销后,本次批准与 @ytahdn 的批准即凑齐分支保护所要求的两票。

Qwen Code · qwen3.8-max

Reviewed at 731d9b0c36a1a803d8d420c20513fb944f937849 · re-run with @qwen-code /triage

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 18, 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: 48 passed · 0 failed · 48 total

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

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

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

Verification report

PR #9396 verification — feat(serve): Add live-state session activity watermark

Verdict: merge-ready — 48/48 scripted assertions passed (0 unexpected failures), verified head 90aa85af2bb941b8b804d4b6d1d7e06e627b881d (merge-ref HEAD^2; matches the snapshot's headRefOid). Findings below are non-blocking suggestions.

中文摘要
  • 结论merge-ready。48/48 脚本化断言通过,0 个意外失败;验证头 90aa85af
  • A/B 结论:中心主张被证明 load-bearing。head 上 PR 新增/修改的 11 个测试单元全绿(bridge 水位 6 项、live-state 路由投影 3 项、列表合并 1 项、SDK 类型形状 1 项);同一批测试在 base(HEAD^1)上 11/11 按预期变红——bridge 6 项全部 expected undefined to be defined,合并测试在"持久化 mtime 更新"单元上回退为 live 优先(12:00:01 顶替 12:00:05,行向后移动),SDK tscTS2344 updatedAt: Actual: never;base 上其余 1124 个既有测试全绿。突变矩阵:删除水位推进(M1)与去掉 +1ms 逻辑时钟(M2)均被精确捕获;>=>(M3)与"先发布后写水位"(M5)为无害幸存者(见下)。
  • Findings(均不阻塞):① 拆除路径(close/kill/shutdown/crash flush)的水位推进是 write-only——四条 flush 路径都在任何公开读可达之前删除 entry,设计文档转换矩阵"flush 推进一次"一行及测试计划中"teardown 路径各推进一次"的测试无法经公开接口钉住;可观测残余(恰好一次终态、行消失)已由我的 H3 钉住。② 设计测试计划两项未随附测试:live-only 列表排序/游标使用填充后的时间戳、Live Task 游标随 running 终态前进;两者均为设计内的预期行为且已结构性核验代码路径。③ M3/M5 幸存者经裁定无害(>=/> 在双方均为规范 toISOString 字符串时不可区分;写-先于-发布由同步代码顺序保证,异步事件总线使函数内重排不可观测)。
  • 未覆盖:逐 commit 归因(depth-2 shallow,仅聚合 diff 可验);真实 daemon E2E 与 Web Shell 消费(PR 设计明确排除);REST 路由级 deadline(改用 bridge 级 deadlineMs);live-only 路由端点级排序(仅合并级排序被钉住)。

Scope

Central claim — the bridge advances a strictly monotonic per-session watermark exactly once per prompt terminal that reached running (written before the terminal is published), projects it as the already-typed optional BridgeSessionSummary.updatedAt, and the workspace live-state route returns it.

Secondary claims — (1) full workspace session lists merge live and persisted activity timestamps keeping the later valid value, so an async transcript write can never move a row backward; (2) turn activity never advances the catalog version, and queued-only terminals, heartbeats, and admission never advance the watermark.

A/B load-bearing proof

Base arm = scratch worktree at HEAD^1 (a18b080387) with the PR's four changed test files overlaid verbatim from the merge commit; head arm = the merge checkout. Both arms share the root node_modules (the PR touches no package.json/lockfile) with the per-package nested node_modules symlinked into the worktree (the base worktree initially failed collection on ajv/dist/2020.js and the build-generated src/generated/git-commit.js — both environment gaps, fixed by link/copy; neither is part of the change under test). Realpath assertion for control purity: readlink -f node_modules/@qwen-code/qwen-code-core/__w/qwen-code/qwen-code/packages/core, and the PR's diff contains zero changes under packages/core; the cli vitest aliases (@qwen-code/qwen-code-core, @qwen-code/acp-bridge/*) resolve to worktree-local sources, so each arm tests its own code.

Cell Oracle head base
Bridge watermark suite (6 tests) getSessionSummary().updatedAt semantics 6 green 6 red, all expected undefined to be defined (behavioral, not collection)
live-state route exact-object projection (2 tests) HTTP body deep-equal incl. updatedAt green red — key absent from response
live-state selected-workspace isolation (1 test) per-bridge updatedAt value green red — undefined vs 2026-07-08T00:03:00.000Z
Full-list merge, 4 cells (1 test) merged updatedAt via real listWorkspaceSessionsForResponse + on-disk transcript green red — backward regression cell: live-first keeps 12:00:01 over newer mtime 12:00:05
SDK DaemonSessionLiveState shape (1 type assertion) tsc --noEmit -p tsconfig.test-fence.json green red — TS2344 ... updatedAt: "Expected: string, Actual: never"
Non-PR tests in the same two cli files 1124 tests green green (control: overlay perturbs nothing else)

Witnesses: 01-ab-head-bridge-watermark-green.png, 02-ab-base-bridge-watermark-red.png (the base run shows the six reds beside green control tests from the same file), 03-ab-base-cli-merge-red.png.

Regression gates at head: packages/acp-bridge full package 31 files / 1588 passed; packages/cli/src/serve full directory 159 files / 5170 passed, 1 skipped (pre-existing skip) — the live-only-list and Live Task bystander surfaces show no breakage; repo-wide npm run typecheck green; eslint + prettier clean on all 8 changed files, both gates proven live (planted unused variable → @typescript-eslint/no-unused-vars exit 1; planted formatting break → prettier exit 1; both reverted).

Independent harness (mock-free, real code paths)

Canonical copies in this artifact dir (watermark-harness.test.ts, merge-harness.test.ts); runnable copies were placed in the packages during the round and removed afterwards. Bridge cells drive the real createAcpSessionBridge through the in-memory ACP channel seam; merge cells drive the real listWorkspaceSessionsForResponse against real on-disk transcripts. All 7 green at head (04-head-harnesses-green.png):

  • H1 happens-before: a subscriber reading the summary synchronously inside the turn_complete delivery sees the new watermark — the client-visible half of "written before published".
  • H2 deadline sibling: a wedged prompt with deadlineMs: 60 advances exactly once at the prompt_deadline_exceeded terminal; a late natural result afterwards neither publishes a second terminal nor advances again (transition-matrix row exercised end-to-end).
  • H3 teardown sibling: a crash flush publishes exactly one terminal for the running prompt and the row disappears (see Finding 1 for why the advance itself is unobservable there).
  • H4 forward jump + correction: with the clock mocked to T, T+100 s, T, the watermark is T, T+100 s, T+100 s+1 ms — strictly increasing, never corrected downward (the accepted tradeoff, now pinned).
  • M-TIE / M-ORDER / M-LIVE: same-millisecond live/persisted keeps the instant; a fresh live watermark reorders the merged list; a live-only row carries the watermark through the insertion path.

Mutation matrix

Applied in a scratch worktree at the merge commit; positive controls are M1/M2 (caught). Raw log: logs/mutation-matrix.txt; witness: 05-m2-mutant-tiebreaker-red.png.

Mutant Guard Shipped suite Harness Adjudication
M1: delete advanceTurnActivity(entry) watermark advance 6/6 red H1/H2/H4 red; H3 green (pins terminal, not advance) killed
M2: drop Math.max(now, prev+1) +1 ms logical tie-breaker exactly the pinned-clock test red exactly H4 red killed
M3: >=> in laterActivityTimestamp tie preference green green, incl. M-TIE survivor — dead distinction: both candidates are built with new Date(...).toISOString(), so equal instants are byte-identical strings; >= and > cannot disagree on any production input
M5: advance moved after the broadcast write-before-publish order green green survivor — unobservable by construction: the EventBus delivers through bounded async queues, so any reordering inside the synchronous publishPromptTerminal still lands before every subscriber read; the invariant is held by synchronous code order (H1 pins the client-visible guarantee)

No mutant regressed a killed cell to survived; the unmutated control is green.

Findings (non-blocking)

F1 — the teardown-flush watermark advance is write-only (Suggestion, doc/plan accuracy). All four flush paths remove the session entry from the bridge map before any public read can observe the advanced value: closeSession and killSession call byId.delete(...) before flushPromptTerminals; shutdown likewise; the crash path flushes first but deletes before the asynchronously-delivered event reaches any subscriber (my first H3 attempt read getSessionSummary inside the handler and got SessionNotFoundError — that failed probe is the evidence). Consequently the transition-matrix row "flushed during close, kill, crash, or shutdown → advance once" and the design test-plan item "teardown terminal paths each advance exactly once" describe an advance no client, route, or test can ever read; the row disappears with the entry, exactly as the protocol's failure semantics promise. The observable residue (exactly-once terminal, row removal) is real and is pinned by H3. No code change needed; a one-line note in the design doc (or test plan) that the flush advance is bookkeeping-only would close the gap.

F2 — two design-test-plan items ship unpinned (Suggestion, coverage). (a) "Live-only session list and cursor keys use the populated timestamp" — the behavior change is real and intended (getSummaryActivityTime at session-list.ts:561 reads updatedAt ?? createdAt and feeds the live cursors at :570/:886/:904/:1100/:1110), but no shipped test exercises live-only ordering with a populated watermark. (b) "Live Task cursor changes after a running terminal" — structurally verified (live-task-service.ts:585 and :649-650 derive thread updatedAt from bridge.getSessionSummary(...).updatedAt, :515 sorts by it) but unpinned. Both are the PR's own audit table rows; adding two small tests would make the plan's enumeration fully covered.

F3 — mutation survivors classified (completeness, not merge conditions). M3 and M5 above are adjudicated harmless with independent evidence (M-TIE run under M3; M5 run under both suites plus the async-bus argument). Neither is dead code in a risky sense: M3's >= documents the live-wins tie policy, M5's order is the causal contract. Listed so the author knows the suite's exact pinning surface.

Not covered

  • Per-commit attribution: the checkout is depth-2 (git rev-list HEAD^1..HEAD^2 returns 1 of the snapshot's 3 commits); the aggregate HEAD^1..HEAD diff was verified, the two docs commits' content verified as the committed design doc, but per-commit behavior attribution was out of reach.
  • Real-daemon E2E and Web Shell consumption — explicitly out of scope by the PR's design; the bridge-level harness used deadlineMs directly instead of the REST deadline route.
  • Live-only route endpoint ordering — pinned only at the merge level (M-ORDER); the live-only list endpoint itself was not driven with populated watermarks (see F2).
  • The 1 skipped test in the full serve run is pre-existing and unrelated.
  • Harness development iterations (not PR defects): the first H3 used shutdown() (unreadable by design) and the first H4 used mockReturnValueOnce chains consumed by internal Date.now() calls; both were fixed before the final green runs.

Methodology

Environment: CI verify container (node:22-bookworm), merge-ref checkout at depth 2, npm ci + npm run build pre-run at head. A/B used scratch worktrees under tmp/ (removed after capture); the base arm reused the root dependency tree — clean because the PR changes no manifest — with nested per-package node_modules symlinked in and the realpath check quoted above; vitest aliases keep cross-workspace imports worktree-local. Harnesses are vitest specs importing the production modules directly (in-memory ACP channel and real on-disk transcripts as the only fakes, i.e. fakes at the peer boundary, never in the code under test). Mutation runs applied/reverted via mutate.mjs in a third worktree. Terminal captures via scripts/verify-capture.mjs (non-TTY, --reporter=verbose); raw logs in logs/. Assertion counting: one entry per executed test cell / gate / harness expectation / mutation outcome, with expected-red A/B cells and expected-survivor mutants counted as passes per the contract; full counts in assertions.json.

Evidence images

01-ab-head-bridge-watermark-green

02-ab-base-bridge-watermark-red

03-ab-base-cli-merge-red

04-head-harnesses-green

05-m2-mutant-tiebreaker-red

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

Qwen Code · sandboxed verification

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

Comment thread packages/acp-bridge/src/bridge.test.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread docs/developers/qwen-serve-protocol.md
Comment thread packages/acp-bridge/src/bridge.ts

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

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-2 downstream-surface test deliverables unshipped — already reported at packages/acp-bridge/src/bridge.ts:3573 (comment 3803671629)
  • R1-4 docs/users/qwen-serve.md stale status-route claim — already reported at packages/acp-bridge/src/bridge.ts:3573 (comment 3803671629)
中文说明

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

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/cli/src/serve/server/session-list.ts Outdated

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

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R1-3 design-doc-promised watermark tests missing (deadline/teardown/duplicate-suppression/clock-jump) — already reported at packages/acp-bridge/src/bridge.test.ts:17693 (comment 3803671612, by qwen-code-ci-bot)

Not explored to full depth (tool budget reached): "agent 2": none — no check was cut short..

中文说明

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未探索到全部深度(达到工具调用预算):"agent 2"none — no check was cut short.

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

Comment thread packages/cli/src/serve/server/session-list.ts
Comment thread packages/cli/src/serve/server/session-list.ts
doudouOUC and others added 2 commits August 18, 2026 20:33
read_thread and wait_threads read the bridge summary directly, so once the
summary began carrying the running-turn watermark their fallbacks stopped
consulting the persisted transcript timestamp. Because the recorder writes
asynchronously, one task could report a later recency from the thread list than
from a thread read, and a wait cursor keyed on the live value alone stopped
changing when only the transcript advanced, so a consumer waiting for that
flush saw an unchanged cursor and exited early. Move the merge rule into a
shared helper and apply it at all three read points, including the revision
fallback used for a session with no attached client.

Add the watermark cases the design doc enumerates but the previous commit did
not ship: the deadline path publishes its terminal twice and must still advance
exactly once, a corrected forward clock jump must never decrease the value, and
a clock that advances between terminals must be reported instead of the logical
tie-breaker. Cover the single-session status route's verbatim pass-through of
the field, and cover the helper's both-invalid tail directly because no route
can supply two invalid candidates.

Correct two design-doc test-plan claims that did not match the code: the
teardown paths advance a watermark no consumer can read, because the entry
leaves live state in the same operation, and the duplicate deadline terminal
comes from the raced rejection reaching the settle handler rather than from a
late agent result.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
The organized view applied the live merge only on the first page, so page 1
sorted rows and encoded its cursor from merged activity keys while later pages
keyed the same rows by persisted mtime alone. That was harmless while bridge
summaries never carried an activity timestamp, because both keys were the mtime.
Now that a settled turn advances a watermark that leads storage until the
recorder flushes, a live row ordered onto page 1 by its watermark falls behind
the page-1 cursor boundary on page 2 and is returned a second time, displacing a
genuinely new row. Merge live state on every page so both pages key rows the
same way; a live-only row still has no persisted key to page by and stays a
first-page insertion.

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

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

Partially reviewed — gaps disclosed.

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

Not explored to full depth (tool budget reached): "agent 3a": none — no checks were cut short..

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

  • packages/sdk-typescript/test/unit/daemon-public-surface.test.ts:446 — [test] SDK public-surface assertion for updatedAt is enforced only by typecheck, not the test runner
中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent 3a"none — no checks were cut short.

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

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

Comment thread packages/cli/src/serve/server/session-list.ts
Comment thread packages/cli/src/serve/server/session-list.ts

@wenshao wenshao 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): "agent 6a": none — no check was cut short..

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

  • packages/cli/src/serve/server/session-list.ts:407 — [review] Default session list: cross-page ordering divergence between mtime partition and merged-key sort
  • packages/sdk-typescript/test/unit/daemon-public-surface.test.ts:446 — [test] SDK public-surface assertion for updatedAt is inert at test-run time
中文说明

未探索到全部深度(达到工具调用预算):"agent 6a"none — no check was cut short.

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

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

Comment thread packages/cli/src/serve/server/session-list.ts
@danialzivehdadr

This comment has been minimized.

An activity key merges the bridge's in-memory watermark, so it is not a
stable property of a row: when a live entry retires mid-pagination the key
regresses to the transcript mtime, and a live-only row that persists mid-pass
re-enters the scan keyed by its first flush. Either way a row already emitted
on an earlier page could pass the strictly-older cursor filter again and
displace a genuinely new row, which was structurally impossible while activity
keys came from mtime alone.

The organized and metadata activity cursors now carry the identities already
emitted at a live-derived key, and the after-cursor filter excludes them, so
one pass returns a session at most once. The list prunes itself: an identity
is dropped once its persisted floor alone can no longer pass the key filter or
once the row leaves the filtered collection while not live. Past a 64-identity
cap the highest floors are dropped first, degrading to the previous at-most-
once duplicate instead of failing the pass. Cursors minted before the field
existed stay valid, and the field is omitted when empty.
@doudouOUC
doudouOUC requested a review from wenshao August 18, 2026 16:50
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ incomplete — infrastructure failure - workflow run

The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details.

中文 — 判定:⚠️ 未完成 · 基础设施故障

验证作业未完成(检出、runner 或初始化错误),未生成报告。详见工作流运行日志。

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 81ff5998fbd0e34fd4e300c6b765ac940e24b830 — 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.

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

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ incomplete — infrastructure failure - workflow run

The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details.

中文 — 判定:⚠️ 未完成 · 基础设施故障

验证作业未完成(检出、runner 或初始化错误),未生成报告。详见工作流运行日志。

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): chunk 5: could not execute the new/changed unit tests (live-task-service.test.ts, multi-workspace-sessions.test.ts, server.test.ts) — the review worktree has no node_mod….

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

  • packages/acp-bridge/src/bridge.ts:2027 — [review] watermark-before-broadcast happens-before is unpinned by any test
  • docs/design/2026-08-18-workspace-session-live-state-updated-at.md:302 — [review] 'same effective recency' claim falsified by the bare-watermark surfaces
  • docs/design/2026-08-18-workspace-session-live-state-updated-at.md:531 — [review] Test Plan stability promises (attach/detach, interaction wait, streamed updates) unpinned
  • docs/design/2026-08-18-workspace-session-live-state-updated-at.md:556 — [review] Test Plan 'Live Task cursor stable across heartbeats' half untested
  • docs/developers/qwen-serve-protocol.md:2312 — [review] live-state refresh guidance omits the keep-the-later rule (SDK doc twin)
  • packages/cli/src/serve/server.test.ts:15815 — [review] 'watermark leads mtime' test under-proves its title; no discriminating every-page-merge geometry
  • packages/cli/src/serve/server/activity-timestamp.ts:19 — [review] module docstring overclaims cross-route recency consistency
中文说明

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

未探索到全部深度(达到工具调用预算):chunk 5:could not execute the new/changed unit tests (live-task-service.test.ts, multi-workspace-sessions.test.ts, server.test.ts) — the review worktree has no node_mod…

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

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

Comment thread packages/cli/src/serve/server/session-list.ts Outdated
Comment thread packages/cli/src/serve/server/session-list.ts Outdated
Comment thread packages/cli/src/serve/server.test.ts
Comment thread packages/cli/src/serve/server/session-list.ts Outdated
Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
Comment thread docs/developers/qwen-serve-protocol.md Outdated
Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
Comment thread packages/cli/src/serve/server.test.ts Outdated
…nation

The emitted-identity carry could still drop a carried session mid-pass and
re-admit it later: an identity absent from a page's collection was discarded
even though absence can be transient (pre-flush TTL cache, mid-pass group
movement), organized re-entry was evaluated under the row's current pin state
only, and the live-only cursor key could move backward when a wall-clock
rollback landed the first watermark behind createdAt.

Retain absent carried identities at a negative-infinity floor, test organized
re-entry under both pin states, and floor the first watermark advance at the
entry's createdAt. Extend the retire test to a three-page pass so carried-set
propagation through an intermediate cursor is pinned, probe scan visibility in
the mid-pass-flush tests, and cover the live-list-failure and unpin paths.
Scope the at-most-once pagination wording in the design and protocol docs to
what the carry actually guarantees.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

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

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

  • packages/acp-bridge/src/bridge.test.ts:17735 — [probe] watermark-before-broadcast happens-before unpinned by any test
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

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

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

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

Comment thread docs/design/2026-08-18-workspace-session-live-state-updated-at.md Outdated
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ incomplete — infrastructure failure - workflow run

The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details.

中文 — 判定:⚠️ 未完成 · 基础设施故障

验证作业未完成(检出、runner 或初始化错误),未生成报告。详见工作流运行日志。

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 731d9b0c36a1a803d8d420c20513fb944f937849 — 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.

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

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

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

@wenshao

wenshao commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Runtime verification report — LGTM ✅

I built a real end-to-end environment for this PR and ran the reviewer test plan against it rather than reading the diff. Everything in the plan holds; 59/59 runtime assertions pass on the shipped build, the PR's own suites are green (1923 tests), and every production hunk is load-bearing (9/9 source mutations killed, plus 4 compiled-dist A/B arms that reproduce the exact bugs the follow-up commits fix).

Environment

AFTER arm PR head 731d9b0c36
BEFORE arm merge-base 2af4f903de (feat(web-shell): Consume workspace session live-state (#9366))
Harness two real qwen serve daemons (own ports, own HOME, shared workspace + seeded session corpus), each spawning a real qwen --acp child; an OpenAI-compatible mock upstream with SLEEP:n hold, FAILTURN and a write_file tool-call mode so I can park a turn in running, fail one, or park one in isWaitingForPermission on demand
Driving raw REST + the real /session/:id/events SSE stream + the typed DaemonClient from packages/sdk-typescript
Platform Linux, Node 22.22.2

Both arms were built from source into real dist artifacts (core → channel-base → acp-bridge → cli → sdk).

Reviewer test plan — item by item

# Plan item Result
1 updatedAt is optional + additive under v: 1 and the existing capability; no new route / gate / readiness feature PASS — envelope keys are still exactly {v, catalogVersion, sessions}; row keys are the 5 pre-existing fields plus updatedAt; the key is absent (not null) when there is no watermark; the advertised capability surface is identical between the two arms (114 features on both, zero added, zero removed, same protocolVersions); trust/selector gate still 400 workspace_mismatch for a non-path selector and for an unregistered absolute path
2 Advances exactly once, only on a running prompt's formal terminal; written before the terminal is published; strictly increasing under same-ms terminals / rollback / corrected forward jump; never driven by admission, streaming, queue-only cancellation, heartbeat or interaction waits PASS — see the detail below
3 Ordinary turn activity leaves generation + revision, cache invalidation, runtime ownership, trust checks, status codes and memory-only behaviour unchanged PASS — 5 turns with no session churn: {generation, revision} byte-identical before and after, while the watermark advanced; a real catalog change still bumps revision (control). live-state returned only the 2 live rows and never any of the 10 persisted-only rows; mean latency 0.7 ms vs 5.2 ms for the full catalog
4 Live/persisted merge keeps the later valid timestamp; absent/unparseable candidates preserve the other valid value; the live-state route still projects only the bridge-local value PASS — both directions exercised on a real session, and all 10 edge cases driven against the shipped compiled laterActivityTimestamp()
5 SDK exports the optional field; GET /session/:id/status returns the bridge-local watermark without merge; the design doc's compatibility / consumer / test / rollout claims still hold PASSDaemonClient.getWorkspaceSessionLiveState() decodes the field with real types against the live daemon; with the transcript mtime pushed 90 s ahead, the list reports the mtime while /session/:id/status still reports the (earlier) bridge value, exactly as the protocol doc now says
Item 2 in detail — what does and does not advance the watermark (all observed on the wire)
Trigger Observed
session admission (POST /session) no updatedAt
POST /session/:id/heartbeat no updatedAt
mid-stream, prompt held in running for 1.5 s no updatedAt
queue-only cancellation — queue was [7c701569:running 898592f3:queued], deleted the queued one no updatedAt
interaction wait — isWaitingForPermission: true on a real write_file approval, held 1.5 s unchanged
running prompt settles (turn_complete) advances
running prompt fails (upstream 400, turnError retained) advances
running prompt cancelled advances (matches the doc's "success, error, cancellation, and deadline alike")
7 consecutive terminals 7 strictly increasing values
session idle for 1.5 s no drift

Write-before-publish was tested as a genuine race, not by reading the code: I subscribe to GET /session/:id/events, and the instant the turn_complete frame lands I issue the live-state request. The watermark is already there.

Clock safety was tested by driving the shipped compiled advanceTurnActivity() out of packages/acp-bridge/dist/bridge.js (re-exported verbatim — nothing reimplemented) with Date.now under test control: 5 terminals inside one millisecond → …000, …001, …002, …003, …004 (the tie-break really is +1 ms, not a duration); a 60 s backward step still increases; a forward jump is adopted and never corrected back down; the first advance floors at createdAt; an unparseable createdAt degrades to wall time rather than NaN.

Lifecycle: after a daemon restart the generation changes, a session restored with POST /session/:id/load carries no live watermark while the full catalog still reports its persisted recency, and the next running terminal re-establishes it.

Evidence

Watermark semantics

Isolation, clock safety, merge helper

Dist-mutation A/B

Suites and mutation teeth

Are the extra guards load-bearing?

The base build never populates updatedAt, so a plain BEFORE/AFTER diff cannot show what the merge rule and the two cursor guards are for. I therefore built the counterfactual instead: the same feature, with one guard reverted in the compiled dist, and re-ran the same real scenario. All four arms reproduce a concrete user-visible defect that the shipped build does not have.

Reverted hunk Real-daemon consequence
laterActivityTimestamplive.updatedAt ?? existing.updatedAt the row reports a 90-second-stale recency and moves backward in activity order
every-page live merge + emitted-identity cursor (both) a size=1 organized walk returns the boundary row twice (11 rows, 10 distinct)
emitted-identity cursor only, live entry retired mid-pass same duplicate (11 rows, 10 distinct)
emitted-identity cursor only, sourceType-filtered walk, live entry retired mid-pass same duplicate on the second guarded path (16 rows, 15 distinct)

One measurement is worth calling out because it changes how these guards should be read: on this machine the transcript mtime consistently lands 3–8 ms before the terminal that mints the watermark. So "live watermark leads storage" is not a rare race — it is the ordinary post-turn state of every session, which means the duplicate-row hazard would have been hit routinely, not occasionally. The three follow-up commits are not over-engineering.

Test suites and mutation teeth

All six suites the PR touches are green on the PR build: bridge.test.ts 749, server.test.ts 1021, multi-workspace-sessions.test.ts 118, live-task-service.test.ts 17, activity-timestamp.test.ts 3, daemon-public-surface.test.ts 15 — 1923 passed, 0 failed.

To check the new coverage actually bites, I reverted each production hunk in source and re-ran the mapped suite. 9/9 killed, including the ones that are easy to leave untested: the queued-only gate, the createdAt floor, the metadata-path cursor exclusion, and the Live Task read path.

One note for future refactors: the live-state route's updatedAt projection is covered only by multi-workspace-sessions.test.ts. Deleting those six lines leaves all 1021 tests in server.test.ts green — worth knowing before someone adds a response projection or field whitelist there.

Cursor compatibility (checked explicitly, since it is a wire change)

  • The page-1 cursor decodes to {group, archiveState, last, emitted:[…]} and names the live-derived row it just emitted.
  • A legacy cursor with no emitted key at all is still accepted → 200.
  • Malformed emitted ([123], [""], "nope", {}) is rejected with 400 invalid_cursor rather than silently ignored.
  • A full 64-identity cursor is accepted and is 879 URL characters — comfortably inside any URL/header limit.

Not covered by this verification

Web Shell request-count reduction (out of scope by design), sustained turn-churn performance, multi-daemon runtime replacement beyond a plain restart, and Windows/macOS — this run is Linux only. The residual duplicate the protocol doc already calls out (a persisted-only row unpinned between page fetches) is unchanged from before this PR and correctly documented.

Recommendation: merge. The contract is implemented as designed, the additive field is genuinely additive, the guards are all load-bearing, and the documentation matches the observed behaviour line for line.

中文版报告

运行时验证报告 — 建议合并 ✅

我没有只看 diff,而是在本地搭建了真实的端到端环境,按 Reviewer 测试方案逐条跑了一遍。结论:方案中的每一条都成立,shipped 构建上 59/59 条运行时断言全部通过,PR 自带的测试套件全绿(1923 条),并且每一处生产代码改动都是"有承重作用"的(9/9 源码变异全部被杀,另有 4 个编译产物 A/B 臂精确复现了后续 commit 所修复的那几个 bug)。

环境

AFTER 臂 PR head 731d9b0c36
BEFORE 臂 merge-base 2af4f903defeat(web-shell): Consume workspace session live-state (#9366)
harness 两个真实 qwen serve daemon(各自端口、各自 HOME,共享同一 workspace 与同一份预置会话语料),各自拉起真实的 qwen --acp 子进程;OpenAI 兼容 mock 上游支持 SLEEP:n 挂起、FAILTURN 失败、以及 write_file 工具调用模式,因此可以按需把一次 turn 停在 running、让它失败、或者停在 isWaitingForPermission
驱动方式 原始 REST + 真实的 /session/:id/events SSE 流 + packages/sdk-typescript 里带类型的 DaemonClient
平台 Linux,Node 22.22.2

两个臂都从源码构建成真实 dist 产物(core → channel-base → acp-bridge → cli → sdk)。

Reviewer 测试方案逐条结果

# 方案条目 结果
1 updatedAtv: 1 与既有 capability 下的可选增量字段;不新增路由 / feature gate / readiness feature 通过 —— 响应外层键仍然恰好是 {v, catalogVersion, sessions};行内键 = 原有 5 个字段 + updatedAt;没有水位时该键是缺失而不是 null;两个臂对外广播的 capability 完全一致(各 114 个 feature,零新增零删除,protocolVersions 相同);trust/selector 网关对非路径选择器与未注册的绝对路径仍返回 400 workspace_mismatch
2 仅在到达 running 的 prompt 赢下正式终态时恰好推进一次;写入发生在终态事件发布之前;同毫秒终态 / 时钟回拨 / 修正后的向前跳跃下仍严格递增;admission、streaming、仅排队取消、heartbeat、交互等待都不推进它 通过 —— 明细见下
3 普通 turn 活动不改变 generation + revision、缓存失效、runtime 所有权、trust 检查、状态码与纯内存行为 通过 —— 无会话增删的 5 次 turn:{generation, revision} 前后完全一致,而水位确实推进了;真正的目录变更仍会 bump revision(对照组)。live-state 只返回 2 个 live 行,10 个仅持久化的行一个都没出现;平均延迟 0.7 ms,全量目录 5.2 ms
4 live/持久化合并取较晚有效值;缺失或无法解析的一侧不顶替另一侧的有效值;live-state 路由仍只投影 bridge-local 值 通过 —— 在真实会话上双向都验证过,10 个边界用例直接打在 shipped 编译产物的 laterActivityTimestamp()
5 SDK 暴露该可选字段;GET /session/:id/status 直接返回 bridge-local 水位(无合并);设计文档的兼容性 / 消费者 / 测试 / 发布结论在实现中依然成立 通过 —— DaemonClient.getWorkspaceSessionLiveState() 以真实类型对着运行中的 daemon 解出该字段;把 transcript mtime 推后 90 秒后,列表返回 mtime,而 /session/:id/status 仍返回(更早的)bridge 值,与新版协议文档描述完全一致
第 2 条明细 —— 什么会 / 不会推进水位(全部为线上观测)
触发 观测结果
会话准入(POST /session updatedAt
POST /session/:id/heartbeat updatedAt
prompt 停在 running 流式输出中 1.5 秒 updatedAt
仅排队取消 —— 队列为 [7c701569:running 898592f3:queued],删除排队那条 updatedAt
交互等待 —— 真实 write_file 审批下 isWaitingForPermission: true,保持 1.5 秒 不变
running prompt 正常结束(turn_complete 推进
running prompt 失败(上游 400,turnError 被保留) 推进
running prompt 被取消 推进(与文档 "success, error, cancellation, and deadline alike" 一致)
连续 7 次终态 7 个严格递增的值
会话空闲 1.5 秒 不漂移

"先写水位、后发终态"是按真实竞态测的,不是读代码得出的:我订阅 GET /session/:id/events,在 turn_complete 帧落地的那一刻立刻发起 live-state 请求,水位已经在了。

时钟安全性是直接驱动 packages/acp-bridge/dist/bridge.jsshipped 编译产物advanceTurnActivity()(原样再导出,没有任何重写实现),并接管 Date.now:同一毫秒内 5 次终态 → …000, …001, …002, …003, …004(tie-break 确实是 +1 ms 而不是一段时长);回拨 60 秒后仍然递增;向前跳跃被采纳且之后不会被向下修正;首次推进以 createdAt 兜底;createdAt 无法解析时退化为 wall time 而不是 NaN

生命周期: daemon 重启后 generation 变化;用 POST /session/:id/load 从磁盘恢复的会话没有 live 水位,而全量目录仍报告它的持久化 recency;下一次 running 终态会重新建立水位。

关键截图

见上方英文部分的四张图(分别是:水位语义、隔离性与时钟安全性、编译产物变异 A/B、测试套件与变异测试)。

那些额外的守卫真的有用吗?

base 构建根本不会填充 updatedAt,所以单纯 BEFORE/AFTER 对比看不出合并规则和两个游标守卫是干什么的。于是我构造了反事实:同一个功能,但在编译产物里回退掉其中一个守卫,再跑同一个真实场景。四个臂全部复现了 shipped 构建不存在的、用户可见的具体缺陷。

被回退的改动 真实 daemon 上的后果
laterActivityTimestamplive.updatedAt ?? existing.updatedAt 该行报告过时 90 秒的 recency,活动排序回退
每页 live 合并 + emitted 身份游标(两者同时回退) size=1 的 organized 分页把边界行返回了两次(11 行,10 个不同)
仅回退 emitted 身份游标,分页中途 live 条目退场 同样重复(11 行,10 个不同)
仅回退 emitted 身份游标,sourceType 过滤分页,中途 live 条目退场 第二条被守卫的路径上出现同样重复(16 行,15 个不同)

有一个测量结果值得单独说,因为它改变了这些守卫的定位:在这台机器上,transcript mtime 稳定地比产生水位的那次终态早 3–8 毫秒。也就是说"live 水位领先存储"并不是罕见竞态,而是每个会话每次 turn 之后的常态——意味着重复行的问题会被经常性触发,而不是偶发。那三个后续 commit 不是过度设计。

测试套件与变异测试

PR 触及的六个套件在 PR 构建上全绿:bridge.test.ts 749、server.test.ts 1021、multi-workspace-sessions.test.ts 118、live-task-service.test.ts 17、activity-timestamp.test.ts 3、daemon-public-surface.test.ts 15 —— 1923 通过,0 失败

为了确认新增覆盖真的有牙齿,我在源码层面逐个回退生产改动并重跑对应套件,9/9 全部被杀,包括那些最容易漏测的点:仅排队终态的门禁、createdAt 兜底、metadata 路径的游标排除、以及 Live Task 读路径。

给后续重构留一条提示:live-state 路由里 updatedAt 的投影multi-workspace-sessions.test.ts 覆盖。把那 6 行删掉,server.test.ts 的 1021 条测试仍然全绿——将来有人给这个路由加响应投影或字段白名单之前,最好知道这一点。

游标兼容性(因为这是线上格式变更,专门验证)

  • 第一页游标解出 {group, archiveState, last, emitted:[…]},且确实点名了刚刚以 live 键发出的那一行。
  • 完全没有 emitted 键的老游标仍被接受 → 200
  • 畸形 emitted[123][""]"nope"{})会被拒绝并返回 400 invalid_cursor,而不是被静默忽略。
  • 满载 64 个身份的游标可以正常使用,URL 长度 879 字符,远低于任何 URL/header 限制。

本次未覆盖的范围

Web Shell 请求数下降(设计上属于范围外)、持续 turn 压力下的性能、除普通重启之外的多 daemon runtime 替换,以及 Windows/macOS —— 本次仅在 Linux 上运行。协议文档已经写明的残余情况(仅持久化的行在两次翻页之间被取消置顶仍可能重复)在本 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.

Partially reviewed — gaps disclosed.

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

Not reviewed: reverse audit — stopped at the 5-round plan cap without two consecutive dry rounds (round 5 still reported two confirmed findings).

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

  • packages/sdk-typescript/test/unit/daemon-public-surface.test.ts:446 — [test] SDK public-surface assertion for updatedAt is enforced only by typecheck, not the test runner
  • packages/cli/src/serve/server.test.ts:15991 — [probe] No test pins the removal (self-pruning) side of the carried-emitted-identity state machine
  • docs/design/2026-08-18-workspace-session-live-state-updated-at.md:452 — [review] Design doc's follow-up contract rule 1 claims turnCompleted carries the workspace cwd; the shipped event has no cwd field
  • packages/cli/src/serve/server.test.ts:16417 — [probe] Cap-overflow drop order between floored and floor-less (-Infinity) carried identities is unpinned by any test
中文说明

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

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

未审查:reverse audit — stopped at the 5-round plan cap without two consecutive dry rounds (round 5 still reported two confirmed findings)。

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

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

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

审查结论 / Review Verdict: ✅ 批准 / Approve

(纯静态审查,未运行测试或构建。Static review; no tests or builds were run.)

总体评价 / Overall

本 PR 为 acp-bridge 引入了按会话严格单调的活动水位线,并以可选 updatedAt 字段投影到会话列表与 SDK。审查在 head(731d9b0c)上逐点验证了核心契约:

This PR introduces a per-session strictly monotonic activity watermark in acp-bridge, projected as an optional updatedAt field onto session summaries and the SDK. Reviewed against the head commit (731d9b0); core contract points verified one by one:

  1. 恰好一次推进 / Exactly-once advance:水位线仅在进入过 running 的 prompt 赢得正式终态闩锁时推进一次;publishPromptTerminal 是唯一终态漏斗,5 个调用点(teardown flush、deadline、natural settle ×3、cancelSession)全部核对一致。
    The watermark advances exactly once when a prompt that reached running wins its formal terminal latch; publishPromptTerminal is the sole terminal funnel, and all five call sites were checked.

  2. 先写后发布 / Write-before-publish:水位线推进发生在终态向订阅者广播之前(terminalPublished 标记顺序正确)。
    The watermark is advanced before the terminal state is broadcast to subscribers.

  3. 严格单调 / Strict monotonicityMath.max(Date.now(), previous + 1) 覆盖同毫秒竞争与钟回拨;首次推进以 createdAt + 1 为下限;钟前跳不破坏单调性。
    The formula covers same-millisecond races and clock rollback; the first advance is floored at createdAt + 1; forward clock jumps do not break monotonicity.

  4. later-valid 合并一致 / Consistent later-valid merge:所有会话列表读取路径(含分页 re-enter 双固定场景)统一用 laterActivityTimestamp 合并 live 水位与持久化 mtime;archived 会话不参与合并;updatedAt 采用键缺失(而非 null)语义,SDK 类型注释明确其仅作排序信号、不作为 transcript 已 flush 的证明。
    All session-list read paths uniformly merge the live watermark with persisted mtime via laterActivityTimestamp (including pagination re-entry); archived sessions are excluded; updatedAt uses absent-key (not null) semantics, documented in the SDK types as ordering-only, not proof that the transcript was flushed.

  5. 此前讨论的四个阻塞项已在 head 上确认关闭:逐页合并、活动游标 carried identities(防止游标键回归)、dual-invalid 回退(live ?? persisted,含测试)、文档重复内容清理。
    All four previously raised blockers are confirmed closed at head: per-page merge, carried identities in activity cursors (preventing cursor-key regression), the dual-invalid fallback (tested), and the doc deduplication.

  6. catalog 不变量保持 / Catalog invariant held:目录生成与修订不因水位线推进而改变;两份协议文档与实现一致且足够完整。
    Catalog generation and revision are unchanged by watermark advances; both protocol docs are thorough and consistent with the implementation.

本次独立静态审查与维护者在同一 head 上的运行时验证(59/59 断言、9/9 变异体被杀)结论一致。
This independent static review is consistent with the maintainer's runtime verification on the same head (59/59 assertions, 9/9 mutants killed).

🟢 一个非阻塞建议 / One non-blocking suggestion

bridge.test.ts 的水位线 describe 块(8 个用例)都在异步迭代完成后才读取 updatedAt,因此“把 advanceTurnActivity 移到广播之后”的变异体仍可通过测试——即“先写后发布”的时序契约目前缺少同步订阅者视角的断言。实现本身是正确的,建议后续补一个在同步订阅回调内断言水位线已推进的用例,把该契约钉死。

The watermark describe block in bridge.test.ts reads updatedAt only after async iteration completes, so a mutant that moves advanceTurnActivity after the broadcasts would still pass — the write-before-publish timing contract lacks a synchronous-subscriber assertion. The implementation is correct; consider adding a case that asserts the watermark inside a synchronous subscriber callback to pin this contract.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks @ytahdn for the thorough static review and the approval. ✅

I verified the non-blocking suggestion against this head (731d9b0c36) and it is accurate: advanceTurnActivity runs before broadcastTurnComplete/broadcastTurnError inside the terminal funnel (write-before-publish is implemented as documented), but the watermark describe block observes events only through an async-iterator collector and reads updatedAt after the prompt settles — so a mutant that reorders the advance after the broadcasts would survive the unit suite. This is the same gap the reviewer bot recorded as its round-4 deferred probe (watermark-before-broadcast happens-before unpinned).

Recording it as a deferred follow-up rather than pushing now, per the convergence posture (round 5+, suggestion-level, implementation confirmed correct): this exact head carries both the maintainer's runtime verification (59/59 assertions, 9/9 mutants killed) and your approval, and any push would void them via stale-review dismissal and restart the multi-hour CI/bot cycle. The synchronous-subscriber assertion that pins the write-before-publish contract will go into the follow-up alongside the bot's recorded deferrals.

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ incomplete — infrastructure failure - workflow run

The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details.

中文 — 判定:⚠️ 未完成 · 基础设施故障

验证作业未完成(检出、runner 或初始化错误),未生成报告。详见工作流运行日志。

Qwen Code · sandboxed verification

@wenshao wenshao 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 reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): chunk 7: run the three new server.test.ts cases end-to-end — the review worktree has no installed workspace deps ( @qwen-code/channel-base fails to resolve), so the fil….

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

  • packages/cli/src/serve/server/session-list.ts:327 — [probe] metadata forged-cursor rejection untested (mutant survives)
  • packages/sdk-typescript/test/unit/daemon-public-surface.test.ts:446 — [probe] SDK public-surface updatedAt assertion inert at test-run time; no typecheck step in PR CI
  • packages/cli/src/serve/server/activity-timestamp.ts:17 — [probe] laterActivityTimestamp docstring overclaims cross-route recency consistency
  • packages/cli/src/serve/server/session-list.ts:170 — [probe] unbounded emitted array accepted at cursor decode (encode caps at 64)
  • packages/cli/src/serve/server.test.ts:15991 — [probe] carried-identity shrink path untested (never-shrink mutant survives)
  • packages/acp-bridge/src/bridge.test.ts:17693 — [probe] per-session watermark isolation untested (bridge-scoped hoist mutant survives)
中文说明

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

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

未探索到全部深度(达到工具调用预算):chunk 7:run the three new server.test.ts cases end-to-end — the review worktree has no installed workspace deps ( @qwen-code/channel-base fails to resolve), so the fil…

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

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@doudouOUC
doudouOUC dismissed wenshao’s stale review August 19, 2026 03:31

Already have 2 approves, 3ks.

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 19, 2026
Merged via the queue into QwenLM:main with commit dd82ba4 Aug 19, 2026
137 checks passed
@doudouOUC
doudouOUC deleted the agent/workspace-session-live-state-updated-at-design branch August 19, 2026 03:33

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

Code Review Overview (AI Generated)

PR: #9396 feat(serve): Add live-state session activity watermark
Type: New Feature + Bug Fix (pagination correctness)
Change size: +2451/-55 across 15 files
Reviewed at HEAD: 731d9b0c36a1a803d8d420c20513fb944f937849

Findings Summary

  • Critical/Major: 0
  • Minor: 0
  • Nit: 1 (see below)

Key Observations

The implementation is architecturally clean and well-defended. The advanceTurnActivity strictly-monotonic design (Math.max(Date.now(), previous + 1)) correctly handles same-millisecond terminals, clock rollback, and forward jump/correction in all four cases. The laterActivityTimestamp helper is precisely scoped with correct tie-breaking semantics (live wins on equal instants via liveTime >= persistedTime, preventing string-encoding flip-flop across two representations of the same time). The emitted-identity cursor carry mechanism in nextEmittedSessionIds is a principled solution to the pagination duplication problem introduced by live-derived activity keys — the Number.NEGATIVE_INFINITY floor for absent and live-only sessions ensures they are retained in the carry list (prioritized against the 64-cap drop) since their re-admission risk is highest.

The design document (695 lines) provides exceptional context: transition matrix, monotonicity proof, downstream consumer audit, and test plan — all verified against the implementation.

Nit: parseEmittedSessionIds does not validate carry-list length on inbound path

nextEmittedSessionIds caps the outbound emitted-identity list at MAX_EMITTED_CURSOR_SESSION_IDS = 64, but parseEmittedSessionIds accepts arrays of any length on the inbound path. A client that modifies a base64url cursor could submit a carry list larger than 64, bypassing the design-time cap. The practical impact is low — this is an internal daemon, URL/query-parameter length limits (~2 KB for 64 UUIDs at 36 chars each) provide a physical bound, and the worst outcome is a client excluding their own sessions from a paginated result. Nonetheless, making the invariant symmetric would be tighter:

if (
  !Array.isArray(value) ||
  value.length > MAX_EMITTED_CURSOR_SESSION_IDS ||
  value.some((id) => typeof id !== 'string' || id.length === 0)
) {
  return undefined;  // cursor rejected
}

Cross-Validation

Phase 1 (blind review) completed before reading prior reviews. Phase 2 cross-validation against design-review by doudouOUC, five automated probe rounds by qwen-code-ci-bot, one round by wenshao, and approval by ytahdn.

Finding Reviewer My Assessment
Organized-list every-page merge not applied qwen-code-ci-bot Confirmed fixed — commit ba3f9e2; re-verified in listOrganizedWorkspaceSessionsForResponse: merge runs outside isFirstPage gate
Cursor-key regression on live-entry retirement (organized + metadata) qwen-code-ci-bot, wenshao Confirmed fixed — commit 81ff599; emittedBeforePage exclusion verified in after-cursor filter for both paths
Live-only row re-admitted after mid-pass persist qwen-code-ci-bot Confirmed fixed — commit 81ff599; row gains a persisted key and passes reenters check
Carried identity dropped on transient mid-pass absence qwen-code-ci-bot Confirmed fixed — commit be744e4; !row branch now retains at Number.NEGATIVE_INFINITY floor unconditionally
reenters evaluated under current pin state only qwen-code-ci-bot Confirmed fixed — commit be744e4; [true, false].some(...) evaluates both pin states
Date.now() half of Math.max not independently tested qwen-code-ci-bot Confirmed fixed — clock-advance-between-terminals test verifies wall-clock path; pinned-clock tests verify logical +1 path
live-task-service.ts ?? fallbacks not updated with laterActivityTimestamp qwen-code-ci-bot Confirmed fixed — commit 7af6bca; all three read points updated
Both-invalid laterActivityTimestamp fallback untested qwen-code-ci-bot Confirmed fixed — activity-timestamp.test.ts covers (undefined, 'not-a-timestamp'), ('not-a-timestamp', undefined), ('live-garbage', 'persisted-garbage'), and (undefined, undefined)
session_status verbatim pass-through untested qwen-code-ci-bot Confirmed fixed — server.test.ts "200 passes the activity watermark through verbatim" test
Design doc normative formula missing createdAt floor qwen-code-ci-bot Confirmed fixed — HEAD commit 731d9b0; formula now matches advanceTurnActivity implementation
Wire-contract "at-most-once" claim too strong qwen-code-ci-bot Confirmed scoped — be744e4 narrows claim to what the carry actually guarantees
Three-page carry-set propagation untested qwen-code-ci-bot Confirmed fixed — retire test extended to three-page pass; intermediate-cursor carry asserted
Unique-1: parseEmittedSessionIds no inbound length cap New Nit — internal daemon context, practical impact low

Additional Audit Coverage

Areas independently verified beyond existing findings:

  • advanceTurnActivity formula: Traced all four cases — (1) normal advance: Math.max(Date.now(), prev+1) where Date.now() > prev+1 → wall clock wins; (2) same-ms: Date.now() == prevprev+1 wins; (3) rollback: Date.now() < prev+1prev+1 wins; (4) forward-jump-then-correction: prev set to jump value, correction has Date.now() < prev+1prev+1 wins. First-advance floor: previous = lastTurnEndedAtMs ?? (isFinite(createdAtMs) ? createdAtMs : undefined) correctly uses createdAtMs+1 as floor when no prior watermark exists.
  • mergeLiveSessionSummary spread interaction: ...existing then ...live would set updatedAt to live.updatedAt; the explicit updatedAt: laterActivityTimestamp(...) overrides this correctly. No semantic conflict.
  • Default (numeric cursor) path deduplication safety: Only adds live-only rows on isFirstPage; paginated by storage offset for persisted rows; emitted-identity carry is not needed because no live-key-derived cursor is emitted on this path. Confirmed correct.
  • publishPromptTerminal write-before-broadcast ordering: advanceTurnActivity(entry) executes inside if (mutateTurnState) before any broadcastTurn* call. Node.js single-threaded event loop ensures the watermark is observable via getSessionSummary immediately after the terminal is published. Confirmed.
  • listLiveWorkspaceSessionsForResponse cursor stability: The live-only path sorts by updatedAt ?? createdAt. Retiring sessions disappear from the in-memory live list entirely (no persisted floor to regress to), so key regression is structurally impossible. No emitted-identity carry needed. Confirmed.
  • SDK type completeness: DaemonSessionLiveState.updatedAt?: string is verified by daemon-public-surface.test.ts type assertion. The JSDoc correctly scopes the field as "recency for ordering only" and "not proof that the transcript was flushed".
  • laterActivityTimestamp equal-instant tie: liveTime >= persistedTime (not >) correctly returns live on equal instants, preventing string-encoding flip-flop between two timezone representations of the same moment.

Final Verdict

APPROVE — The implementation is correct and complete at HEAD 731d9b0c. All Critical and Major issues found across five prior review rounds are confirmed fixed. The design handles all documented edge cases with dedicated tests. The single Nit (cursor inbound length validation) has negligible real-world impact given the internal-daemon context and URL-length physical bounds.


This review was generated by QoderWork AI

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

Independent review of the source (activity-timestamp.ts, session-list.ts, session.ts).

Approve. The watermark contract is implemented correctly:

  1. laterActivityTimestamp is correct. Date.parseNumber.isFinite gate, returns the later valid timestamp, falls back to whichever is valid (or live ?? persisted when both invalid). The mergeSummary change (updatedAt: laterActivityTimestamp(live.updatedAt, existing.updatedAt)) is exactly right — an async transcript write can never move a row backward.
  2. The emitted cursor field is the right fix for the pagination dedup hazard. A session whose watermark advances between pages can shift sort position and be emitted twice; carrying the already-emitted id set through the cursor (capped at MAX_EMITTED_CURSOR_SESSION_IDS = 64, validated as an array of non-empty strings) closes it. parseEmittedSessionIds rejects malformed values (fail-closed) and the cursor is backward-compatible (emitted only serialized when non-empty).
  3. Additive field under v: 1 with no new route/gate — matches the compatibility requirement, old clients keep the unchanged path.

No blocking findings.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants