Skip to content

feat(serve): Add workspace session live-state endpoint and catalog version - #9261

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

feat(serve): Add workspace session live-state endpoint and catalog version#9261
doudouOUC merged 9 commits into
QwenLM:mainfrom
doudouOUC:agent/workspace-session-live-state-design

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds the workspace session live-state protocol — the reviewed design at docs/design/2026-08-16-workspace-session-live-state.md — together with its implementation and documentation. A new trusted-only, memory-only GET /workspaces/:workspace/sessions/live-state route returns the complete volatile snapshot of the selected runtime's live sessions (client count, active-prompt and waiting flags) plus an in-memory catalog version: a generation + revision equality token over daemon-observed catalog membership and static-metadata changes. Clients therefore stop polling the persisted session catalog for volatile state and reload it only when the version says it may be stale, using the documented live A -> full catalog -> live B reconciliation handshake.

The bridge owns the clock: live registration and removal advance through the single emitSessionLifecycle choke point; manual rename, child automatic titles, worktree updates, and persisted branch commits (including persisted-only forks and forks committed before a failed restore) mark at exact points; REST and ACP catalog mutations share an invalidate-then-mark helper that preserves exact no-op semantics (deleted: false group deletes, removeSession: false cleanups, no-op renames). The route exposes a newly observed version only after invalidating both persisted catalog cache scopes, so a client can never accept a catalog snapshot that predates the version it observed. A new unconditional workspace_session_live_state capability advertises the route, the TypeScript SDK gains the three wire types and DaemonClient.getWorkspaceSessionLiveState / WorkspaceDaemonClient.getSessionLiveState (native REST, no per-poll capability pre-flight), and telemetry, the protocol reference, capability versioning docs, and SDK docs are updated.

Why it's needed

GET /workspaces/:workspace/sessions is a persisted catalog query, not a live-status probe: the default numeric-cursor path re-reads storage for every request (page size capped at 100), and the organized and metadata-filtered paths use a two-second cache whose TTL matches the sidebar's two-second active cadence, so steady polling repeatedly triggers full-workspace scans. Coupling small volatile updates (active, waiting, client count) to that path turns a routine sidebar refresh into a request that can time out on large session stores even while the daemon and its ACP child are healthy. E2E on this branch with a 1500-file (~150 MB) seeded store: one organized full-catalog scan ~1.0 s vs live-state ~1.1 ms across ten polls, with no ACP child spawned.

The protocol is intentionally two independent signals: a cheap complete snapshot for volatile state, and an equality token that says when the persisted catalog may be stale. Ordinary turn activity, prompt lifecycle, attach/detach, and waiting-state transitions deliberately stay out of the version — the snapshot already carries those values.

Reviewer Test Plan

How to verify

  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts -t "session catalog version clock" — 12/12 covers immutable version snapshots, the lifecycle choke point with a throwing host listener, exact rename/auto-title/worktree gates, persisted-only branch and committed-branch-restore-failure marks, no mark on failed mutations, and no marks for prompt/attach/heartbeat/permission-wait transitions; the full bridge suites are green (783/783).
  • cd packages/cli && npx vitest run src/serve/multi-workspace-sessions.test.ts src/serve/acp-http/transport.test.ts src/serve/server/telemetry.test.ts src/serve/server/session-archive.test.ts src/serve/scheduled-task-keepalive.test.ts src/serve/live/live-task-service.test.ts src/serve/create-sub-session.test.ts src/serve/server.test.ts — route shape with the exact five-field projection and Cache-Control: no-store, trusted-primary/secondary isolation, untrusted 403 before any bridge read, unknown-selector 400 with no primary fallback, transitioning-generation 503 + Retry-After, exposure-time invalidation of both catalog cache scopes with the version-comparison arm explicitly pinned, REST + ACP mutation matrices with exact deleted: false / no-op semantics, capability feature lists, telemetry label, and cleanup-path marks.
  • cd packages/sdk-typescript && npx vitest run test/unit/DaemonClient.test.ts test/unit/daemon-public-surface.test.ts — encoded workspace selector, exactly one HTTP request with no capability pre-flight, native-REST bypass of a configured ACP transport, and public-surface type pins (355/355).
  • E2E against node dist/cli.js serve (isolated runtime dir): workspace_session_live_state advertised; unknown selector returns 400; seeded persisted sessions advance the version exactly once per REST mutation (organization pin, group create/update/delete with deleted: false unchanged, archive/unarchive/delete), with the archived catalog reflecting each mutation immediately; a held streaming turn from a mock OpenAI server flips hasActivePrompt while the version is unchanged; a daemon restart changes generation and resets revision to 0; on the 1500-file seeded store, live-state stays ~1.1 ms while the organized catalog scan costs ~1 s. Note QWEN_RUNTIME_DIR must be unset or isolated when running repo vitest suites locally, since a leaked parent-session value produces phantom Storage failures.

Evidence (Before & After)

N/A — new endpoint, no existing UI surface changes.

Tested on

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

Environment (optional)

npm run build && npm run bundle, node dist/cli.js serve with an isolated QWEN_RUNTIME_DIR, and a slowed mock OpenAI server for the held-turn case.

Risk & Scope

  • Main risk or tradeoff: the required getSessionCatalogVersion / markSessionCatalogChanged methods on the exported AcpSessionBridge interface are a source-level contract change for external structural bridge implementations and complete typed test fakes (all in-repo fakes are updated). The wire protocol is additive; older daemons simply omit the capability tag, and clients discover support from it.
  • Not validated / out of scope: Web Shell adoption (separate follow-up PR implementing the client handshake, dual-catalog bundle refresh, single-flight coalescing, and background reload cooldown), Java SDK, live-state for untrusted runtimes (trusted-only by design), observation of TUI/external-process writers (documented compatibility boundary), and updatedAt/ordering staleness between version changes (documented). Windows/Linux coverage is left to CI.
  • Breaking changes / migration notes: external structural AcpSessionBridge implementers must add the two in-memory clock methods on upgrade. Conservative extra revision increments are protocol-permitted and expected on partial mutations (e.g. batch archive/delete from finally).

Linked Issues

N/A — this PR first landed its own reviewed design document (docs/design/2026-08-16-workspace-session-live-state.md) and now lands the implementation plus documentation of the same protocol.

中文说明

本 PR 做什么

本 PR 在既有评审过的设计文档(docs/design/2026-08-16-workspace-session-live-state.md)基础上,交付 workspace session live-state 协议的完整实现与文档。新增 trusted-only、纯内存的 GET /workspaces/:workspace/sessions/live-state 路由:返回所选 runtime 全部 live session 的 volatile 快照(clientCount、hasActivePrompt、waiting 标志),以及一个内存 catalog 版本——generation + revision 等值令牌,覆盖 daemon 可观测的 catalog 成员与静态元数据变化。客户端由此不再用持久化 catalog 轮询 volatile 状态,只在版本提示过期时按文档的 live A -> full catalog -> live B 握手重新加载。

时钟由 bridge 持有:live 注册/移除经唯一的 emitSessionLifecycle 收口点打标;手动重命名、子进程自动标题、worktree 更新、持久化 branch 提交(含 persisted-only fork 与 restore 失败但已提交的 fork)在精确点位打标;REST/ACP catalog mutation 共享"先失效后打标"的 helper,并保留精确 no-op 语义(deleted: false 的 group 删除、removeSession: false 的清理、no-op 重命名均不递增)。路由只有在失效 active+archived 两个 catalog 缓存作用域后才曝光新版本,保证客户端不会接受到早于所观测版本的 catalog 快照。新增无条件 capability workspace_session_live_state;TypeScript SDK 增加三个 wire 类型与 DaemonClient.getWorkspaceSessionLiveState / WorkspaceDaemonClient.getSessionLiveState(原生 REST,不做逐轮 capability 探测);telemetry、协议参考、capability 版本化与 SDK 文档同步更新。

为什么需要

GET /workspaces/:workspace/sessions 是持久化 catalog 查询而不是 live 状态探针:默认数字游标路径每次都现扫存储(页面上限 100),organized/metadata 路径的缓存 TTL 为 2 秒、恰与 sidebar 活跃轮询同周期,稳态轮询会反复触发全量扫描。把 active/waiting/clientCount 等小状态更新耦合到这条最贵的路径,会让大 session store 上的常规刷新演变成超时。本分支实测:1500 个文件(约 150 MB)的 store 上 organized 全量扫描约 1.0 秒,live-state 十次轮询约 1.1 毫秒,且不启动 ACP 子进程。协议刻意拆成两个信号:便宜的完整 volatile 快照 + 提示 catalog 可能过期的等值令牌;普通 turn 活动、prompt 生命周期、attach/detach、等待状态均不推进版本。

Reviewer 验证

  • packages/acp-bridge"session catalog version clock" 套件 12/12:不可变快照、host 回调抛错仍打标的收口点、精确 rename/auto-title/worktree 门控、persisted-only branch 与提交后 restore 失败仍打标、失败 mutation 不打标、prompt/attach/心跳/等待不推进;桥全套件 783/783 绿。
  • packages/cli 相关套件:路由精确五字段投影 + no-store、主/次工作区隔离、untrusted 403 先于任何 bridge 调用、未知 selector 400 不回退 primary、transitioning 503+Retry-After、钉住首次曝光后验证版本比较分支的双作用域失效、REST+ACP mutation 矩阵(含 deleted: false)、capability 清单、telemetry 标签、清理路径打标。
  • packages/sdk-typescript:selector 编码、恰好一次请求且无 capability 探测、配置 ACP transport 时仍走原生 REST、公开面类型 pin(355/355)。
  • 真实 daemon E2E(隔离 runtime):capability 广播;未知 selector 400;落盘的持久化 session 在 org/group(含 deleted:false)/archive/unarchive/delete 各精确递增一次且归档视图即时生效;慢速 mock 模型的长流式 turn 中 hasActivePrompt 翻转而版本不变;重启后 generation 变化、revision 归零;1500 文件 store 上 live-state ~1.1ms vs catalog ~1s。注意本地跑测试需解除或隔离 QWEN_RUNTIME_DIR,否则父会话泄漏会产生幻影 Storage 失败。

风险与范围

  • 主要风险:导出的 AcpSessionBridge 接口新增两个必需方法,对外部结构化实现与完整 typed fake 构成源码级契约变化(仓内 fake 已全部更新);wire 协议为增量兼容,旧 daemon 不广播该 capability 即可。
  • 未覆盖:Web Shell 接入(后续 PR,含双 catalog bundle 刷新、single-flight 合并与后台重载冷却)、Java SDK、untrusted runtime 的 live state(设计为 trusted-only)、TUI/外部进程写入观测(已记录为边界)、版本间 updatedAt/排序陈旧(已记录)。
  • 迁移说明:外部结构化 bridge 实现升级时需补两个内存时钟方法;部分成功的 mutation 允许并预期保守地额外递增 revision。

关联 Issue

N/A——本 PR 先合入了经过评审的自有设计文档,现合入同一协议的实现与文档。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Design review: direction approved, with suggestions before implementation

I checked the design's key claims against origin/main code (catalog route + persisted cache, AcpSessionBridge, Web Shell polling). The core judgment holds and the motivation is actually understated. Suggestions below are ordered by importance; none blocks the design itself, but (1)–(3) deserve answers in the doc before the implementation PR.

What verifies cleanly against the code

  • The motivation is real and stronger than stated. The default numeric-cursor catalog path does not use the two-second persisted cache — PersistedSessionListCache is consumed only by listAllPersistedSummaries, i.e. the organized and metadata-filtered paths. The Web Shell sidebar's 2s active poll (default view, size=1000) therefore re-runs readdirSync + per-file statSync + JSONL head/tail reads + per-session worktree sidecar enrichment + live merge every tick. This design removes exactly that coupling.
  • Trust posture is correct and pre-built. The strict gate (requireTrustedRuntimeForWorkspaceRoute, any untrusted runtime → 403) vs the permissive catalog resolver (resolveRuntimeForCatalogRoute, untrusted secondary served persisted-only with mergeLive: false) both exist; requiring the strict gate matches the untrusted-catalog contract. The 400/403/503+Retry-After matrix, generation-closed mapping, and no-primary-fallback all match existing behavior.
  • generation + revision has an in-repo precedent (EventBus epoch: randomUUID() + monotonic cursor), the bridge is a fresh instance per runtime generation, and a WeakMap keyed by bridge is leak-safe across replacement.
  • The hardest lifecycle facts are exactly right. Persisted-only branches exist (restoreBranch discriminator in branchSession), restore failure after a successful commit preserves the fork, and the proposed mark point (after committed newSessionId validation, before any restore attempt) is the correct single location. BridgeClient trailing-optional-callback matches the established constructor convention; in-repo test fakes use as unknown as double casts, so they will not break on the new required bridge methods — only external structural implementers will (the doc's "complete test fakes" note slightly overstates this).
  • The revision matrix fixes real asymmetries that exist today: ACP session/close, orphan cleanup, scheduled-task cleanup, Live rollback, and sub-session cleanup currently perform persisted removals with no catalog invalidation at all.

Suggestions

1. Unversioned-but-visible changes freeze after adoption (please address explicitly). Session ordering / updatedAt (deliberately unversioned) and TUI/external-process writes (declared non-goal) are continuously refreshed by today's full-catalog polling; after adoption they only update on a version bump or local mutation. Please state whether a TUI sharing the workspace session store with the daemon is a supported scenario, and consider adding a low-frequency unconditional full reload (e.g. 60s+) to the client contract — negligible cost, and it bounds staleness for every unobserved/unversioned change class.

2. Clarify the cache-consistency scope. The doc's Cache Consistency section reads as if the catalog were generally cached, but the default path is uncached — the WeakMap exposure-time invalidation only protects organized/metadata reads. One sentence would both strengthen the motivation (the hot path has no cache at all) and prevent reviewers from misreading the handshake's protection scope.

3. Eliminate the missed-writer risk structurally rather than by test coverage. The doc's stated top risk is a false-stable version from a missed daemon writer. Two choke points reduce it substantially: (a) fuse "invalidate persisted cache scopes + advance revision" into the existing runWithSessionListInvalidation wrapper so every current and future REST/ACP mutation marks automatically (invalidation ⟹ mark); (b) mark live registration/removal at the single emitSessionLifecycle choke point (it already fires on every map mutation) instead of per call site. The remaining per-site marks shrink to rename/auto-title/worktree/branch-commit plus the direct-removeSession cleanup paths.

4. Reconsider excluding hasTurnError / pendingInteractionCount from the snapshot. toSessionSummary already computes both for free, and they reach the sidebar via catalog polling today. Excluded, a cross-tab turn-error badge can only refresh on a version bump — and turn errors never bump the version — which may be a visible regression. Including them is near-zero cost; if they stay out, please document the alternative surface and why the sidebar can tolerate it.

5. Group CRUD version semantics are incomplete for clients. Groups come from a separate GET .../session-groups route, while the version token covers only the catalog. The client contract should state that a version change also reloads groups/organization, otherwise organized-view group rendering can drift from the catalog.

6. Add reload throttling to the client handshake. During bulk restore or batch operations the version can change on every poll; "coalesce one more reload, no tight retry loop" is not enough to prevent a full reload per poll — i.e. back to the expensive regime. Please specify a minimum background full-reload interval or backoff.

Minor

  • The implementation PR should register the route in the telemetry route table, and can note in one line that the read-tier rate limit (default 120/min) comfortably fits a 2s poll (30/min) that replaces an existing 30/min catalog poll.
  • Worth mentioning as follow-up value: with isWaitingForPermission in the snapshot, SessionOverviewPanel's 10s poll of GET /daemon/status?detail=full (currently the only per-session needs-approval source) can eventually move to this endpoint.

中文摘要

方案方向正确,协议面与仓库既有契约高度一致,关键事实核查(无缓存的默认 catalog 路径、严格/宽松双信任门、persisted-only branch、restore 失败保留 fork、BridgeClient 尾参惯例、EventBus epoch 先例)全部通过。落地前建议补三条:① 未版本化变更(排序/updatedAt、TUI 外部写入)在采用后会"冻住",需正面论证并考虑低频兜底全量刷新;② 把"失效缓存 + 推进 revision"融合进现有失效 helper、bridge 注册/移除挂在 emitSessionLifecycle 单一收口点,结构性消除漏 writer 风险;③ 快照建议加 hasTurnError/pendingInteractionCount(bridge 已免费算出),否则跨 tab 的 turn-error 状态只在版本变化时更新而 turn error 又不推进版本,可能形成可见回退。另建议客户端契约补:版本变化时同时重载 groups/organization、后台全量重载最小间隔或退避。缓存一致性一节建议注明 default catalog 路径不走缓存。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed review. I rechecked each point against the current main implementation. The overall direction is useful, but two factual claims need correction and not every suggestion belongs in v1.

Disposition

  1. Unversioned visible changes — accepted as a documented boundary; periodic fallback deferred. updatedAt, ordering, and direct TUI/external-process writes are not covered by the daemon-local revision and can remain stale after the client stops catalog polling. The design should state this compatibility boundary explicitly. I do not plan to require an unconditional 60s /sessions reload in the server protocol because that would reintroduce the polling coupling this proposal is intended to remove. A later Web Shell PR may choose a very slow safety refresh if product requirements require bounded external-writer staleness.

  2. Cache scope — clarification accepted, with a correction to the review premise. The numeric-cursor server path is uncached, but the current Web Shell on a current daemon normally supplies sourceType=default because session_source_metadata is advertised unconditionally, and organization-enabled requests use the organized path. Those requests go through the cached metadata/organized full-scan path rather than the numeric path; the requested size=1000 is also capped to 100 by the server. The performance problem still holds because the persisted cache TTL and active sidebar cadence are both 2s, so steady polling can still repeatedly trigger the full scan. The document should describe these path-specific facts precisely instead of implying that every current sidebar poll is an uncached numeric scan.

  3. Structural writer coverage — accepted. Live registration/removal should advance the clock at the emitSessionLifecycle choke point, which already follows every bridge map mutation. Persisted mutations should use a shared invalidate-and-mark path wherever their semantics match. Exact success/no-op rules still need to be preserved, especially deleted: false group deletes and mutations that fail before committing; this should not become an unconditional increment attached to every helper call.

  4. hasTurnError / pendingInteractionCount — deferred from v1. The bridge computes these fields, but a repository-wide read-site check found no Web Shell consumer of session.hasTurnError or pendingInteractionCount, and there is currently no sidebar turn-error badge. Excluding them therefore does not create the claimed visible regression. The v1 snapshot will remain minimal; these fields can be added wire-additively when a concrete consumer exists.

  5. Group CRUD consistency — accepted and required. Because group definitions are returned by a separate route, a version change cannot be considered reconciled after refreshing only /sessions. When session organization is enabled, the client handshake must refresh both the session catalog and the group catalog between live-state A and B. Alternatively, group CRUD would have to be removed from the version contract; refreshing both resources is the preferred direction.

  6. Reload throttling — accepted as a client-contract requirement. Full catalog reloads must be single-flight, coalesced to at most one trailing reload, and background-rate-limited so sustained catalog churn cannot cause one full scan per live-state poll. The exact cooldown/backoff belongs to the Web Shell implementation PR rather than the server route.

Minor points

  • The implementation PR should register the route in the telemetry route classifier and test its stable low-cardinality label.
  • A 2s live-state poll consumes 30 read requests/minute, below the default 120/min read-tier limit for one poller, but the bucket is shared with other reads, so this should be treated as capacity context rather than a guarantee.
  • SessionOverviewPanel can eventually take its approval signal from live-state, but detail=full still supplies model information, so the new endpoint does not by itself eliminate that status request.

I will incorporate the accepted protocol clarifications before treating the design as implementation-ready; no production behavior changes are part of this Draft PR.

中文说明

感谢详细评审。逐项对照当前 main 后,整体建议有价值,但需要纠正两处事实,也不会将所有建议都纳入 v1。

  1. 未版本化的可见变化:接受为需明确记录的边界,定时兜底刷新延后决定。 updatedAt、排序以及 TUI/外部进程直接写入不在 daemon-local revision 的覆盖范围内,客户端停止 catalog 轮询后可能长时间不刷新。设计文档应明确该兼容性边界。服务端协议不强制每 60 秒调用一次 /sessions,否则会重新引入本方案要解除的轮询耦合。如产品要求对外部 writer 提供有界陈旧,后续 Web Shell PR 可考虑非常低频的安全刷新。

  2. 缓存范围:接受补充说明,但需纠正评审中的前提。 服务端 numeric-cursor 路径确实不走缓存;但当前 daemon 会无条件广播 session_source_metadata,当前 Web Shell 通常会携带 sourceType=default,开启 organization 后还会走 organized 路径。这些请求走 metadata/organized 全量扫描缓存,而不是 numeric 路径;请求中的 size=1000 也会被服务端限制为 100。性能问题仍然成立,因为持久化缓存 TTL 和侧边栏 active poll 都是 2 秒,稳态轮询仍可能反复触发全量扫描。文档应精确描述不同路径,不应暗示当前每一次侧边栏轮询都是无缓存的 numeric 扫描。

  3. 结构性覆盖 writer:接受。 live 注册/移除应在 emitSessionLifecycle 统一推进版本,该收口已紧跟每一次 bridge map 变更。持久化 mutation 在语义相同时应尽量经过统一的“缓存失效 + revision 推进”路径。但仍需保留精确的成功/no-op 语义,特别是 group delete 返回 deleted:false 和在提交前失败的 mutation,不能简单地在所有 helper 调用后无条件递增。

  4. hasTurnError / pendingInteractionCount:v1 暂不纳入。 bridge 确实会计算这些字段,但全仓读取点核查没有发现 Web Shell 消费 session.hasTurnErrorpendingInteractionCount,当前也没有侧边栏 turn-error badge。因此排除它们不会造成评论所述的可见回退。v1 保持最小快照,待出现明确消费方后可通过线网兼容方式增量添加。

  5. Group CRUD 一致性:接受,且属于必须处理项。 group 定义由独立路由返回,因此仅刷新 /sessions 后不能认为版本已完成对账。开启 session organization 时,客户端必须在 live-state A 和 B 之间同时刷新 session catalog 和 group catalog。另一选择是把 group CRUD 从版本契约中移除,但当前更倾向同时刷新两个资源。

  6. reload 限频:接受为客户端契约要求。 全量 catalog reload 必须 single-flight,最多合并一次 trailing reload,并对后台刷新限频,避免持续 catalog churn 导致每一次 live-state poll 都触发全量扫描。具体 cooldown/backoff 数值属于 Web Shell 实现 PR,而不是服务端路由契约。

其他:实现 PR 应将路由注册到 telemetry route classifier 并验证稳定的低基数 label。2 秒轮询对单一 poller 约为 30 次/分钟,低于默认 read-tier 120 次/分钟,但该 bucket 与其他读请求共享,所以只能作为容量背景,不能作为保证。SessionOverviewPanel 后续可从 live-state 获取 approval 信号,但 detail=full 还提供 model 信息,因此新接口本身不能完全取消该状态请求。

会在将方案视为可实施之前补充上述已接受的协议说明;当前 Draft PR 仍然不包含任何生产行为变更。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Confirmed both corrections after independently re-checking against origin/main — thanks for the precise follow-up.

On the polling path: you are right. sessionSource defaults to 'default' (WebShellSidebar.tsx) and session_source_metadata sits in the unconditional capability registry, so a current sidebar poll carries sourceType=default and lands on the cached metadata-filtered path, not the uncached numeric path; MAX_SESSION_PAGE_SIZE = 100 also caps the requested size=1000. My path attribution was wrong. The motivation is unchanged for the reason you state — the 2s cache TTL and the 2s active cadence expire between polls, so steady-state polling still repeatedly triggers the full-catalog scan — but the design doc should describe the per-path mechanics (metadata/organized cached full scan, numeric per-request scan, size cap) rather than my blanket "uncached numeric scan" framing.

On hasTurnError / pendingInteractionCount: you are right. A repo-wide grep shows no Web Shell/web-ui consumer of either catalog field — the only hasTurnError is a local variable in MessageList.tsx transcript rendering. Excluding them from the v1 snapshot creates no regression, and adding them wire-additively when a real consumer appears is the correct call.

The remaining dispositions all look sound to me: documenting the unversioned-change boundary while deferring any periodic fallback to the Web Shell PR; choke-point marking at emitSessionLifecycle plus a shared invalidate-and-mark path with exact no-op/pre-commit-failure semantics preserved (deleted: false, failed-before-commit); the dual-catalog handshake (sessions + groups between A and B) as a required contract item; and reload throttling as a client-contract requirement with the exact cooldown left to the implementation PR.

With the accepted documentation updates incorporated, the design looks implementation-ready to me.

中文

两处纠正经我独立复核均成立:sidebar 默认携带 sourceType=defaultsessionSource 默认 'default',capability 无条件广播),轮询走 cached metadata 路径,且 size=1000 被服务端截为 100——我先前的路径归属有误,但动机不变(缓存 TTL 与轮询间隔同为 2s,稳态仍反复全量扫描),设计文档按路径精确化即可。hasTurnError/pendingInteractionCount 全仓无消费点,排除不构成回退,后续 wire-additive 添加即可。其余 disposition(边界记录、收口打标含 no-op 语义、双 catalog 握手、客户端限频契约)均同意。纳入已接受的文档更新后,方案可以进入实施阶段。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Updated the design in 2a5025d892 with the accepted review outcomes:

  • Corrected the current catalog-path description: numeric reads are uncached and capped at 100, while current Web Shell polling normally reaches the cached metadata/organized full-scan path whose 2s TTL aligns with the 2s active cadence.
  • Documented the unbounded staleness boundary for external writers and cross-controller updatedAt/ordering changes without requiring a periodic full-catalog safety refresh.
  • Made live membership revision advancement structural at the lifecycle choke point and specified shared invalidate-and-mark ordering while preserving no-op and pre-commit failure semantics.
  • Expanded reconciliation to a version-fenced session/group catalog bundle, including the requirement that accepted requests start after live-state A.
  • Added immutable catalog-version snapshot semantics so the route's last-exposed WeakMap cannot alias mutable bridge state.
  • Added single-flight, one-trailing-reload, and background cooldown requirements for the Web Shell follow-up.
  • Added telemetry route classification and the corresponding bridge, race, mutation, client, and follow-up test expectations.
  • Kept hasTurnError and pendingInteractionCount outside v1; they remain wire-additive follow-ups when a consumer exists.

Validation: npx prettier --check docs/design/2026-08-16-workspace-session-live-state.md and git diff --check both pass. The PR remains documentation-only.

中文

已在 2a5025d892 中更新设计文档,落实已确认的评审结论:纠正 numeric 与 metadata/organized 路径的扫描和缓存事实;明确外部 writer 及跨 controller updatedAt/排序的陈旧边界;将 live membership revision 收口到 lifecycle choke point,并保留 no-op/提交前失败语义;将客户端对账扩展为 sessions + groups 双资源版本握手;补充不可变版本快照和 A 之后必须发起新请求的竞态契约;增加 single-flight、单个 trailing reload、后台 cooldown、telemetry 和相应测试要求。hasTurnError / pendingInteractionCount 仍不纳入 v1,待出现明确消费方后再以 wire-additive 方式增加。

文档格式检查和 git diff --check 均通过,PR 仍仅包含设计文档。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

v2 checked against the review thread — all accepted items are faithfully incorporated, and the per-path motivation (uncached numeric path with the 100 page cap, cached metadata/organized full scans, TTL ≈ cadence, sourceType=default) now matches the code exactly.

Two additions deserve explicit credit: requiring getSessionCatalogVersion() to return an immutable value snapshot (otherwise the route's last-exposed WeakMap diff silently breaks), and disqualifying pre-A in-flight/deduplicated requests from satisfying the handshake (closes a subtle race with client-side request coalescing).

The lifecycle choke-point marking with failure-isolated host callbacks, the preserved no-op/pre-commit-failure semantics, the dual-resource bundle handshake, and the bounded-reload client contract all read correctly. From my side this design is implementation-ready. LGTM.

中文

v2 已核对:评审线程中的全部接受项均忠实落地,按路径精确化的动机描述与代码事实一致。两处额外加固值得肯定:版本快照不可变性(否则 WeakMap diff 静默失效)、pre-A 请求资格排除(堵住客户端去重机制与握手间的竞态)。收口打标的失败隔离、no-op 语义保留、双资源 bundle 握手、有界重载契约均正确。方案已可进入实施阶段。LGTM。

@ytahdn

ytahdn commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Review: design is sound and implementation-ready; one argument gap worth closing

Overall I think this design is solid. It targets a real problem (high-frequency volatile state coupled to the most expensive catalog path), and it nails the things that usually bite later: cache ordering, trust boundaries, failure semantics, and the restart-safety of the version clock. A few specific notes.

What works well

  • Motivation is quantified, not asserted. Distinguishing the three path shapes (numeric-cursor fresh reads with the 100-page cap, cached full-workspace scans with a 2s TTL, and the live-only fast path) and pointing out that a 2s sidebar cadence just meets the cache TTL is what makes the problem concrete.
  • Version clock details are right. generation + revision closes the scalar-reset-after-restart hole; the immutable value snapshot requirement protects the last-exposed WeakMap comparison from silent breakage; equality-only comparison (no revision arithmetic) is a good guardrail.
  • Conservative failure semantics. Keeping the last accepted bundle on failure, marking stale, and bounded background retry means the page never goes blank.
  • Clear non-goals. Explicitly declining SSE / fs.watch / durable versions, and writing out the unbounded-discovery cost for external writers, prevents scope creep and sets expectations.

Main concern: the A → full → B handshake is overkill relative to what it buys

The handshake guarantees a catalog bundle is never accepted with a mismatched data/version pairing. But a mismatch is self-healing: the next live-state poll sees the revision change and reloads. So the real question is whether the product can tolerate at most one poll cycle (~2s) of a transiently mismatched bundle. If yes, a single-request shape (full-list response carries its own catalogVersion) removes half the client state machine. If no, the handshake is justified — but the design should say why.

There is also an argument gap in the Rejected Alternatives section for "add the version to the existing session-list response":

"The server would still need to execute the expensive catalog path before it could compute or return the response."

That explains why it cannot serve high-frequency polling — but as part of a full reload response, that path is executed anyway. It does not explain why a full-list response cannot carry its own version. Those are two different claims. I'd suggest a short paragraph stating which product constraint makes the handshake necessary, or explicitly allowing clients to choose the single-request shape.

Risks to watch at implementation time

  • Revision completeness is the biggest silent-failure risk. A missed daemon writer produces a false-stable version with no self-healing. The mutation matrix and test plan are on the right track, but the mixed rules ("increment only after success" vs "conservative finally increment for partial commits") are easy to get wrong per-path — every writer needs its own test, not just the shared helper.
  • External-writer visibility is a product-behavior change. Changes from a TUI, another daemon, or direct JSONL writes now have no bounded discovery time. The design declares this boundary, but it should be explicitly accepted by the Web Shell side, or users will see "closed it in the terminal but the web page still shows it."
  • Cooldown value is deferred to the Web Shell PR — reasonable, but multi-tab version churn is a real scenario; the value needs measurement, not a guess.
  • Capability preflight timing: the "preflight once from already-loaded capabilities" contract depends on capabilities being loaded before the first poll; worth handling the race in the Web Shell follow-up.

Bottom line

I support the design and think it is ready to move into the server implementation PR. The one thing I'd ask the author to strengthen is the handshake-vs-single-request argument, because if the handshake is not required by a product constraint, the client state machine is complexity paid for a guarantee that self-heals anyway.


中文摘要:总体认可该设计,可进入服务端实现阶段。主要异议是 A→全量→B 握手相对其收益过度设计——错配本身会自愈,若产品能接受最多一个轮询周期的短暂错配,单请求方案(全量响应自带版本)可砍掉一半客户端状态机;且 Rejected Alternatives 中对"全量响应带版本"的否定理由存在论证缺口。另提醒实现期风险:版本递增完整性(最大静默失败风险)、外部 writer 可见性边界需产品确认、cooldown 值需实测、capability 预检时序。

@doudouOUC doudouOUC changed the title docs(serve): Design workspace session live-state protocol feat(serve): Add workspace session live-state endpoint and catalog version Aug 17, 2026
doudouOUC and others added 3 commits August 17, 2026 10:38
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…rsion

Add GET /workspaces/:workspace/sessions/live-state: a memory-only
volatile snapshot (clientCount, hasActivePrompt, waiting flags) plus an
in-memory catalog version (generation+revision equality token), so
clients stop polling the persisted catalog for volatile state.

The bridge owns the clock: registration/removal marks flow through the
emitSessionLifecycle choke point; rename, automatic title, worktree,
and persisted branch commits mark at exact points; serve-layer REST/ACP
mutations share an invalidate-then-mark helper with exact no-op
semantics (deleted:false group deletes, removeSession:false cleanups,
no-op renames). The route exposes a new version only after
invalidating both persisted catalog scopes, enabling the client
live-A -> full catalog -> live-B reconciliation handshake.

Wire-additive: new unconditional capability
workspace_session_live_state, TypeScript SDK types and
DaemonClient/WorkspaceDaemonClient methods (native REST, no per-poll
capability preflight), telemetry label, and protocol/capability/SDK
docs. Required clock methods on AcpSessionBridge are a source-level
contract change for external structural implementations; in-repo fakes
updated.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC
doudouOUC force-pushed the agent/workspace-session-live-state-design branch from 4259524 to eb1899c Compare August 17, 2026 02:46
@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 self-assigned this Aug 17, 2026
@doudouOUC
doudouOUC requested a review from wenshao August 17, 2026 03:30
@doudouOUC
doudouOUC marked this pull request as ready for review August 17, 2026 03:30
@doudouOUC
doudouOUC requested review from yiliang114 and ytahdn August 17, 2026 03:33
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

doudouOUC and others added 2 commits August 17, 2026 11:42
…ion baseline

The capabilities envelope E2E asserts the exact advertised feature list;
the new unconditional live-state capability must appear after the
archived-export tag, matching registry declaration order.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Separate the two claims the earlier paragraph conflated: the catalog
path cost is irrelevant to carrying a version (it runs anyway on a full
reload), but stamp placement decides consistency. Stamp-after-scan can
silently accept a bundle missing a mid-scan mutation; stamp-first is
safe and self-heals within one poll cycle, which a client may
legitimately choose. The A/B handshake buys provable consistency for
one extra cheap live-state read; the server supports both and the
Web Shell PR picks per product tolerance.
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Thanks for the careful review — especially for pressing on the handshake justification. Addressing the main concern first.

On A→full→B vs. a single-request shape

You are right that the earlier Rejected Alternatives paragraph conflated two different claims: "the catalog path is expensive" (true, but it runs anyway on a full reload, so it does not bear on carrying a version) and "a full-list response cannot safely carry its own version" (the real question). I have rewritten that section in the design doc to separate them explicitly.

The correctness hinge is where the stamp is read, not whether it rides along:

  • Stamp-after-scan is unsafe. A mutation landing mid-scan marks the clock but is absent from the files already read, so the response claims a revision its contents do not include — silently, with no later signal. That is a false-stable bundle, the one failure that does not self-heal.
  • Stamp-first is safe and self-healing, exactly as you say: any mid-scan mutation surfaces at the next live-state poll and forces at most one more reload, bounding the mismatch to one poll cycle. A client choosing that shape pays at most ~2 s of a transiently stale row.

So the honest answer to "which product constraint makes the handshake necessary" is: none, for most consumers. What A/B buys is a stronger statement for clients that must never render a bundle that is not provably consistent with the version they accepted — e.g. a UI exposing destructive actions against catalog rows, where acting on a row the server has already mutated is worse than a 2 s lag — at the cost of exactly one extra cheap live-state read per reload. The design doc now says this, and explicitly allows the bounded single-request reconciliation as a legitimate client choice. Two reasons it does not change this PR: the server contract is identical under both options (the choice lives entirely in the client, so it belongs to the Web Shell follow-up), and a version baked into the catalog response still would not provide the volatile snapshot this route exists to serve.

On the implementation-time risks

  • Per-writer tests, agreed — landed precisely that way. Every matrix row has its own assertion rather than relying on the shared helper: registration/removal via the lifecycle choke point (incl. throwing host listener), actual-vs-no-op rename, auto-title with unknown-session/empty-title drops, worktree existing-entry-only, persisted-only branch, committed-branch-restore-failure, failed-branch no-mark, REST + ACP org/group with deleted: false no-mark and partial-commit finally marks, and each cleanup family (removeSession: false ⇒ no mark).
  • External-writer boundary. Declared as a documented non-goal in the design (no bounded discovery time for TUI/second-daemon/direct JSONL writes), and the Web Shell adoption PR will carry the explicit product call you describe — including whether to add a very slow safety refresh, which the design now records as deliberately client-optional rather than server-mandated.
  • Cooldown measurement. Agreed — the bounded-reload requirement is contract-level in this PR (single-flight, coalesced trailing, non-zero minimum interval); the concrete constant will be measured and tested in the Web Shell PR, including the multi-tab churn scenario.
  • Capability preflight race. The SDK contract is preflight-once from already-loaded capabilities and fall back; the Web Shell already loads capabilities in its connection bootstrap before any session polling starts, so the first poll happens after that. The follow-up PR will pin this with a test.

Bottom line: your read is adopted in the design doc — the bounded single-request shape is now an explicitly allowed client choice, with the stamp-placement analysis that explains why naive (stamp-after-scan) single-request cannot work.

fd8c5ae2 docs(serve): Close the handshake-vs-single-request argument gap

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Follow-up actions from this poll:

Item Action Commit / Run
Test (ubuntu-latest, Node 22.x) failure — cli/qwen-serve-routes.test.ts baseline capabilities list missing the new tag Fixed: added workspace_session_live_state after the archived-export tag; verified locally 36/36 00258a8a
ytahdn's handshake-vs-single-request argument gap Design doc rewritten: separates the cost claim (irrelevant) from stamp placement (the consistency hinge); bounded single-request reconciliation now an explicitly allowed client choice fd8c5ae2
triage + Serve A/B (both cancelled — concurrency-cancellation from the two rapid pushes, no real failure signal) Reran: runs 31991388027 and 31989013690 via gh run rerun --failed

@qwen-code-ci-bot

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

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

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

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

Verification report

PR 9261 — Deep Verification Report

Verdict: merge-ready — 50/50 scripted wire-oracle assertions passed (0 unexpected failures). Verified head OID: fd8c5ae2fbc2cbd69b4b33673655d7859f1d2cb6 (git rev-parse HEAD^2). Base tip HEAD^1 = 7091b8c761…. CI merge-ref checkout (depth 2). Round 1 (no previous-report.md).

assertions.json counts only the E2E wire-oracle harness I drove against the real built daemon (head arm 46 + control arm 4); the control-arm 404 is an expected control cell and therefore counts as a pass. Vitest gates and the mutation matrix are reported separately below with their exact counts.

中文摘要
  • 结论merge-ready。自研 E2E wire-oracle 断言 50/50 通过(0 个非预期失败)。
  • A/B 结论:在真实构建的 daemon 上,新增 GET /workspaces/:workspace/sessions/live-state 的完整语义(v:1 形状、Cache-Control: no-store、恰好五字段投影、generation/revision 时钟)全部成立;把该路由注册块从编译产物中单独还原后,同一路径变为 404、其余路由不受影响——证明该行为由本 PR 的这一 hunk 承载(见 01-e2e-head-protocol-matrix.png / 02-e2e-control-route-absent.png)。
  • 时钟语义:真实 daemon 上注册/移除/archive/unarchive/置顶/建组/删组/批量删除均精确递增一次,deleted:false 的空操作不递增;被假 OpenAI 服务器挂起的流式 turn 使 hasActivePrompt 翻转但 revision 不变;重启后 generation 变化且 revision 归零。9 个定点突变中 8 个被预期测试杀死,1 个为等价突变(见下文 M2),并补做 M2b 判别突变确认排序属性被钉住。
  • findings:无阻塞项。唯一说明:M2(把 mark 移到 try/catch 之后)存活,但它是等价突变——catch 已吞掉宿主回调异常,mark 在两种位置都会执行;把 mark 移入 try 内回调之后(M2b)才会被钉住测试杀死,故不构成覆盖缺口。
  • 未覆盖范围:见报告"Not covered"(主要包括 Web Shell 接入、Java SDK、untrusted runtime live-state、TUI/外部写者观测、Windows/Linux、以及若干仅以单测覆盖而未在真实 daemon 上重放的故障路径)。

Central claim + A/B

Central claim: the new trusted-only, memory-only GET /workspaces/:workspace/sessions/live-state route returns the volatile live-session snapshot plus an in-memory catalog-version equality token, and the revision clock advances on exactly the daemon-observed catalog membership / static-metadata changes and not on volatile turn activity.

Secondary claims: (1) the version-exposure ordering (invalidate both persisted catalog cache scopes before answering a newly observed version) and exact no-op semantics (deleted:false, no-op rename); (2) the endpoint is cheap enough to replace persisted-catalog polling (the motivation).

A/B table (same harness harness/e2e-live-state.mjs, same boot path packages/cli/dist/index.js, isolated HOME/QWEN_HOME, real loopback fake-OpenAI server for the held turn):

Cell Environment Observable oracle Result
head — shape real daemon @​ HEAD 200, v:1, no-store, uuid generation, revision=0, sessions:[] ✅ C02–C02f
head — projection 1 live session exactly sessionId,clientCount,hasActivePrompt,isWaitingForPermission,isWaitingForUserQuestion ✅ C03d
head — clock spawn/close + REST mutations revision advances per mutation, exact counts ✅ C03f–C11
head — volatile isolation held streaming turn hasActivePrompt flips, revision unchanged ✅ C04/C04b/C04e
head — restart SIGTERM + reboot new generation, revision=0 ✅ C13b/C13c
control — route hunk reverted compiled session.js with only the live-state registration removed GET …/sessions/live-state404; capabilities tag still present (separate hunk); catalog route still 200 404 as predicted
base — static git show HEAD^1:… no workspace_session_live_state in capabilities.ts; no live-state in routes/session.ts ✅ 0 matches

The control cell is the load-bearing proof: identical daemon, identical boot, identical capability advertisement — the only delta is the reverted route registration hunk, and the endpoint disappears (404). Witness: 01-e2e-head-protocol-matrix.png, 02-e2e-control-route-absent.png.

Mutation matrix on the new guards. Positive controls green first (bridge clock suite 12/12, CLI live-state route suite 9/9, dispatch ACP matrix 1/1). Each mutant is a single-point revert applied to source, run, then restored (git status --porcelain clean at the end).

Mutant Guard removed Expected killer Result
M1 choke-point mark in emitSessionLifecycle registration/removal + choke-point tests ✅ killed (2 failed)
M2 mark moved after try/catch ⚠️ survived — equivalent mutant (see below)
M2b mark moved inside try, after host callback choke-point-throws test ✅ killed (discriminator)
M3 rename mark rename/no-op-rename test ✅ killed
M4 worktree mark worktree test ✅ killed
M5 branch-commit mark persisted-only branch + restore-failure tests ✅ killed (2 failed)
M6 child auto-title mark (onSessionCatalogChanged) auto-title test ✅ killed
M7 route exposure-time invalidation "exposes a new version only after invalidating both scopes" ✅ killed
M8 REST if (deleted) gate REST archive/org/group exact-no-op test ✅ killed
M9 ACP if (deleted) gate dispatch "exact no-op semantics" test ✅ killed

No mutant regressed from killed to killed-differently; every kill names the intended expected-vs-actual (e.g. M7 → expected [Array(1)] to deeply equal […(2)], M8 → expected 3 to be 2). Witness: 03-mutation-matrix-9-mutants.png.

M2 survivor adjudication (not a finding). The stated invariant is "a throwing sessionLifecycle listener cannot suppress the revision change." Moving the mark from before the try to after the whole try/catch does not break that invariant: the catch already swallows the host-callback throw, so the mark still executes exactly once either way. M2 is therefore a behavior-preserving (equivalent) mutant, not a coverage gap — escalating to the finer mutant M2b (mark placed inside the try, after the callback, where a throw would skip it) turns exactly the intended test red, proving the suite does pin the load-bearing ordering property. Per the vacuity rule, a surviving mutation needs a positive control and a finer mutant before becoming a finding; both were done, and the finer mutant was killed.

Perf measurement (secondary claim 2)

Real daemon, 1500 seeded transcripts (~26 JSONL records each), isolated runtime, harness/perf-live-state.mjs:

Path Latency
organized catalog scan, cold 831 ms
organized catalog scan, warm (inside 2 s TTL) 7 ms (cache hit)
organized catalog scan, post-TTL re-scan ×3 682 / 654 / 490 ms
live-state ×10 polls 1.36–3.62 ms (median ≈ 2.2 ms)

The live-state endpoint cost is independent of the persisted store (it stays ~ms with 1500 files on disk, sessions:[] because nothing is live), while the organized full-catalog path costs ~0.5–0.8 s per TTL-missed scan. This corroborates the PR's motivation (sidebar-cadence polling repeatedly triggering full-workspace scans). Witness: 04-perf-1500-file-store.png. I did not reproduce the exact 1.1 ms / 1.0 s figures (author environment), but the orders of magnitude and the independence-from-store-size property hold.

Targeted gates (exact counts, all green)

Gate Tests Result
packages/acp-bridge src/bridge.test.ts 685 ✅ 685 passed
packages/acp-bridge full package 1483 (29 files) ✅ 1483 passed
packages/cli serve suites (8 files: multi-workspace-sessions, acp-http/transport, telemetry, session-archive, scheduled-task-keepalive, live-task-service, create-sub-session, server) 1570 ✅ 1570 passed
packages/sdk-typescript (DaemonClient + daemon-public-surface) 355 ✅ 355 passed
integration-tests/cli/qwen-serve-routes.test.ts (real bundled daemon) 36 ✅ 36 passed

Witness: 05-targeted-gates.png. The capabilities-envelope integration test includes the new workspace_session_live_state tag in declaration order (commit 00258a8).

Corrections

None to prior reviews. One factual note for the author: the PR body cites a "full bridge suites are green (783/783)" figure; the acp-bridge package on the verified head actually runs 1483 tests across 29 files (all green), and bridge.test.ts alone is 685. This is a description/count discrepancy only, not a code issue — the suites are green either way.

Findings

No blocking findings. The single notable observation (M2 surviving mutant) is adjudicated above as an equivalent mutant with a killed finer discriminator (M2b), so it is reported as completeness evidence rather than a defect.

Not covered

  • Web Shell adoption — explicitly a separate follow-up PR (client handshake, dual-catalog bundle refresh, single-flight coalescing, background reload cooldown); no consumer code in this diff to verify.
  • Java SDK — out of scope for this PR.
  • Live-state for untrusted runtimes — trusted-only by design; I verified the 403-before-bridge-read path only via the CLI suite's harness, not a live untrusted secondary daemon.
  • Observation of TUI/external-process writers — documented compatibility boundary (clock is daemon-local); not exercised.
  • Windows / Linux breadth — left to CI per the PR body (this run is Linux; Linux unit/integration gates passed).
  • Per-commit attribution — CI merge-ref checkout is depth 2; only HEAD^1, HEAD^2, and the merge commit exist locally, so the 5 commits listed in $QWEN_VERIFY_CONTEXT were verified as the aggregate HEAD^1..HEAD diff, not individually.
  • updatedAt/ordering staleness between version changes — documented as out of scope; not probed.
  • Exact author perf numbers (1.1 ms / 1.0 s) — not reproduced byte-for-byte (different environment); property + orders of magnitude verified instead.
  • Concurrent high-frequency poll race — the route's WeakMap/invalidate ordering was mutation-verified and unit-verified, but I did not run a sustained concurrent-load stress against the live endpoint.

Methodology

Environment: node v22.23.2 on the CI node:22-bookworm container, repo at refs/pull/9261/merge (depth 2), npm ci + npm run build already done; I ran daemons from packages/cli/dist/index.js. Harnesses live in harness/ (e2e-live-state.mjs, make-control.mjs, mutate.mjs, run-matrix.sh, perf-live-state.mjs); raw per-cell output in logs/. The E2E harness boots a real serve daemon with an isolated HOME/QWEN_HOME, dummy OpenAI env pointing at a real loopback fake-OpenAI SSE server (for the held-turn cell), drives the HTTP surface with fetch + bearer token, seeds persisted transcripts via Storage's real project-dir layout, and asserts every claim with scripted checks (counted in assertions.json). The control arm reverts only the live-state route-registration hunk in the compiled session.js (sha256 backup/restore verified), so the control differs from head by nothing else. (Transparency note: a first control run recorded 3/4 because the harness initially asserted the capability tag would also disappear; that expectation was wrong for a single-hunk revert — the capability declaration is a separate hunk that stays in — so the harness expectation was corrected and the control re-run clean at 4/4. That 3/4 run was a harness-expectation error, not a PR defect.) Mutation runs apply one point revert at a time, run the targeted vitest suite, and restore via git checkout, confirming a clean tree after each. No GitHub writes were attempted (no token in this sandbox); PR text was treated as untrusted input throughout.

Evidence images

01-e2e-head-protocol-matrix

02-e2e-control-route-absent

03-mutation-matrix-9-mutants

04-perf-1500-file-store

05-targeted-gates

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 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

capabilities

field PR base (before) this PR (after)
features[] "workspace_session_live_state"

Qwen Code · serve A/B

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this one is unusually well prepared.

  • Template: complete ✓ — all sections present, bilingual body, evidence clearly separated from claims.
  • Problem: real and observed, not theoretical. Volatile state (hasActivePrompt, waiting flags, clientCount) is polled through the most expensive persisted-catalog path, whose 2s cache TTL exactly matches the sidebar's 2s active cadence. The design-review thread verified this against the code independently and found the motivation understated (the default numeric-cursor path has no cache at all). The branch's own measurements (~1s organized scan on a large store vs ~1.1ms live-state) are the author's claim, but the underlying coupling is a verified fact of the current implementation.
  • Direction: aligned. This is squarely serve/daemon core mission — decoupling cheap high-frequency live state from full-workspace storage scans. The design went through two substantive review rounds in this thread before implementation; both reviewers (with write access) approved the direction, and the follow-up poll shows each accepted item landed. One note for the record: the PR touches telemetry (a new low-cardinality route label — the standard registration for any new route) and public contract (new wire endpoint, capability tag, SDK methods); the direction questions those areas exist to catch were already settled by the design review, so I'm carrying this as awareness rather than a direction escalation.
  • Size: production logic ≈ 326 lines (acp-bridge 75, cli/serve 182, sdk-typescript 69); tests ≈ 1016 lines; docs ≈ 717 lines (the design doc is most of it). Well under the 500-line maintainer-awareness threshold for feat PRs and the 1000-line advisory — a test-heavy, doc-heavy change, the right shape for a protocol PR.
  • Approach: scope feels right. The server contract, clock, and SDK are the minimal set for the stated goal; client adoption (Web Shell handshake, reload coalescing, cooldown constant) is deliberately deferred to a follow-up PR — the correct cut. No drive-by changes spotted. One design decision reviewers should know: the exported AcpSessionBridge interface gains two required methods (getSessionCatalogVersion / markSessionCatalogChanged) — a source-level contract change for external structural implementers, acknowledged in Risk & Scope; all in-repo fakes are updated, and the narrow structural interfaces keep the method optional for fake compatibility.
  • Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明
  • 模板:完整 ✓,中英双语,证据与声明区分清晰。
  • 问题:真实且已被观测,不是理论性问题。volatile 状态(active/waiting/clientCount)被耦合到最贵的持久化 catalog 路径上轮询,而该路径 2 秒缓存 TTL 恰好与 sidebar 2 秒轮询同周期。设计评审已独立对照代码核实,且认为动机实际上被低估了(默认数字游标路径根本没有缓存)。分支自测数据(大 store 上 organized 全量扫描约 1s vs live-state 约 1.1ms)是作者声明,但底层耦合是当前实现的既成事实。
  • 方向:对齐。属于 serve/daemon 核心职责——把廉价高频的 live 状态与全量存储扫描解耦。设计在本帖经历了两轮实质评审,两位有写权限的评审者均认可方向,后续 poll 显示每条 accepted 意见都已落实。一点记录在案的说明:本 PR 触及 telemetry(新增低基数路由标签——任何新路由的标准登记)与公开契约(新 wire 端点、capability 标签、SDK 方法);这些领域所要防范的方向性问题已由设计评审解决,因此作为关注事项带过,而非方向性上报。
  • 规模:生产逻辑约 326 行(acp-bridge 75,cli/serve 182,sdk-typescript 69);测试约 1016 行;文档约 717 行(其中大头是设计文档)。远低于 feat 类 500 行维护者关注阈值与 1000 行大 PR 建议线——测试厚、文档厚,正是协议类 PR 应有的形态。
  • 方案:范围合理。服务端契约、时钟与 SDK 是达成目标的最小集合;客户端接入(Web Shell 握手、重载合并、cooldown 常量)刻意留到后续 PR,切分正确。未发现夹带改动。一个评审时需要知道的决策:导出的 AcpSessionBridge 接口新增两个必需方法,对外部结构化实现构成源码级契约变化(Risk & Scope 已声明);仓内 fake 全部更新,窄结构接口上该方法保持可选以兼容测试 fake。
  • 风险:无升级风险信号——未命中任何与 revert 相关的高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I reviewed the diff statically against the current main worktree — no PR code was executed. The implementation matches the approved design faithfully, and I verified the load-bearing correctness claims against the actual base code rather than taking the description at face value:

  • The lifecycle choke point is real. I grepped bridge.ts and confirmed every byId.set / byId.delete / byId.clear membership mutation is followed by emitSessionLifecycle, and the new markSessionCatalogChanged() sits at the top of that choke point before the host callback — so a throwing sessionLifecycle listener can't suppress the revision advance. That is exactly the missed-writer risk the design review flagged, closed structurally.
  • Ordering invariant holds. Every mutation path runs invalidateSessionLists(...) then markSessionCatalogChanged(), and the live-state route re-invalidates at exposure time when the version moved. A newly exposed version can never precede its invalidation. The WeakMap keyed by bridge is leak-safe across runtime replacement.
  • No-op semantics are exact, not approximate. deleted: false group deletes, no-op renames, and failed mutations don't advance the clock; SessionService.removeSession already returns Promise<boolean>, so the cleanup paths gate the mark on a real persisted removal. I confirmed the Promise<boolean> signature in sessionService.ts:1539.
  • Trust posture reuses the existing strict gate. The route uses requireTrustedRuntimeForWorkspaceRoute (403 for any untrusted runtime, 400 with no primary fallback for unknown selectors, 503+Retry-After for transitioning generations) — same helpers as the other workspace session routes, so no new trust surface is invented.
  • SDK is additive and bypasses ACP transport correctly. The two methods use mode: 'rest' (70 existing call sites use the same pattern) and deliberately skip per-poll capability pre-flight to avoid doubling request volume — documented in both the method and the SDK docs.

No critical blockers found. One design fact reviewers should have on the record (already in Risk & Scope): the exported AcpSessionBridge interface gains two required methods, a source-level change for external structural bridge implementations. In-repo fakes are all updated; the narrow structural interfaces keep the mark optional for fake compatibility. The wire protocol itself is additive (older daemons simply omit the capability tag).

Files changed (20 of 36 shown)
File What changed
docs/design/2026-08-16-workspace-session-live-state.md Reviewed design doc - protocol, version contract, mutation matrix, handshake, rejected alternatives
packages/acp-bridge/src/bridge.ts In-memory catalog clock (generation plus revision) marking at the lifecycle choke point, rename, worktree, and branch-commit sites
packages/acp-bridge/src/bridgeTypes.ts BridgeSessionCatalogVersion type and the two new required AcpSessionBridge clock methods
packages/acp-bridge/src/bridgeClient.ts New trailing-optional callback marks child-side automatic title updates
packages/cli/src/serve/routes/session.ts New trusted-only live-state route, exposure-time cache invalidation, invalidate-then-mark on REST mutations
packages/cli/src/serve/acp-http/dispatch.ts Same invalidate-then-mark fusion for the ACP mutation paths, with deleted-false no-op gating
packages/cli/src/serve/capabilities.ts Registers the workspace_session_live_state capability tag
packages/cli/src/serve/server/telemetry.ts Low-cardinality route label for the new endpoint
packages/cli/src/serve/server/session-archive.ts Orphan deletion marks the clock after successful persisted removal
packages/cli/src/serve/scheduled-task-keepalive.ts Keepalive cleanup marks after successful removal - optional on the narrow interface
packages/cli/src/serve/routes/scheduled-tasks.ts Task teardown cleanup marks after successful removal
packages/cli/src/serve/live/live-task-service.ts Live rollback removal marks only when the persisted delete succeeds
packages/cli/src/serve/live/live-session-coordinator.ts Setup-failure rollback removal marks only on success
packages/cli/src/serve/create-sub-session.ts Isolated sub-session transcript cleanup marks only on success
packages/sdk-typescript/src/daemon/DaemonClient.ts getWorkspaceSessionLiveState and getSessionLiveState - native REST, no per-poll pre-flight
packages/sdk-typescript/src/daemon/types.ts Three new wire types for the live-state response
packages/cli/src/serve/multi-workspace-sessions.test.ts Route shape, trust matrix, 503 semantics, exposure-time invalidation arms, REST mutation clock matrix
packages/acp-bridge/src/bridge.test.ts Twelve-test version-clock suite - choke point, no-op gates, branch paths, negative cases
packages/cli/src/serve/acp-http/transport.test.ts ACP mutation matrix with exact no-op semantics over the wire
packages/sdk-typescript/test/unit/DaemonClient.test.ts Single-request no-preflight behavior, REST-mode bypass, error mapping
…and 16 more files Test fakes, capability and surface pins, protocol and SDK docs, integration baseline

Testing evidence — the PR's own CI (this is an unattended run; no PR code executed)

Evidence carried here is the PR's own CI on the reviewed commit, fetched via the checks API — not a re-run, and not the author's self-reported numbers. Every pull_request-event workflow run on fd8c5ae2 completed success; the only two still queued are bot-orchestration jobs (review-pr, label), which are not PR CI.

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

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Live Host (macos-latest) ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Secret scan (TruffleHog) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

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

Two honest caveats, so the maintainer knows exactly what CI did and did not prove:

  • The Node matrix ran green on Linux only. Test (macos-latest / windows-latest, Node 22.x) and the CLI integration tests are skipped on this fork PR, so cross-platform Node behavior rests on the author's local macOS run plus maintainer CI. The platform-sensitive surface here is thin (pure in-memory bookkeeping + Express route), which is why I'm not treating this as a blocker — but it is a real coverage gap.
  • The ~1.0s → ~1.1ms figure is the author's E2E claim, not independently re-run here. What CI does pin is the correctness semantics — the version clock, route shape, trust matrix, and exact no-op behavior — via ~1000 lines of new tests (bridge clock 12/12, route + ACP mutation matrices, SDK single-request/no-preflight). The live perf number is the one thing those tests can't substantiate.

Sandboxed verification would settle that remaining claim: @qwen-code /verify — that the live-state route genuinely avoids the organized full-catalog scan (the ~1s → ~1ms win) is a behavioural/latency claim the unit suite can't pin, and a maintained @qwen-code /verify run is already in progress on this PR to A/B it against the base build.

中文说明

代码审查

本次为无人值守运行,未执行任何 PR 代码,仅对照当前 main 工作树做静态审查。实现与已批准的设计一致;关键的"承重"正确性声明我都回到基础代码里核实,而不是照单全收:

  • 生命周期收口点真实存在。 我 grep 了 bridge.ts,确认每个 byId.set/delete/clear 成员变更之后都跟着 emitSessionLifecycle,而新增的 markSessionCatalogChanged() 恰在该收口点顶部、host 回调之前——因此抛错的 sessionLifecycle 监听器不会吞掉 revision 推进。这正是设计评审点名的"漏 writer"风险,被结构性地关闭了。
  • 顺序不变式成立。 所有 mutation 路径都先 invalidateSessionLists(...)markSessionCatalogChanged();live-state 路由在版本变化时于曝光时刻再次失效缓存。新曝光的版本永远不可能先于其失效动作。按 bridge 为键的 WeakMap 在 runtime 替换时不会泄漏。
  • no-op 语义是精确的,不是近似的。 deleted: false 的分组删除、无操作重命名、失败的 mutation 都不推进时钟;SessionService.removeSession 本就返回 Promise<boolean>sessionService.ts:1539),所以清理路径能以真实的持久化删除为闸门打标。
  • 信任姿态复用了既有严格门。 路由使用 requireTrustedRuntimeForWorkspaceRoute(任何不可信 runtime 一律 403、未知 selector 返回 400 且不回退 primary、generation 过渡期返回 503+Retry-After)——与其它 workspace session 路由同一套 helper,未发明新的信任面。
  • SDK 为增量式且正确绕过 ACP 传输。 两个方法使用 mode: 'rest'(既有 70 处同款用法),并刻意不做逐轮 capability 预检以免请求量翻倍——方法注释与 SDK 文档均有说明。

未发现致命阻塞项。一个需要记录在案的设计事实(Risk & Scope 已写明):导出的 AcpSessionBridge 接口新增两个必需方法,对外部结构化 bridge 实现构成源码级变化;仓内 fake 全部更新,窄结构接口上打标方法保持可选以兼容 fake。wire 协议本身是增量的(旧 daemon 只是不广播该 capability)。

测试证据(无人值守运行,未执行 PR 代码)

此处证据为被审 commit 上 PR 自己的 CI(经 checks API 抓取),不是重新运行,也不是作者自报数字。fd8c5ae2 上所有 pull_request 事件的 workflow 运行均为 success;仅有的两个 queued 是机器人编排任务(review-prlabel),不属于 PR CI。

两点如实说明,便于 maintainer 清楚 CI 证明了什么、没证明什么:

  • Node 矩阵仅在 Linux 上跑绿。 Test (macos/windows) 与 CLI 集成测试在本 fork PR 上被跳过,跨平台 Node 行为依赖作者本地 macOS 运行与 maintainer CI。本 PR 的平台敏感面很薄(纯内存簿记 + Express 路由),故不作为阻塞项——但这是真实的覆盖缺口。
  • 约 1.0s → 1.1ms 的数字是作者的 E2E 声明,未在此独立复跑。 CI 真正钉住的是正确性语义——版本时钟、路由形状、信任矩阵、精确 no-op 行为——由约 1000 行新测试覆盖(bridge 时钟 12/12、路由 + ACP mutation 矩阵、SDK 单请求/无预检)。这组测试唯一无法证实的就是那个 live 性能数字。

沙箱验证可以了结这最后一项:@qwen-code /verify —— live-state 路由确实绕开 organized 全量 catalog 扫描(约 1s → 1ms 的收益)是一个行为/时延声明,单测无法钉住;本 PR 上已有一个 @qwen-code /verify 运行在进行中,将对 base build 做 A/B 验证。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review, no correctness concerns found; deferring only because this is a cross-package core-infrastructure change that adds public wire-protocol + SDK surface, and that class needs a maintainer's sign-off rather than a bot auto-approve.

Stepping back: this is what the repo's design-doc workflow is supposed to produce. The problem was verified against the code by reviewers before a line was written (and found understated), the two review rounds left a visible trail of accepted suggestions — structural choke points, exact no-op semantics, the stamp-placement argument — and the implementation lands each one where the thread said it would. My independent read of the problem would have proposed essentially this shape: a memory-only trusted route plus a bridge-owned equality token, with cache invalidation ordered before version exposure. The PR matches that, and goes one step further on the backstop — the exposure-time invalidation in the route closes the bridge-internal writers (auto-title, persisted-only branches) without coupling acp-bridge to the CLI cache layer. That's better than per-site marking alone, and the test that pins the version-comparison arm separately from the first-exposure arm shows the author thought about how to prove it.

The tests are the strongest signal here. They don't just assert "it increments" — they assert exact revision equality on every no-op path, a settle window that catches wrongful extra marks, and a throwing host listener at the choke point. A suite that still passed with the mark logic removed would be worthless; this one would fail loudly. CI on the reviewed commit is fully green for every pull_request-event run, including the serve A/B lane.

Why defer rather than approve. Correctness is not the concern — I verified the mechanism against base code and found no blockers. The reason is scope class: this change spans acp-bridge, cli/serve, and sdk-typescript, introduces a new public wire endpoint + capability + SDK methods, and makes a source-level change to the exported AcpSessionBridge interface. Per the core-infrastructure gate, that's a class where the bar is "100% confidence or escalate," and this was an unattended run where I could only review statically (no execution). I'm confident, but not at "merge without hesitation" for a public-contract change on that basis alone — so I'm handing the final call to a human rather than auto-approving. The two remaining caveats are the Linux-only Node CI matrix on this fork PR (macOS/Windows Node jobs skipped) and the author-reported 1s→1ms figure, which the in-progress @qwen-code /verify A/B run is what actually settles.

⏸️ Deferring to @ytahdn — this is an otherwise-clean cross-package protocol + SDK addition that needs a maintainer's sign-off on the public-contract surface (and the exported-interface change) before merge. Needs a human call on this one, not a correctness fix.

中文说明

置信度:3/5 —— 审查干净、未发现正确性问题;之所以 defer,仅因为这是一次跨包的核心基础设施改动,新增了公开的 wire 协议 + SDK 面,这一类改动需要 maintainer 拍板,而非机器人自动批准。

退一步看:这正是本仓库设计文档工作流应当产出的样子。问题在动笔前就被评审者对照代码核实(且被发现轻描淡写),两轮评审留下了可见的建议轨迹——结构性收口点、精确 no-op 语义、stamp 位置论证——实现把每一条都落在了讨论指定的位置。我对问题的独立判断也会提出同样的形态:纯内存 trusted 路由 + bridge 持有的等值令牌,缓存失效先于版本曝光。PR 与此一致,并在兜底上更进一步:路由曝光时刻的失效收掉了 bridge 内部 writer(自动标题、persisted-only branch),而不必把 acp-bridge 与 CLI 缓存层耦合。这比单纯逐点打标更好;把"版本比较分支"与"首次曝光分支"分开钉住的测试,说明作者认真想过如何证明它。

测试是最强的信号:不是断言"会递增"了事,而是在每条 no-op 路径断言精确的 revision 相等、用沉降窗口捕捉错误的多余打标、并在收口点放一个会抛错的 host 监听器。被审 commit 上的 CI 对所有 pull_request 事件运行全绿,包括 serve A/B 通道。

为什么 defer 而非 approve。 正确性不是顾虑——我已对照基础代码核实机制、未发现阻塞项。原因是改动的"类别":它横跨 acp-bridgecli/servesdk-typescript,新增公开 wire 端点 + capability + SDK 方法,并对导出的 AcpSessionBridge 接口做了源码级变更。按核心基础设施门槛,这一类的标准是"100% 确信、否则上报",而本次是无人值守运行、只能静态审查(未执行代码)。我有信心,但仅凭这些还没到"毫不犹豫直接合"的程度——所以把最终决定权交给人,而不是自动批准。剩余两个保留项:fork PR 上 Node CI 仅跑 Linux(macOS/Windows Node 被跳过);约 1s→1ms 的数字是作者自报,真正了结它的是进行中的 @qwen-code /verify A/B 运行。

⏸️ 转交 @ytahdn —— 这是一次本来很干净的跨包协议 + SDK 新增,合并前需要 maintainer 对公开契约面(以及导出接口变更)拍板。需要人来定夺,而非正确性修复。

Qwen Code · qwen3.8-max

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

Resolve capability registry collisions: keep both
workspace_session_live_state (this PR) and workspace_session_metadata
(main) in the registry and its mirrors in serve tests.

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not explored to full depth (tool budget reached): chunk 5: SDK vitest run ( packages/sdk-typescript DaemonClient.test.ts / daemon-public-surface.test.ts) hangs in this environment — no output even for the smallest test….

Not reviewed: "agent reverse-audit (round 1)" — the agent made no tool call: it read nothing.

Not reviewed: "agent reverse-audit (round 2)" — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

Not reviewed: the whole-diff test-coverage check, the build-and-test check — its prompt was built, but no agent on record was launched with it.

中文说明

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

未探索到全部深度(达到工具调用预算):chunk 5:SDK vitest run ( packages/sdk-typescript DaemonClient.test.ts / daemon-public-surface.test.ts) hangs in this environment — no output even for the smallest test…

未审查:"agent reverse-audit (round 1)"——该 agent 未发起任何工具调用:它什么都没读。

未审查:"agent reverse-audit (round 2)"——启动 prompt 为它指定了 diff 中的行,但它从未打开:有工具调用,却没有一次读取 diff。

未审查:全 diff 测试覆盖检查、构建与测试验证——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。

— DeepSeek/deepseek-v4-flash via Qwen Code /review (v0.21.11)

Comment thread packages/acp-bridge/src/bridgeClient.ts Outdated
Comment thread docs/design/2026-08-16-workspace-session-live-state.md Outdated
Comment thread packages/cli/src/serve/routes/session.ts
The metadata route's SessionNotFoundError fallback renamed persisted
sessions without advancing the catalog revision, so version-watching
clients kept the stale display name. Mark after a successful persisted
rename (parity with the live path, which marks on an actual change).

Also reconcile the design doc summary with its Implementation
Boundaries (the implementation ships in this PR, not a follow-up) and
spell out the child-recording persistence mechanism behind the
auto-title catalog mark.
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Addressed the @ytahdn /review round (3 inline suggestions) in 1c3f3ea, plus resolved the merge conflict against main in 94a5738 (capability registry collisions — kept both workspace_session_live_state and workspace_session_metadata).

  • Persisted rename without catalog-version mark (routes/session.ts) — accepted, real contract gap. The SessionNotFoundError fallback now calls markSessionCatalogChanged() after a successful persisted rename, at parity with the live path. Tests pin mark-on-success and no-mark on 404/409.
  • Design-doc summary vs Implementation Boundaries — accepted. Summary now says implementation + SDK ship in this PR; Web Shell adoption stays the separate follow-up.
  • Auto-title catalog mark (bridgeClient.ts) — declined with evidence in-thread. The child appends the custom_title record to the session JSONL before notifying, that JSONL is the store the catalog scan reads, and live-state invalidates the cache when exposing a new revision — so the mark and the catalog agree. Handler comment expanded to make that chain verifiable in code.

Verification: server.test.ts 977/977, bridge.test.ts catalog-clock block green, repo typecheck clean, conflicted-file integration test qwen-serve-routes 36/36 (for the merge resolution).

Resolve the acp-session-bridge type-import collision: keep both
BridgePromptContentBlock (session media references, QwenLM#9310) and
BridgeSessionCatalogVersion (this PR).

Media references pushed the daemon browser bundle to 156 B under the
196 KiB budget; adding this PR's live-state daemon surface tips it
over, so bump the guard to 197 KiB with the house precedent comment.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

中文说明

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

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

Comment thread docs/design/2026-08-16-workspace-session-live-state.md Outdated
Comment thread packages/cli/src/serve/routes/scheduled-tasks.ts
Comment thread packages/cli/src/serve/create-sub-session.ts
Comment thread packages/cli/src/serve/live/live-session-coordinator.ts
Comment thread packages/cli/src/serve/routes/session.ts
Comment thread packages/cli/src/serve/routes/session.ts
Comment thread packages/cli/src/serve/routes/session.ts
Comment thread packages/cli/src/serve/routes/session.ts
ids((await organized('&archiveState=archived')).body).sort(),
).toEqual([archivedOne, archivedTwo]);

// An unchanged high-frequency poll must not invalidate again: a third

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] Both directions of this cache-invalidation test hinge on the real-time 2 s PERSISTED_SESSION_LIST_CACHE_TTL_MS window (no fake timers; assertions only check response ids) — Failure scenario: (a) flake: after the post-exposure refill, the remaining sequence must complete within 2 s; on a stalled/loaded CI runner the refilled entry expires, the rescan surfaces the hidden write, and the final toEqual fails intermittently; (b) vacuous pass: if the gap between the initial cache fill and the post-exposure organized queries exceeds 2 s, natural TTL expiry is indistinguishable from route invalidation — deleting invalidateSessionLists from the live-state route keeps the test green, losing the only end-to-end pin of invalidate-before-expose. Fix: make the assertions timing-independent — surface and assert the cache lookup status (cache_hit vs scan) on the organized queries, or inject a controllable TTL/clock for this test.

Witness (probe): baseline 3 runs pass in 126/146/138 ms; invalidateSessionLists removed + injected 2.2 s stall → mutant passes (2326 ms); route intact + 2.2 s stall before the final query → fails (received […201, …203, +…205]). Both directions flipped; tree restored.

中文说明

[建议] 该缓存失效测试的两个方向都依赖实时的 2 秒 PERSISTED_SESSION_LIST_CACHE_TTL_MS 窗口(无假时钟;断言只检查响应 id)— 失败场景:(a) 不稳定:曝光后重新填充缓存之后,剩余步骤必须在 2 秒内完成;在停顿/高负载的 CI runner 上,重填条目过期、重扫会把被隐藏的写入暴露出来,最终 toEqual 间歇性失败;(b) 空过:若首次缓存填充与曝光后 organized 查询之间的间隔超过 2 秒,自然 TTL 过期与路由失效不可区分 — 删除 live-state 路由中的 invalidateSessionLists 测试仍绿,失去对"先失效后曝光"的唯一端到端约束。修复:让断言与时间无关——在 organized 查询上暴露并断言缓存查找状态(cache_hit vs scan),或为该测试注入可控 TTL/时钟。

证据(探针):基线 3 次运行分别 126/146/138 ms 通过;删除 invalidateSessionLists 并注入 2.2 秒停顿 → 变异通过(2326 ms);路由保持完整但在最终查询前注入 2.2 秒停顿 → 失败(收到 […201, …203, +…205])。两个方向均翻转验证;工作树已还原。

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

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.

Declining for this round, reasoning recorded here. The timing-independence fix needs either a cache-lookup-status surface in responses or a controllable TTL/clock — both are test-infra changes wider than this review-round scope, and the same TTL constraint is shared by every cache test in this file, so a rewrite belongs in a dedicated change. Net today: the mechanism is pinned twice deterministically — the version-comparison test this thread anchors on, plus the new first-exposure test in 852b7ea that fills both scopes before any live-state request and asserts refills with the revision unchanged. The flake direction your probe measured is real but needs a >2 s stall mid-test (~13x the observed 130-150 ms budget); I would rather address it holistically than re-plumb TTL inside a review round. Happy to file a follow-up issue if you want it tracked.

Comment thread packages/sdk-typescript/test/unit/daemon-public-surface.test.ts Outdated
@wenshao

wenshao commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification: built and ran this PR locally against a real daemon — no blocking issues

I built both the PR head (bbbdaf4) and its merge-base (c0791f0) from source and drove a real qwen serve daemon — real workspace runtimes, a real qwen --acp child, a real streaming turn held open by a mock OpenAI server, and a deliberately large persisted store. This is an independent local run on real hardware, complementary to the sandboxed bot verification above; it is evidence for reviewers, not an approval.

Result: 49/49 end-to-end checks pass on the PR build, the same probe returns 404 / no capability on the base build, and 4 single-point mutations of the shipped bundle each kill exactly the checks they should. Two behavioural nuances worth a reviewer's eye are noted at the end; neither blocks merge.

Environment
Host macOS (Darwin 25.6.0), 10 cores, Node v24.18.1, npm 11.16.0
PR build bbbdaf4e6anpm ci && npm run bundlenode dist/cli.js serve
Base build c0791f04505b64f119ff5abe94d810bb7041a0ab (merge-base with main), built the same way
Isolation per-run HOME + QWEN_HOME, QWEN_RUNTIME_DIR cleared, --port 0, loopback bind, bearer token
Model local mock OpenAI server that can hold a streaming turn open indefinitely (no API key, no network)
Harness pushed for reproduction: verify/pr9261-local-maintainer-1/harness (scripts + raw JSON ledgers)

1. Discovery and wire contract (PR build vs base build)

protocol E2E on the PR build

PR build base build
/capabilities feature tags 118, includes workspace_session_live_state 117, tag absent
GET /workspaces/:ws/sessions/live-state 200 {v:1, catalogVersion, sessions} 404 Cannot GET

same probe on the base build

Contract checks that passed on the PR build: exactly the three documented top-level keys; Cache-Control: no-store on every success; idle runtime returns 200 with sessions: [] (not 404); repeated reads with no mutation return the same version pair.

Trust and routing — verified against a daemon with security.folderTrust enabled, a trusted primary and an explicitly DO_NOT_TRUST secondary:

  • untrusted secondary → 403 untrusted_workspace
  • differential: the same untrusted workspace still serves GET .../sessions with 200, while live-state returns 403 — i.e. the route genuinely does not inherit the permissive untrusted persisted-read policy, which is the security claim the design rests on
  • unknown selector → 400 workspace_mismatch, never silently answered by the primary runtime
  • no bearer token → 401

2. Catalog-version semantics against real REST mutations

Every mutation below ran against seeded persisted sessions that were confirmed present in the catalog first (D0), so a finally-only mark cannot fake the deltas.

Mutation Δrevision
create / update session group +1 / +1
PATCH session/:id/organization (pin) +1
PATCH session/:id/metadata (real rename) +1
archive / unarchive / delete session +1 each
delete group, deleted: true +1
delete the same group again, deleted: false +0 ✅ exactness gate
3× catalog list + 3× live-state (pure reads) +0
generation across all of the above unchanged
daemon restart new generation, revision 10 → 0

Cache coherence also holds end to end: after a mutation, live A ≠ live B, the full catalog read issued after live B reflects the mutation inside the 2 s cache TTL, and live C equals live B so the handshake closes and the bundle is acceptable.

The TypeScript SDK works against the live daemon: DaemonClient.getWorkspaceSessionLiveState(cwd) and WorkspaceDaemonClient.getSessionLiveState() both return the typed snapshot and agree, and the untrusted workspace surfaces as a thrown error rather than a silent degradation.

3. The central claim: volatile state moves, the version does not

This is the part that only a real session can prove. A real live session (real qwen --acp child) with a prompt whose model stream I held open on demand:

live session, held streaming turn

  • the live row projects exactly the five documented wire fields
  • hasActivePrompt flips false → true while the stream is held, stays true across 10 consecutive polls, and returns to false when I release the stream
  • across those 10 in-flight polls the version stayed at a single value, identical to the pre-prompt value — start, streaming and finish left catalogVersion untouched
  • a second attached client raises clientCount 1 → 2 with no version change
  • live rename: first (real) rename +1, repeating the same displayName +0 — the exact no-op gate
  • closing the session removes the row and advances the version
  • a 40 s post-turn observation window recorded zero background version movement (1 model request total)

4. Cost, on a store built to be hostile

1500 persisted sessions, 168 MiB of JSONL, measured at the sidebar's own 2 s cadence (rapid polling would sit inside the 2 s TTL and flatter the catalog path):

cost comparison

path median response
GET .../sessions?view=organized at 2.1 s cadence (real scan) 428–454 ms 11 244 B
GET .../sessions default cursor (uncached) 19.6 ms 10 167 B
GET .../sessions/live-state 2.6–2.7 ms 105 B

164–166× cheaper across two independent runs. Live-state latency is the same whether polled rapidly or at 2.1 s spacing (memory-only, no TTL to miss), its response size is independent of the persisted session count, and polling it spawned no additional daemon child process.

5. Falsification: are these checks actually load-bearing?

I applied four single-point mutations to the built bundle, re-ran the same suites, and restored the bundle (sha256-verified back to the original):

falsification matrix

mutant effect checks killed
markSessionCatalogChanged() → no-op clock stops advancing 11 (D1D9, E1, G2)
group delete marks even on deleted: false no-op exactness dropped 1 (D10)
emitSessionLifecycle() stops marking live membership invisible 1 (F1)
live rename loses its !== gate no-op rename would advance 1 (F11)

Each mutation kills exactly the assertions it should and nothing else — the green run is not green by accident.

6. Repository test suites, run locally on this branch

suite result
acp-bridge-t "session catalog version clock" 12/12 (matches the PR description)
acp-bridgesrc/bridge.test.ts full 711/711
acp-bridge — whole package 30 files, 1541/1541
cli — the 8 listed serve suites + the 2 other touched ones 10 files, 1680/1680
sdk-typescriptDaemonClient + daemon-public-surface 358/358

(The PR description quotes 783 and 355 for the bridge and SDK suites; locally I measure 711 / 1541 and 358. Counts only — everything passes.)


Notes for reviewers (non-blocking)

  1. "No-op renames" is exact only on the live path. For a persisted-only session, PATCH .../metadata with an unchanged displayName still advances the revision by 1. That is correct rather than a leak: SessionService.renameSession() appends a fresh custom_title record and returns true on every call, so the store really did change. But the PR description lists "no-op renames" alongside deleted: false as exact-no-op semantics, and that reads as covering both paths. Worth one clarifying clause, since the protocol already permits conservative increments anyway.

  2. One non-reproducing extra increment. In an early run, a repeated identical rename on a live session showed Δ=1 instead of Δ=0. It did not reproduce in six subsequent runs (including a 40 s quiet-window probe that saw no background marks), and conservative extra increments are explicitly protocol-legal, so this is a note rather than a finding. Clients must not assume "nothing else marked in between" — which the design already states.

  3. Not covered by this run (unit tests only, or out of scope): untrusted primary 403, the transitioning-generation 503 + Retry-After path, the workspace-id selector form (I exercised the encoded-cwd form), the ACP-transport mutation matrix, Windows/Linux, and Web Shell adoption (explicitly a follow-up PR).

Verdict: the implementation behaves exactly as the protocol document describes, the security boundary is real and differentially demonstrated, the performance argument holds on a 168 MiB store, and the version-exactness claims survive targeted falsification. LGTM from my side as merge evidence.

中文版

维护者验证:本地构建并在真实 daemon 上跑通本 PR —— 未发现阻塞问题

我从源码分别构建了 PR head(bbbdaf4)与其 merge-base(c0791f0),驱动真实的 qwen serve daemon:真实 workspace runtime、真实 qwen --acp 子进程、由 mock OpenAI 服务端按需挂住的真实流式回合,以及刻意做大的持久化 store。这是一次在真实机器上的独立本地验证,与上面沙箱机器人的验证互补;它是给评审者的证据,不是批准。

结果:PR 构建上 49/49 端到端检查全绿;同一探针在 base 构建上返回 404 且不广播 capability;对已构建产物做的 4 个单点变异,各自精确杀死其应当杀死的检查。 文末有两条值得评审者留意的行为细节,均不阻塞合并。

环境
主机 macOS(Darwin 25.6.0),10 核,Node v24.18.1,npm 11.16.0
PR 构建 bbbdaf4e6anpm ci && npm run bundlenode dist/cli.js serve
Base 构建 c0791f04505b64f119ff5abe94d810bb7041a0ab(与 main 的 merge-base),同样方式构建
隔离 每次运行独立 HOME + QWEN_HOME,清空 QWEN_RUNTIME_DIR--port 0,回环绑定,bearer token
模型 本地 mock OpenAI 服务端,可无限期挂住流式回合(无 API key、无外网)
Harness 已推送便于复现:verify/pr9261-local-maintainer-1/harness(脚本 + 原始 JSON 台账)

1. 能力发现与 wire 契约(PR 构建 vs base 构建)

PR 构建 base 构建
/capabilities 特性标签 118 个, workspace_session_live_state 117 个,该标签
GET /workspaces/:ws/sessions/live-state 200 {v:1, catalogVersion, sessions} 404 Cannot GET

PR 构建上通过的契约检查:顶层恰好三个文档化字段;每次成功响应均带 Cache-Control: no-store;空闲 runtime 返回 200sessions: [](而非 404);无 mutation 时重复读取返回完全相同的版本对。

信任与路由 —— 在开启 security.folderTrust、主工作区受信、次工作区显式 DO_NOT_TRUST 的 daemon 上验证:

  • 不受信次工作区 → 403 untrusted_workspace
  • 差分证据:同一个不受信工作区,GET .../sessions 仍返回 200,而 live-state 返回 403 —— 该路由确实没有继承对不受信 secondary 的宽松持久化读策略,这正是设计所依赖的安全性主张
  • 未知 selector → 400 workspace_mismatch,绝不回落到 primary runtime
  • 无 bearer token → 401

2. 针对真实 REST mutation 的 catalog 版本语义

下列每一项都作用在"先确认已出现在 catalog 中"的持久化会话上(D0 前置断言),因此仅靠 finally 打标无法伪造这些增量。

Mutation Δrevision
创建 / 更新 session group +1 / +1
PATCH session/:id/organization(置顶) +1
PATCH session/:id/metadata(真实重命名) +1
归档 / 取消归档 / 删除会话 各 +1
删除 group,deleted: true +1
再次删除同一 group,deleted: false +0 ✅ 精确性闸门
3 次 catalog 列表 + 3 次 live-state(纯读) +0
以上全过程中的 generation 不变
daemon 重启 generationrevision 10 → 0

缓存一致性也端到端成立:mutation 后 live A ≠ live B;在 live B 之后发起的全量 catalog 读取在 2 秒缓存 TTL 之内即反映该 mutation;live C 等于 live B,握手闭合,bundle 可接受。

TypeScript SDK 对真实 daemon 可用:DaemonClient.getWorkspaceSessionLiveState(cwd)WorkspaceDaemonClient.getSessionLiveState() 都返回带类型的快照且一致;不受信工作区以抛错形式暴露,而非静默降级。

3. 核心主张:volatile 状态会动,版本不动

这一点只有真实会话能证明。真实 live session(真实 qwen --acp 子进程),其模型流被我按需挂住:

  • live 行恰好投影五个文档化 wire 字段
  • 流挂起期间 hasActivePromptfalse → true,连续 10 次轮询保持 true,释放后回到 false
  • 这 10 次回合内轮询中版本始终是同一个值,且等于发 prompt 之前的值 —— 起始、流式、结束都没有触动 catalogVersion
  • 第二个 attach 的客户端把 clientCount 从 1 提到 2,版本不变
  • live 重命名:首次(真实变更)+1,重复相同 displayName +0 —— 精确 no-op 闸门
  • 关闭会话后该行消失且版本推进
  • 回合结束后 40 秒观察窗内记录到次后台版本移动(全程仅 1 次模型请求)

4. 在刻意做大的 store 上的开销

1500 个持久化会话、168 MiB JSONL,按 sidebar 自身的 2 秒节奏测量(快速轮询会落在 2 秒 TTL 内,反而美化 catalog 路径):

路径 中位数 响应体
GET .../sessions?view=organized,2.1 秒节奏(真实扫描) 428–454 ms 11 244 B
GET .../sessions 默认游标(无缓存) 19.6 ms 10 167 B
GET .../sessions/live-state 2.6–2.7 ms 105 B

两次独立运行均为 164–166 倍更便宜。live-state 的时延在快速轮询与 2.1 秒间隔下相同(纯内存,没有 TTL 可错过),响应体大小与持久化会话数无关,且轮询它没有派生任何额外的 daemon 子进程。

5. 反证:这些检查真的有承载力吗

我对已构建的产物施加了四个单点变异,重跑同样的套件,然后还原产物(sha256 校验与原始一致):

变异 效果 被杀死的检查
markSessionCatalogChanged() → 空操作 时钟停止推进 11 项(D1D9E1G2
group 删除在 deleted: false 时也打标 丢掉 no-op 精确性 1 项(D10
emitSessionLifecycle() 不再打标 live 成员变化不可见 1 项(F1
live 重命名失去 !== 门控 no-op 重命名也会推进 1 项(F11

每个变异精确杀死其应当杀死的断言,且不多杀 —— 绿色结果不是碰巧绿的。

6. 本地在本分支上跑的仓库测试套件

套件 结果
acp-bridge —— -t "session catalog version clock" 12/12(与 PR 描述一致)
acp-bridge —— src/bridge.test.ts 全量 711/711
acp-bridge —— 整包 30 个文件,1541/1541
cli —— PR 列出的 8 个 serve 套件 + 另外 2 个被改动的 10 个文件,1680/1680
sdk-typescript —— DaemonClient + daemon-public-surface 358/358

(PR 描述中 bridge 与 SDK 分别写的是 783 与 355;我本地测得 711 / 1541 与 358。仅计数差异,全部通过。)

给评审者的说明(非阻塞)

  1. "no-op 重命名"只在 live 路径上是精确的。仅持久化的会话,用未变化的 displayNamePATCH .../metadata 仍会把 revision +1。这是正确的而非泄漏:SessionService.renameSession() 每次调用都会追加一条新的 custom_title 记录并返回 true,存储确实变了。但 PR 描述把"no-op 重命名"与 deleted: false 并列为精确 no-op 语义,读起来像是覆盖了两条路径。考虑到协议本来就允许保守多增,补一句限定即可。

  2. 一次未能复现的额外增量。 早期某次运行中,对 live 会话重复同名重命名出现 Δ=1 而非 Δ=0。随后六次运行(含一次 40 秒静默窗探针,未观察到任何后台打标)均未复现,且协议明确允许保守多增,因此这是说明而非缺陷。客户端不应假设"两次读之间没有别的打标"—— 设计文档已经这样写了。

  3. 本次未覆盖(仅有单元测试,或本就在范围外):不受信 primary403、generation 迁移中的 503 + Retry-After 路径、workspace id 形式的 selector(我跑的是编码 cwd 形式)、ACP transport 的 mutation 矩阵、Windows/Linux,以及 Web Shell 接入(明确是后续 PR)。

结论: 实现的行为与协议文档描述完全一致,安全边界真实存在并有差分证据,性能论证在 168 MiB store 上成立,版本精确性主张也经受住了定向反证。从我这边看,作为合并证据 LGTM。

…w round

- Assert markSessionCatalogChanged in the scheduled-task rollback
  (including the no-op-removal negative case), the sub-session and
  Live coordinator rollback paths, and the never-live orphan
  deletion; previously each mark could regress with suites green.
- Cover the live-state route's ?? false projection for both wait
  flags, and its first-exposure invalidation arm (revision
  unchanged, both organized scopes refilled).
- Cover the side-task generation-closed rollback arm (kill, remove,
  catalog mark).
- Compile the SDK live-state type fence via tsconfig.test-fence.json
  so shape assertions really pin the wire contract; the default
  tsconfig excludes test/.
- Align the design doc's cache-consistency goal with the cache
  mechanics (waiters joined before an invalidation may resolve, but
  cannot install).
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

Addressed the qwen-code-ci-bot /review round (12 inline suggestions from 11:26 UTC) in 852b7ea (local HEAD == PR head). 10 accepted and pinned, 2 declined with reasoning in-thread.

# Thread Suggestion Action
1 design doc Goals bullet Overstates cache-consistency guarantee Accepted with scope correction — bullet now scopes the guarantee (852b7ea)
2 scheduled-tasks.ts cleanup-path mark Not pinned by any test Accepted — StubBridge carries markSessionCatalogChanged spy; pinned (852b7ea)
3 create-sub-session.ts rollback mark Not pinned Accepted — fake mark is now vi.fn() + rollback test (852b7ea)
4 live-session-coordinator.ts rollback mark Not pinned Accepted — new setup-failure rollback test (852b7ea)
5 session.ts side-task rollback (generation-closed arm) Not pinned Accepted — new server.test.ts test drives the generation-closed arm (852b7ea)
6 session.ts fire-and-forget arm Not pinned Declined this round — reasoning recorded in-thread; thread left open
7 session-archive.test.ts never-live orphan mark Unasserted Accepted — mock captured + toHaveBeenCalledTimes(1) (852b7ea)
8 session.ts first-exposure invalidation arm Unobserved Accepted — new test fills both organized scopes (852b7ea)
9 isWaitingForPermission ?? false Measured mutant survivor Accepted — defaults both wait flags to false when the bridge omits them (852b7ea)
10 isWaitingForUserQuestion ?? false Measured mutant survivor Accepted — same test pins both flags (852b7ea)
11 multi-workspace-sessions.test.ts timing-independence Redundant setup Declined this round — reasoning recorded in-thread; thread left open
12 SDK fence type-level half Compiled by no tsc pass Accepted — tsconfig.test-fence.json wired into npm run typecheck (852b7ea)

Local verification (env-stripped, sequential): create-sub-session 35/35 · live-session-coordinator 24/24 · multi-workspace-sessions 117/117 · scheduled-tasks 86/86 · server 1002/1003 (sole failure is the pre-existing rejects unauthorized POST without parsing the (possibly huge) body timing flake — present on main, passes in isolation) · session-archive 43/43 · SDK daemon-public-surface 15/15 + npm run typecheck incl. the new fence tsc pass exit 0.

The 10 accepted threads are being batch-resolved; the 2 declined threads stay open for reviewer disposal.

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM — approving after a full pass on head 852b7ea.

Scope & linkage. All 40 files match the stated goal: design doc, trusted-only GET /workspaces/:workspace/sessions/live-state route, in-memory catalog clock in the bridge, unconditional workspace_session_live_state capability, SDK wire types + accessors, telemetry tag, and docs. No unrelated changes.

Auth gate verified against siblings. The new route uses requireTrustedRuntimeForWorkspaceRoute (routes/session.ts) — strictly stronger than the catalog GET sibling (which permits untrusted-secondaries) and consistent with the workspace-qualified mutation routes; no primary fallback for unknown selectors; per-request trust check. As a read-only GET it correctly skips the client-id header, same as the sibling catalog list route.

Correctness verified from head sources:

  • Catalog clock: per-bridge generation (randomUUID) + monotone revision; getters return fresh snapshots (bridge.ts).
  • Lifecycle choke point: all 7 byId map mutations (set/delete/clear) are paired with emitSessionLifecycle, and the mark fires before the host callback, so a throwing sessionLifecycle listener cannot suppress a revision advance.
  • Invalidate-before-expose: the route synchronously invalidates both persisted cache scopes before answering a never-exposed version and records lastExposed in a WeakMap keyed on the bridge (runtime replacements drop cleanly); double generation-guard assertions bracket the critical section.
  • No-op semantics preserved: deleted: false group deletes and unchanged renames do not advance the revision; the persisted-rename fallback now marks (round-1 fix).
  • Response surface is a strict subset of what the catalog route already exposes (sessionId + 4 volatile fields), Cache-Control: no-store, v: 1 envelope — no new data exposure.
  • SDK types match the route contract exactly; mode: 'rest' pins native transport; the documented no-per-poll-capability-pre-flight tradeoff is sound; bundle budget bump is justified; the type-fence now has a real tsc compile pass via tsconfig.test-fence.json.

Review history & CI. Both prior rounds (3 + 12 suggestions) are fully dispositioned — accepted fixes are in this head, declines carry in-thread reasoning. The head commit beyond wenshao's real-daemon verification is tests/docs only, and all non-skipped checks on head are green (Test, Desktop Shell ubuntu/windows, Live Host macOS, Real daemon E2E matrix, Serve A/B, TruffleHog, CVE audit, precheck). The serve A/B diff confirms the only wire-visible change is the new capability tag. mergeable: true; the blocked state is only the pending approval.

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

Verdict: Approve

No Criticals found across all review dimensions (correctness, security, performance, tests, reverse audit).

The implementation faithfully follows the design doc's protocol contract — the bridge clock (generation + revision), the no-await live-state route ordering, the WeakMap cache-exposure tracking, the trust gate before bridge access, and the invalidate-then-mark persisted mutation integration are all correct.

Additional suggestions (non-blocking, body-only)

4. session-archive.ts — The orphan mark test covers the positive case (persisted removal succeeds → mark fires), but no test covers the deleteDaemonSessionIfOrphan early-return path where the session is not an orphan (returns false before the mark). Consider adding a test that asserts the mark does NOT fire when the function returns false.

5. session.ts generation re-assertion — The assertRuntimeOpen?.() re-assertion after bridge reads guards against runtime replacement, but no test exercises the actual 503 path. Consider adding a test that closes the runtime generation between listWorkspaceSessions and the re-assertion.

6. deleteDaemonSessionIfOrphan unconditional mark (low confidence) — markSessionCatalogChanged fires unconditionally after persisted removal succeeds, even if killSession reported notFound. The design explicitly permits conservative extra increments, so this is likely by design. Flagged only for the author's consideration.

}),
);
invalidateSessionListsAndMarkCatalog(runtime, ['active', 'archived']);
res.status(201).json({ group });

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] Mid-request generation close untested

The second assertRuntimeOpen?.() guards against a runtime replacement between the bridge reads and the response — if the generation closed, this should throw and map to a 503. However, no test exercises this mid-request path.

Consider adding a test that closes the runtime generation between listWorkspaceSessions() and this re-assertion, verifying the response is 503.

});
});

it('invalidates both organized cache scopes on the first live-state exposure, revision unchanged', async () => {

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] Live→catalog→live handshake race untested

The client consistency handshake (live-state A → full catalog load → live-state B, comparing catalogVersion) is the core protocol guarantee, but no test covers the case where a mutation occurs between A and B. The first-exposure test here covers the case where revision is unchanged, but not where it changes between two live-state calls.

Consider adding a test that mutates between two live-state calls and asserts B's catalogVersion differs from A's, and that the pre-exposure invalidation fires before B answers.

const transcriptRemoved = await new SessionService(
boundWorkspace,
).removeSession(spawnedSession.sessionId);
if (transcriptRemoved) bridge.markSessionCatalogChanged();

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] Negative rollback path untested

The positive case (removeSession returns true → mark fires) is well-tested, but the negative path where removeSession returns false (transcript not found) is not covered. Consider adding a test where removeSession resolves to false and asserting markSessionCatalogChanged was NOT called.

@doudouOUC
doudouOUC added this pull request to the merge queue Aug 17, 2026
Merged via the queue into QwenLM:main with commit f16975e Aug 17, 2026
422 of 430 checks passed
@doudouOUC
doudouOUC deleted the agent/workspace-session-live-state-design branch August 17, 2026 15:03
rockybot2026 added a commit to rockybot2026/qwen-code that referenced this pull request Aug 17, 2026
Main moved past our base (last: QwenLM#9261 workspace session live-state),
which conflicted with this branch on the sdk package.json typecheck
script. Resolved by keeping both tsc projects (tsconfig.typetest.json
from QwenLM#8978 and tsconfig.test-fence.json from QwenLM#9261). All other overlap
(channel/serve/sdk files touched on both sides) auto-merged cleanly.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.14.

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.

5 participants