# PJ2026-0104010803 Workbench唯一投影

## 修改历史

| 版本 | 对应 commit id | 更新日期 | 变更说明 |
| --- | --- | --- | --- |

当前正文仍在规格治理草稿中；未定稿前不新增版本号，不为单次编辑追加 `待提交` 版本。

## 正文

## PJ2026-0104010803 Workbench唯一投影需求规格

## 1. 文档控制

| 字段 | 内容 |
| --- | --- |
| 编号 | PJ2026-0104010803 |
| 短名 | Workbench唯一投影 |
| 层级 | L4 专项规格切片 |
| 状态 | 已生效 |
| 实现引用版本 | draft-2026-06-20-p0-durable-facts-model; draft-2026-06-20-p1-view-local-timing-ticker; draft-2026-06-20-p1-zero-split-durable-realtime; draft-2026-06-20-p2-terminal-outbox-recovery; draft-2026-06-22-p1-workbench-redis-derived-cache; draft-2026-06-24-p0-no-ui-timing-fabrication; draft-2026-06-24-p0-aggregate-event-stream; draft-2026-06-24-p1-opencode-message-part-authority; draft-2026-06-25-p0-serve-session-aggregate-authority; draft-2026-06-25-p0-session-warm-runner-contract; draft-2026-06-27-p0-read-model-timeline-contract; draft-2026-06-28-p0-d518-session-timeline-consistency |
| 需求规格模板 | [ISO/IEC/IEEE 29148 需求规格模板](../../templates/iso-iec-ieee-29148-requirements-spec-template.md) |
| 上级规格 | [PJ2026-010401 Web工作台](PJ2026-010401-web-workbench.md) |
| 关联规格 | [PJ2026-010403 API契约](PJ2026-010403-api-contract.md)、[PJ2026-01060505 Workbench性能](PJ2026-01060505-workbench-performance.md)、[PJ2026-010205 HWLAB接入](PJ2026-010205-hwlab-dispatch.md)、[PJ2026-0102 Agent编排](PJ2026-0102-agent-orchestration.md) |
| 规格治理索引 | [规格治理](spec-governance.md) |

本文采用 ISO/IEC/IEEE 29148 需求规格模板的项目裁剪版：正文只保留 Workbench 唯一投影的稳定使命、范围、术语、系统边界、内部分工和原子需求。

## 2. 目的和范围

### 2.1 目的

Workbench唯一投影负责把 AgentRun 执行事实收敛成 HWLAB 自有的 durable Workbench facts，使 Web、CLI、REST、SSE、fake-server 和浏览器回归都消费同一份可恢复、可分页、可诊断的用户态会话事实。

本专项的目标状态是：AgentRun run、command、event 和 result 只作为执行事实输入；HWLAB 只有 `WorkbenchProjectionWriter`、`WorkbenchProjectionFinalizer` 写入 Workbench facts；cloud-api boot/background scheduler 是 startup/resume 的 authority，负责扫描 durable open checkpoint 和 running/projecting turn 并恢复追平；投影推进只能由上游 source event/result、projection writer/finalizer、background scheduler/reconciler 或显式受控 checkpoint replay/reprojection 触发。所有 `GET /v1/workbench/*` 和兼容读路径都只通过 `WorkbenchReadModel` 读取，不在读取时调用 AgentRun、Code Agent manager、trace polling、result polling、`hydrateRealtimeGap` 或 workspace repair 推进事实。Workbench 必须满足 0repair：页面、GET、SSE、fake-server 和 web-probe 都不得通过 reload、切换 session、`sessionRepair`、`realignFreshSession`、localStorage truth、SSE gap repair、visibility gap refresh 或 read-through repair 把已经分裂的 route/session/message/trace 状态补成看起来正确。

Workbench aggregate event stream 是上述唯一投影的提交脊柱。所有 admission、AgentRun event/result、cancel、replay/reprojection 和 diagnostic transition 先归一化为带 `eventSeq`、`aggregateId`、`aggregateSeq`、`turnId`、`traceId`、`sourceRunId`、`sourceCommandId` 和来源幂等键的 append-only 事件，再由同一 projector 写入 message、part、turn、trace、checkpoint、outbox 和 read model。Web、SSE、CLI、fake-server 和 probe 只能消费该 event stream 的投影结果或 cursor replay，不能分别从 trace tail、result envelope、message cache、session list 和浏览器本地状态重新排序。

Workbench message/part authority 必须采用类似 OpenCode serve 的职责分离：prompt admission、session status、message page、message part、event stream 和 abort/cancel 分别有清晰 authority，最终由同一投影提交序收敛。用户消息、assistant 文本、tool/command 行、diagnostic、steer/cancel 控制事件和 final response 必须是稳定 `messageId/partId/order/status/sealedAt` 的 part 事实；禁止把同一 trace 的全部 assistant 输出压成单个 trace-level assistant message，再由 read model、前端 store、CLI renderer 或 analyzer 根据文本内容重新选择“最终回答”。

AgentRun terminal outbox 和 command result 是 Code Agent turn terminal 的上游权威输入之一。Projection finalizer 必须以 at-least-once、幂等方式消费对应 run/command 的 terminal outbox/result；即使 events page 没有出现 terminal event、cloud-api 曾重启、进程内 poller 丢失或 result sync 曾超时，后台 resume 也必须重新读取 command result authority 并把真实 terminal 通过同一 writer 写入 Workbench facts。用户 cancel、GET trace/detail、Web reload 或 CLI renderer 都不能代替这个写侧恢复。

Terminal final response 是 Workbench 投影的 sealed 用户结果。Projection writer/finalizer 一旦把 assistant final text、terminal status 和 `sealedAt` 或等价 sealed 标记写入同一 durable terminal commit，主消息区的正文、finalResponse 和 terminal status 就只能由后续同一写侧投影的受控 replay/reprojection 修正；turn polling、trace hydration、SSE gap、realtime timeout、transport close、read model lag 或 compat wrapper 失败只能写入 trace detail、transport diagnostic、projection diagnostic 或 session health，不能覆盖 sealed 主正文。

CLI trace 视图是 web-probe 工测和自动判别器的人工可读基准。`web-probe collect/observe` 只负责按固定频率保存采样事实；`analyze turn-summary` 和 `analyze trace-frame` 从既有采样事实或同源 read model 渲染文字版 trace 截图，不新增第二套保存来源。自动判别器的 finding 必须能被 CLI trace 视图复核；当二者冲突时，以 CLI trace 视图暴露的有序 turn/message/part/final response 事实作为需要修复的可见症状。

D601 v0.3 可以在 `hwlab-v03` namespace 内为 `hwlab-workbench-runtime` 使用 Redis 派生读缓存，但该缓存只能保存 WorkbenchReadModel 从 durable facts 组装出的短 TTL 快照。Redis 不写入 Workbench facts，不推进 checkpoint，不生成 lifecycle、terminal、final response 或 projectionStatus；缓存 miss、stale 或 unavailable 只能改变读路径性能诊断，不能改变唯一投影事实。

### 2.2 范围内

- AgentRun facts 到 Workbench facts 的标准映射、幂等写入和 terminal commit 语义。
- `session`、`message`、`part`、`turn`、`trace event`、`projection checkpoint` 和 `projection diagnostic` 的 durable facts schema。
- `WorkbenchFactsStore`、`WorkbenchProjectionWriter`、`WorkbenchProjectionFinalizer` 和 `WorkbenchReadModel` 的组件边界。
- AgentRun events 增量拉取 cursor、projection state、result sync state 和 cloud-api 重启后按 durable cursor 继续投影的语义。
- Workbench aggregate event stream 的 append-only 事件 schema、全局 cursor、aggregate cursor、来源幂等键、projector 边界和投影 revision。
- cloud-api 进程重启、进程内后台任务丢失、慢任务超过短轮询预算后的 durable finalizer 追平语义。
- Terminal final response 的 sealed 字段、diagnostic 分仓和 sealed 后不可被读侧失败覆盖的显示语义。
- `GET /v1/workbench/*` 纯读、`/v1/agent/*` compat wrapper 降级和 projection diagnostics 输出要求。
- WorkbenchReadModel 内部的 Redis 派生读缓存 key、payload、失效、TTL 和降级边界。
- Web reducer、CLI renderer、web-probe analyze/collect、fake-server fixture 和 Playwright 回归对同一 durable projection 的消费规则。
- 本专项范围内新增或重构源码文件的 SPEC 头部引用规则。
- D518 HWLAB v0.3 Workbench fake-echo 与 dsflash-go 原入口哨兵对 timeline、refresh、session switch、timing 与 OTel 可见性的关闭验收合同。

### 2.3 范围外

- AgentRun run/command/runner job 的执行生命周期事实归 [PJ2026-0102 Agent编排](PJ2026-0102-agent-orchestration.md)。
- HWLAB 到 AgentRun 的 dispatcher、runtime assembly、manual dispatch 和 provider profile 归 [PJ2026-010205 HWLAB接入](PJ2026-010205-hwlab-dispatch.md) 及 Agent编排下级规格。
- API path、HTTP schema、错误 envelope 和 route policy 归 [PJ2026-010403 API契约](PJ2026-010403-api-contract.md)。
- Web 布局、组件、浏览器交互和可见 UX 归 [PJ2026-010401 Web工作台](PJ2026-010401-web-workbench.md)。
- 用户身份、权限、额度、账本和 owner visibility 归 [PJ2026-0105 用户管理](PJ2026-0105-user-management.md)。
- CI/CD、GitOps、公开入口、Prometheus 和运行面运维归 [PJ2026-0106 平台运维](PJ2026-0106-platform-ops.md)。
- Redis 部署、镜像、资源、NetworkPolicy、TTL 数值和指标接入归 [PJ2026-01060505 Workbench性能](PJ2026-01060505-workbench-performance.md) 与 YAML-first 运维；本专项只定义其不能改变唯一投影 authority。

## 3. 术语表

| 术语 | 定义 |
| --- | --- |
| AgentRun facts | AgentRun 产生的 run、command、runner job、event、result、terminalStatus、failureKind、sourceSeq 和 sourceEventId 等执行事实。 |
| Workbench facts | HWLAB 自有的 session、message、part、turn、trace event、checkpoint 和 diagnostic 持久事实，用于用户态 Workbench 展示。 |
| WorkbenchProjectionWriter | 唯一把 normalized AgentRun facts 写成 Workbench facts 的组件；它不处理 HTTP route、鉴权、UI transient state 或兼容 API 输出。 |
| WorkbenchProjectionFinalizer | 以 checkpoint 为驱动的幂等追平组件；可由 submit 后、event 到达、后台恢复或显式 checkpoint replay/reprojection 触发，但所有入口调用同一 finalization 逻辑。 |
| WorkbenchFactsStore | durable facts 存储接口，负责幂等 upsert、terminal commit、checkpoint、diagnostic、分页查询和 owner visibility 过滤所需的持久化读写。 |
| Workbench aggregate event stream | Workbench 投影自己的 append-only 事件流，承载 admission、source event/result、terminal、diagnostic、replay/reprojection 和 cancel transition；它是 trace/timeline 顺序、SSE replay cursor 和 projector revision 的唯一提交序。 |
| message/part authority | Workbench timeline 的消息和片段权威。每个 user/assistant/tool/diagnostic/final response 片段都有稳定 `messageId`、`partId`、`order`、`status` 和 sealed 状态；trace-level 文本、DOM 行、result envelope 或 analyzer 不能替代它选择最终内容。 |
| WorkbenchReadModel | 唯一读取 Workbench facts 并组装 session rail、session detail、message page、turn snapshot、trace event page 和 projection diagnostics 的读模型。 |
| 上游投影推进 | Workbench facts 只能由 projection writer/finalizer、background scheduler/reconciler、AgentRun source event/result outbox 或显式受控 checkpoint replay/reprojection 推进；REST GET、Web 页面、SSE consumer、SSE open/error/visibility handler、web-probe、fake-server 和 CLI renderer 只能观察或展示投影与诊断，不能以 gap hydration、trace/result polling、reload 或 read-through sync 触发写侧投影。 |
| checkpoint replay/reprojection | 受控管理入口按已持久化 sourceRun/sourceCommand/checkpoint 重放投影逻辑，用于恢复投影 lag 或阻塞；它只能调用同一 finalizer/writer，不由 GET、Web 页面、SSE 订阅或测试 helper 触发，也不得改变 active session 或 route。 |
| projection commit | writer/finalizer 对一组 message、part、turn、trace、session summary 和 checkpoint 的一次幂等持久化提交；terminal commit 必须保持用户可见事实一致。 |
| terminal commit | 标记同一 turn 结束的 projection commit，必须原子更新 assistant final text、message/part status、turn terminal、trace terminal event、session running=false、summary 和 SSE cursor。 |
| sealed final response | terminal commit 写入的 assistant 主正文、finalResponse、message/turn terminal status 和 `sealedAt` 或等价 sealed 标记；它是主消息区用户结果的唯一权威，不参与后续读侧 diagnostic 竞争。 |
| eventSeq | Workbench aggregate event stream 的全局单调提交序，用于 SSE replay、审计和 projector recovery；它不等同于 AgentRun sourceSeq，也不由浏览器生成。 |
| aggregateId/aggregateSeq | session、turn、trace 或 message 这类 Workbench aggregate 内部的稳定身份和单调序列，用于同一 aggregate 内的 ordering、去重和 revision 判断。 |
| projection revision | projector 成功应用到 read model/facts 后产生的 revision，可由 `eventSeq`、`aggregateSeq` 或等价版本字段表达；API/SSE/Redis cache 只能用它判断新鲜度，不能用本地到达顺序替代。 |
| projectedSeq | HWLAB trace event 和 projection commit 的单调序列，用于 REST 分页、SSE replay、fake-server fixture 和审计；不得与 AgentRun `sourceSeq` 混用。 |
| sourceSeq/sourceEventId | AgentRun event/result 的来源序列或来源事件标识，只作为映射审计字段和幂等输入，不作为 Workbench trace page cursor。 |
| checkpoint | finalizer 已成功投影到某个 sourceRunId/sourceCommandId/sourceSeq 的 durable 位置记录，用于 cloud-api 重启或后台任务丢失后继续追平。 |
| projection state | checkpoint 的运行态扩展，至少记录 traceId/sessionId/sourceRunId/sourceCommandId、lastSourceSeq、lastProjectedSeq、sourceLatestSeq、projectionStatus、projectionHealth、resultSyncState、lastError/blocker、failureCount 和 nextRetryAt。 |
| result sync state | AgentRun `/result` 归档或终态补全的低优先级状态；它不得阻塞运行中 events projection。 |
| sourceLatestSeq | finalizer 在本次或最近一次拉取中观察到的 AgentRun command 最新 sourceSeq，用于诊断投影 lag；它不得替代 lastSourceSeq 成为已提交事实。 |
| projection diagnostic | 暴露 projection lag、blocker、lastProjectedSeq、sourceRunId、sourceCommandId、updatedAt 和恢复状态的可诊断字段。 |
| terminal outbox | AgentRun runner 在 command 结束时写出的 durable terminal envelope，包含 runId、commandId、terminalStatus、failureKind、final response 或 terminal error 等终态输入；它必须被 Workbench finalizer 幂等消费。 |
| already terminal cancel | 用户发起 cancel 前，HWLAB 发现 AgentRun command 或本地 sealed projection 已经 terminal 的状态；此时 cancel mutation 不得覆盖 terminal facts，应返回 already-terminal/no-op 或触发 terminal projection。 |
| control command boundary | prompt、steer、retry 和 cancel 的受控 mutation 边界。每个控制命令必须有独立 action、target turn/run/command、HTTP route、commandId 或 no-op reason；采集器、analyzer 和 Web 不能把后续 steer/cancel 误归入前一个 prompt 提交。 |
| AgentRun execution diagnostic | AgentRun durable ledger 对 runner job observation、stale lease、terminalReportState、reconciler backlog 和不可恢复 blocker 的诊断输出；Workbench 只能把它作为 projection diagnostic 输入，不拥有执行状态写权。 |
| transport diagnostic | SSE、turn polling、trace hydration、result sync、realtime timeout 或浏览器网络层观察到的可见性诊断；它只能描述传输或读侧健康，不拥有 message text、finalResponse 或 terminal status 写权。 |
| projection health | 对 projection diagnostic 的只读解释，状态至少区分 `projecting`、`caught-up`、`degraded` 和 `stalled`；它不推进 facts，也不得把 stale projecting 标为 caught-up。 |
| timing display input | Workbench ReadModel 输出的 `startedAt`、`lastEventAt`、`finishedAt` 和 sealed `durationMs` 等投影时间字段；它们是浏览器相对时间文案的唯一事实输入。浏览器本地 now 只能参与直接渲染计算，不成为 Workbench facts，也不得通过 UI 侧平滑、滤波、单调 floor、跳变 cap、二次缓存或本地推断伪造更连续的 `elapsed/recent` 事实。 |
| TraceEventPage | 由 WorkbenchReadModel 返回的 trace 分页事实，必须按 `projectedSeq` 单调排序，并保持 `range.fromSeq <= range.toSeq`；空页用显式 empty range 表达，不生成非法区间。 |
| compat wrapper | 旧 `/v1/agent/*` 或 conversation path 的兼容包装层；只能调用 `WorkbenchReadModel` 或正式 mutation，不拥有第二套事实写入或 read-through repair。 |
| read-through repair | GET 路径为了让页面“看起来正确”而同步调用 AgentRun、Code Agent manager、workspace repair、trace/result polling 或 billing finalizer 推进事实；本专项禁止该模式。 |
| 读侧推理 | REST GET、SSE consumer、compat wrapper、CLI renderer、Web reducer/selectors、Trace renderer、fake-server 或测试断言根据 trace tail、message text、tool event、result cache、session summary、list row、workspace snapshot、localStorage 或 elapsed timeout 推断 turn/session/message lifecycle、terminal、running 或 final response 的行为；本专项禁止该模式。 |
| 事后 repair | 页面或测试发现 active route/session/message/trace 已经分裂后，再通过 reload、切换 session、`sessionRepair`、`realignFreshSession`、workspace selection repair、active tab repair、localStorage truth、GET read-through 或 SSE gap repair 把 UI 补成看起来正确；本专项禁止该模式。 |
| Redis派生读缓存 | WorkbenchReadModel 内部可选的 Redis 缓存层，只保存从 durable Workbench facts 组装出的 session summary、terminal turn snapshot 或 terminal trace page 快照；它可丢弃、可过期、可重建，不是 Workbench facts、checkpoint 或 lifecycle authority。 |
| cache authority input | 构成缓存 key 与有效性判断的权威输入，至少包含 schema version、actor visibility input、session/turn/trace/cursor、projection revision/seq 或等价字段；这些输入必须来自 durable facts/read model。 |
| cache diagnostic | `cacheStatus`、`cacheAgeMs`、cache key class、projection revision/seq、dbQueryAvoided 和 Redis unavailable/stale 等读路径诊断；它只能解释性能和新鲜度，不拥有主 timeline 写权。 |
| CLI trace 视图 | `web-probe analyze` 从采样事实渲染的文字版 trace 视图，包含多 turn 的 `turn-summary` 和单 turn/单 frame 的 `trace-frame` 两层；它用于人工复核自动 finding，不保存新事实，不从 DOM 重新推断。 |
| canonical timeline digest | ReadModel、OTel 和 web-probe 用于比较同一 session/timeline 快照是否一致的脱敏摘要。摘要至少覆盖 sessionId、messageId、role、turnId、traceId、message/part order、status、sealed/timing 字段和文本哈希，不包含完整 prompt、完整 final response、Secret、cookie、Authorization 或 provider payload。 |
| Serve Session Aggregate Authority | Workbench 会话级聚合根，统一接收 prompt admission、steer、cancel、retry、run-state、message/part、trace/timing 和 terminal/final response 的写入序列；它是 Web、REST、SSE、CLI 和 web-probe 可见状态的上游 authority。 |
| durable input/command fact | 用户输入或控制命令进入执行前写入的持久事实，至少绑定 `sessionId`、`turnId`、`messageId`、`commandId`、action、target、admittedSeq、status 和 no-op/blocker；它先于 AgentRun dispatch 或 Kubernetes job 创建。 |
| session execution lane | 同一 Workbench session 的受控执行 lane，把连续 prompt、steer、cancel 和 retry 串到同一 AgentRun session/run command channel、warm runner lease 或等价机制；它不能只是 metadata。 |
| warm runner contract | AgentRun/HWLAB 对连续 turn 的低延迟执行合同：已创建的 runner 在同一 session/run lane 内继续 poll 后续 command，或由明确 runner lease/session channel 承接后续 command；每 turn 新建独立 run-scoped runner Job 不能标称为 runner reuse。 |

## 4. 系统边界和接口

本规格把 Workbench唯一投影作为客户端、API契约、HWLAB接入和Agent编排之间的状态边界看待；本章只描述输入、输出和责任边界。

| 边界项 | 内容 |
| --- | --- |
| 外部使用者 | Cloud Web、HWLAB CLI、Cloud API route、AgentRun adapter、fake-server 回归和运维诊断。 |
| 外部输入 | AgentRun run/command/event/result facts、turn admission metadata、owner/session visibility、显式 checkpoint replay/reprojection 请求、REST/SSE 查询参数。 |
| 受控资源 | Workbench facts、projection checkpoint、diagnostic、projectedSeq、session/message/part/turn/trace 查询模型和 SSE projection commit notification。 |
| 外部输出 | session rail、session detail、message/part page、turn snapshot、trace event page、sealed final response、projection/transport diagnostics、SSE events 和 compat wrapper envelope。 |
| 用户接口 | Cloud Web `/workbench`、同源 `/v1/workbench/*` REST/SSE、HWLAB CLI 的同源 Workbench/API 入口。 |
| 系统边界 | 本专项只定义 Workbench 用户态事实的唯一写入和读取链路；它不定义 AgentRun 执行合同、用户权限策略、Web UI 布局或平台发布机制。 |

## 5. 内部分工与规格索引

| 编号 | 内部模块 | 规格文档 | 主责边界 | 上游依赖 | 下游支撑 |
| --- | --- | --- | --- | --- | --- |
| PJ2026-010401080301 | FactsStore | 本规格 6.1 | durable facts schema、幂等 upsert、terminal commit、checkpoint 和分页读取 | 数据库/runtimeStore、用户管理 visibility | Writer、Finalizer、ReadModel |
| PJ2026-010401080302 | ProjectionWriter | 本规格 6.2 | normalized AgentRun facts 到 Workbench facts 的唯一写入 | HWLAB接入、Agent编排 | ReadModel、SSE、CLI/Web |
| PJ2026-010401080303 | ProjectionFinalizer | 本规格 6.3 | checkpoint 驱动追平、重启恢复和显式 checkpoint replay/reprojection 的同一幂等入口 | FactsStore、AgentRun facts source | Writer、diagnostics |
| PJ2026-010401080304 | ReadModel | 本规格 6.4 | session/message/turn/trace/projection diagnostics 的唯一读模型 | FactsStore、用户管理 visibility | REST GET、compat wrapper、CLI、fake-server |
| PJ2026-010401080305 | CompatWrapper | 本规格 6.5 | 旧 `/v1/agent/*` 和 conversation path 到 ReadModel 或正式 mutation 的兼容映射 | API契约、ReadModel | Web/CLI 迁移期兼容 |
| PJ2026-010401080306 | WebServerState | 本规格 6.6 | Web reducer/selectors 只消费 REST/SSE projection，分仓保存 sealed message projection、trace detail、session status 和 transport diagnostics | Web工作台、API契约 | session rail、timeline、composer、trace detail |
| PJ2026-010401080307 | Regression | 本规格 6.7 | 后端红灯、fake-server Playwright、D601 web-probe 和分叉清零验收 | 平台运维、Web工作台 | issue 收口和防回归 |
| PJ2026-010401080308 | CodeReference | 本规格 6.8 | 源码文件头部 SPEC 引用和实现版本追溯 | 规格治理 | 后续实现审计 |
| PJ2026-010401080309 | DerivedReadCache | 本规格 6.9 | ReadModel 内部 Redis 派生缓存的 authority input、payload、失效和降级边界 | Workbench性能、API契约 | 高频读降尾延迟 |
| PJ2026-010401080310 | AggregateEventStream | 本规格 6.10 | Workbench append-only aggregate event stream、cursor replay、projector revision 和顺序权威 | ProjectionWriter、ProjectionFinalizer、FactsStore | ReadModel、SSE、Web、CLI、fake-server、web-probe |
| PJ2026-010401080311 | CliTraceView | 本规格 6.11 | web-probe 采样事实的 turn-summary/trace-frame 渲染、final response 展示和 analyzer 判据 | ReadModel、AggregateEventStream | 工测复核、自动 finding、issue 证据 |
| PJ2026-010401080312 | ServeSessionAggregate | 本规格 6.12 | prompt admission、steer/cancel、run-state、message/part、trace/timing 的 session aggregate authority | API契约、Agent编排、HWLAB接入 | Web、CLI、web-probe、fake-server |

### 5.1 OpenCode serve 对照原则

本专项采用 OpenCode serve 的架构边界作为对照，但不复制其实现。`/root/opencode/packages/server/src/groups/session.ts` 将 prompt admission 与等待/上下文读取分离；`groups/message.ts` 和 `handlers/message.ts` 通过 message page、cursor 和稳定顺序提供投影读取；`groups/event.ts`、`event-v2-bridge.ts` 和 `bus/global.ts` 把 location-scoped event 与单调 event id 作为同步基础；`session/status.ts` 与 `session/run-state.ts` 集中表达 idle/busy/retry/cancel；`session/message-v2.ts` 与 `session/processor.ts` 以 message/part 追加、delta 和 cleanup 形成最终 assistant message；instance HTTP API 将 prompt、async prompt 和 abort 作为不同 route。

HWLAB 的实现必须吸收这些边界：admission 只创建 stable turn/message/part/control ids；status/cancel 不由 trace tail 推断；message page 和 trace page 只读 read model；event stream/outbox 负责 replay 和 ordering；assistant 最终内容来自 sealed assistant part，而不是从 trace 行、result envelope、DOM、`message.text` 或 analyzer fallback 重新挑选。凡是需要保留旧 compat route 的地方，都只能包装正式 mutation 或 WorkbenchReadModel，不能保留第二套 prompt/result/trace 仲裁。

### 5.2 目标架构图

```mermaid
flowchart LR
  subgraph AgentRun[AgentRun execution backend]
    RUN[run]
    CMD[command]
    SRC[events / result / sourceSeq]
    RUN --> CMD
    CMD --> SRC
  end
  subgraph CloudAPI[HWLAB Cloud API]
    AD[AgentRun Adapter: normalize facts]
    WR[WorkbenchProjectionWriter]
    FIN[WorkbenchProjectionFinalizer]
    FS[(WorkbenchFactsStore)]
    RM[WorkbenchReadModel]
    CACHE[(Redis derived read cache)]
    SSE[SSE publisher]
    COMP[Compat wrappers]
    AD --> WR
    FIN --> WR
    WR --> FS
    FS --> RM
    RM <--> CACHE
    FS --> SSE
    RM --> COMP
  end
  subgraph Consumers[Projection consumers]
    REST[GET /v1/workbench/*]
    WEB[Cloud Web reducer/selectors]
    CLI[HWLAB CLI renderer]
    FAKE[fake-server fixture]
    SSEOUT[/v1/workbench/events]
    RM --> REST
    REST --> WEB
    REST --> CLI
    REST --> FAKE
    SSE --> SSEOUT
    SSEOUT --> WEB
  end
  SRC --> AD
```

目标架构要求 route/auth、adapter、projection writer/finalizer、facts store、read model、SSE publisher 和 compat wrapper 分工清晰。任何 route、GET handler、trace polling、result polling、workspace snapshot 或 front-end reducer 都不能绕过 writer/finalizer 直接改变 Workbench facts。

目标架构还要求投影推进只发生在上游写侧。SSE publisher/handler 只发布和 replay durable outbox commit；Web/CLI/fake-server/SSE consumer 只消费 REST/SSE projection。SSE open/error、visibility change、route hydrate、Trace detail hydration、web-probe 观察、GET refresh 和 observer reload 不能触发 `hydrateRealtimeGap`、read-through sync、result sync 或 trace polling 来推动 projection；缺口只能表现为 projection diagnostic/blocker，并由 scheduler/reconciler/finalizer 或 source event/outbox 追平。

目标架构还要求彻底禁止读侧推理。`turn.status`、`message.status`、`session.running`、`trace terminal`、`finalResponse` 和 `projectionStatus` 必须是 projection writer/finalizer 已经写入 durable facts 的字段；read model、REST route、SSE consumer、compat wrapper、Web reducer、CLI renderer、fake-server 和测试只能读取和重放这些字段。AgentRun facts、trace events、message parts、result envelope、session summary、list row 和 workspace snapshot 只能作为 writer/finalizer 输入或诊断字段，不得在读取链路中通过优先级、fallback、最后事件、空文本、超时或 UI heuristic 生成生命周期事实。

### 5.3 目标数据流图

```mermaid
flowchart TD
  USER[用户 prompt/steer/retry/cancel] --> ADMISSION[Turn admission]
  ADMISSION --> IDS[stable turnId traceId userMessageId assistantMessageId]
  ADMISSION --> AR[AgentRun run/command]
  IDS --> WR0[ProjectionWriter: admission facts]
  AR --> EV[AgentRun events/result]
  EV --> NORM[normalize AgentRun facts]
  NORM --> FIN[ProjectionFinalizer]
  FIN --> WR[ProjectionWriter]
  WR0 --> TX[projection commit]
  WR --> TX
  TX --> FACTS[(Workbench facts + checkpoint + diagnostics)]
  FACTS --> RM[WorkbenchReadModel]
  RM --> CACHE{derived cache?}
  CACHE -->|hit| SNAP[REST snapshot / pages]
  CACHE -->|miss/stale| FACTS
  FACTS --> BUS[SSE projection commit]
  RM --> SNAP[REST snapshot / pages]
  BUS --> STREAM[SSE events]
  SNAP --> RED[Web reducer / CLI / fake-server]
  STREAM --> RED
```

数据流必须保证：admission 只生成稳定标识和初始 facts；AgentRun facts 必须先归一化，再由 writer/finalizer 进入 projection commit；REST snapshot 和 SSE event 只能重放 durable facts；diagnostic 明确表达 lag/blocker，不能让读取路径、页面路径或测试路径隐式修复事实。

### 5.4 terminal commit 关键时序图

```mermaid
sequenceDiagram
  participant AR as AgentRun
  participant Adapter as AgentRun Adapter
  participant Finalizer as ProjectionFinalizer
  participant Writer as ProjectionWriter
  participant Store as FactsStore
  participant SSE as SSE Publisher
  participant Read as ReadModel
  participant Web as Web/CLI/fake-server
  AR-->>Adapter: terminal result + events + sourceSeq
  Adapter-->>Finalizer: normalized facts
  Finalizer->>Writer: finalize sourceRunId/sourceCommandId
  Writer->>Store: atomic terminal commit
  Store-->>Writer: projectedSeq + checkpoint
  Writer-->>SSE: projection commit notification
  Web->>Read: GET session/messages/turn/trace
  Read->>Store: read only
  Read-->>Web: same terminal facts + diagnostics
```

terminal commit 必须在同一幂等提交内更新 assistant final text、message/part terminal status、turn terminal、trace terminal event、session running=false、summary updated、projection checkpoint 和 SSE cursor。若提交失败或暂时无法完成，只能暴露 projection lag/blocker；不得让 session rail 显示 completed、message page 显示 running、turn snapshot 显示 failed 或 trace page 缺 terminal 这类互相矛盾状态。

### 5.5 cloud-api 重启恢复时序图

```mermaid
sequenceDiagram
  participant Boot as cloud-api boot/background scheduler
  participant Store as FactsStore
  participant AR as AgentRun facts source
  participant Finalizer as ProjectionFinalizer
  participant Writer as ProjectionWriter
  participant Read as ReadModel
  Boot->>Store: load open checkpoints / running-projecting turns
  Store-->>Boot: sourceRunId/sourceCommandId/lastSourceSeq/lastProjectedSeq
  Boot->>Finalizer: resume checkpoint
  Finalizer->>AR: fetch events/result after source checkpoint
  AR-->>Finalizer: source facts or still-running status
  Finalizer->>Writer: idempotent catch-up commit
  Writer->>Store: update facts/checkpoint/diagnostic
  Read->>Store: read only snapshots
  Store-->>Read: caught-up facts or explicit lag/blocker
```

重启恢复要求 finalizer 不依赖进程内 90s 轮询作为唯一推进机制。进程内任务丢失、cloud-api 重启或慢任务超过短轮询预算后，boot/background scheduler 必须能从 durable checkpoint 和 running/projecting turn 找回需要追平的 sourceRun/sourceCommand，并以同一 writer 逻辑提交或记录 blocker。GET、SSE、Web 页面、visibility handler、fake-server、web-probe 和 CLI renderer 只能观察恢复状态，不能触发恢复、gap hydration 或 read-through sync。

### 5.6 durable Workbench facts 对象模型

Workbench durable facts 是唯一投影的持久对象模型。后续实现可以选择关系表、文档表或 runtime store 等价结构，但必须保留下列对象、稳定键和职责边界；旧 `agent_sessions.session_json`、result snapshot、trace tail、compact snapshot 和 workspace/conversation metadata 只能作为迁移输入或诊断来源，不得成为读取路径 lifecycle authority。

| 对象 | 稳定键 | 关键字段 | 生命周期职责 |
| --- | --- | --- | --- |
| `workbench_session` | `sessionId` | `owner`, `projectId`, `workspaceId`, `status`, `running`, `currentTurnId`, `summary`, `updatedAt` | session rail、detail 和 composer 可继续操作的用户态会话事实。 |
| `workbench_message` | `messageId` | `sessionId`, `role`, `turnId`, `status`, `sealedAt`, `createdAt`, `updatedAt` | timeline message 容器；assistant terminal message sealed 后不被 diagnostic 覆盖。 |
| `workbench_part` | `partId` | `messageId`, `type`, `status`, `text`, `tool`, `error`, `finalResponse`, `traceRef`, `order`, `sealedAt` | 主正文、工具、错误、final response 和 trace 摘要的组合事实。 |
| `workbench_turn` | `turnId` | `sessionId`, `traceId`, `status`, `terminal`, `failureKind`, `sourceRunId`, `sourceCommandId`, `startedAt`, `lastEventAt`, `finishedAt`, `durationMs`, `sealedAt` | 一次 prompt、steer、retry 或 cancel 的生命周期事实；时间字段只由投影写入。 |
| `workbench_aggregate_event` | `eventSeq` 或 `eventId` | `aggregateId`, `aggregateSeq`, `sessionId`, `turnId`, `traceId`, `messageId`, `sourceRunId`, `sourceCommandId`, `sourceSeq`, `sourceEventId`, `eventType`, `occurredAt`, `committedAt`, `payloadRef`, `redactedPayload`, `projectionRevision` | append-only event stream；负责排序、幂等、SSE replay 和 projector recovery，不直接作为 Web 主 DTO 绕过 read model。 |
| `workbench_trace_event` | `traceId` + `projectedSeq` | `sourceSeq`, `sourceEventId`, `type`, `status`, `timestamp`, `redactedPayload` | Trace event page 的唯一分页事实；`projectedSeq` 是 Workbench cursor。 |
| `workbench_projection_checkpoint` | `sourceRunId` + `sourceCommandId` + `turnId` | `lastSourceSeq`, `lastProjectedSeq`, `sourceLatestSeq`, `status`, `attempt`, `nextRetryAt`, `lastError`, `blocker` | finalizer/resume 追平位置、幂等边界和阻塞恢复状态。 |
| `workbench_projection_diagnostic` | `sessionId` + `turnId` + `traceId` | `projectionStatus`, `health`, `lag`, `blocker`, `sourceRunId`, `sourceCommandId`, `lastProjectedSeq`, `updatedAt` | GET、CLI、Web 和运维可见的投影健康诊断；不推进主事实。 |

对象模型必须支持下列 DTO 投影：session rail summary、session detail、message/part page、turn snapshot、trace event page、projection diagnostic 和 transport diagnostic。DTO 可以裁剪字段，但不得合并 authority；例如 trace detail 可展示 `sourceSeq/sourceEventId` 审计信息，主 timeline 的 finalResponse 只能来自 sealed message/part/turn facts。`workbench_aggregate_event` 是 projector 和 replay 的提交输入，不允许 Web/API/CLI 直接把事件 payload 拼成另一套 timeline。

Assistant 输出必须按 message/part authority 建模。一个 turn 内可以有多个 assistant text/tool/diagnostic/final response part，每个 part 有稳定 order 和 status；final response 是 terminal commit sealed 的 final part，不是“最后一个非空文本”。同一 trace 只生成 `msg_<trace>_agent` 这类单一 assistant 容器而缺少 part 身份、order 和 sealed 状态的实现，不能作为本规格的完成形态。

运行中和终态 timing DTO 必须保持可审计：`elapsed`、`recent update`、轮次完成耗时和 trace 首尾耗时的用户可见差异只能来自 durable projection 或上游事件时间戳，不能由 Web reducer、组件、ticker、fake-server、web-probe 或 CLI renderer 在读侧修正。前端只允许把 `now - startedAt`、`now - lastEventAt` 或 sealed `durationMs` 格式化成中文文案；不得保存 per-message timing floor、recent age cache、sealed duration cache、monotonic repair state 或后台 tab resume 补偿状态。若这些值在采样中出现归零、非单调、跳秒或和 terminal commit 不一致，系统必须暴露 projection/timing diagnostic 并修 writer/finalizer/read model、SSE/outbox 或 AgentRun source event，而不是在 UI 层滤波。


### 5.7 durable projection outbox 与 reconciler

Projection outbox 是 durable commit notification log，与 projection facts 和 aggregate event stream commit 在同一数据库事务提交。outbox 的作用是让 SSE、CLI 和其他消费者从 durable cursor 恢复实时通知，不依赖进程内内存 listener。aggregate event stream 表达“发生了什么”和顺序权威；outbox 表达“某次投影 commit 可以通知消费者”，二者不得分叉成两套 lifecycle truth。

```mermaid
flowchart LR
  AR[AgentRun events/result] --> AD[Adapter normalize]
  AD --> WR[ProjectionWriter]
  WR --> TX[(DB transaction: aggregate events + facts + outbox + checkpoint)]
  TX --> AEV[aggregate event stream]
  TX --> FACTS[durable facts]
  TX --> OUTBOX[durable outbox rows]
  TX --> CP[checkpoint]
  OUTBOX --> SSE[SSE cursor replay]
  OUTBOX --> REC[reconciler worker]
  FACTS --> RM[ReadModel]
  RM --> REST[GET /v1/workbench/*]
  SSE --> STREAM[SSE events]
  CP --> BOOT[boot/resume scheduler]
  BOOT --> FIN[Finalizer]
  FIN --> WR
```

Outbox 每行至少包含 `outboxSeq`（全局单调）、`eventSeq`、`aggregateId`、`aggregateSeq`、`traceId`、`sessionId`、`turnId`、`projectedSeq`、`projectionRevision`、`commitType`（event/terminal/checkpoint/diagnostic）、`createdAt` 和可序列化 payload 摘要。SSE 连接建立时先从 durable outbox 按 `afterSeq` 查询历史 commit notification 进行 replay，再切换到 live tail；cloud-api 重启后不需要内存 listener 即可恢复 SSE 输出。

Reconciler 是后台 worker，消费 outbox 和 checkpoint 驱动 finalizer 追平。它取代旧的请求/轮询式 `syncAgentRunChatResult` 推进模式：AgentRun result sync 只负责拉取/写入 projection source state，事件推进由 outbox/reconciler 管道统一处理。

### 5.8 SSE cursor replay 架构

```mermaid
sequenceDiagram
  participant Client as Web/CLI
  participant SSE as SSE handler
  participant DB as durable outbox
  participant WR as ProjectionWriter
  Client->>SSE: GET /v1/workbench/events?afterSeq=N
  SSE->>DB: SELECT outbox WHERE outboxSeq > N
  DB-->>SSE: replay rows (terminal, events, diagnostics)
  SSE-->>Client: replay events
  Note over WR,DB: new projection commit
  WR->>DB: INSERT outbox row
  DB-->>SSE: live tail notification
  SSE-->>Client: live event
```

SSE handler 不再直接订阅内存 `traceStore.subscribe`。它只从 durable outbox 读取：连接建立时执行 replay（按 `afterSeq` 查询历史 commit notification），replay 完成后切换到 live tail（poll 或 LISTEN/NOTIFY 等价机制）。cloud-api 滚动、多副本和多用户同看同 trace 时，SSE 从同一 durable outbox 恢复，不丢 terminal/sealed 状态。

### 5.9 DB 唯一约束与幂等保障

`workbench_trace_events` 表必须有 `(trace_id, source_event_id)` 唯一约束，防止并发或历史数据写入重复 source event。allocator 查询 sourceEventId 复用 projectedSeq 时，数据库层必须硬约束阻断重复写入。

`(trace_id, projected_seq)` 也应有唯一约束，确保同一 trace 内 projectedSeq 单调不重复。历史 collision 数据在 migration 时通过 collision blocker 策略处理：检测到重复时暴露 `projectionStatus=blocked`、`projectionHealth=degraded` 和稳定 blocker code，不把损坏序列交给 renderer 继续展示。

Projection outbox 的 `outboxSeq` 必须全局单调递增，可通过数据库 sequence 或等价机制生成。outbox 行与 projection facts 在同一事务提交，保证 commit notification 与 durable facts 一致。

Aggregate event stream 必须同时具备 `(eventSeq)` 全局单调约束、`(aggregateId, aggregateSeq)` aggregate 内单调唯一约束和来源幂等唯一约束。来源幂等键至少覆盖 `sourceRunId`、`sourceCommandId`、`sourceEventId` 或等价 source result id；admission、cancel 和 replay/reprojection 这类本地事件必须有稳定 deterministic eventId。任何需要重排、补 terminal 或修 timing 的逻辑都必须追加受控事件并重新投影，不得直接改 read model 或前端 store。

### 5.10 0读侧推理验收矩阵

| 层 | 禁止模式 | 验收方式 |
| --- | --- | --- |
| 后端 turn projection | `result?.status ?? trace?.status ?? session?.status` | 源码扫描 + 单元测试构造多来源矛盾样本 |
| 后端 SSE handler | `traceStore.subscribe` / 内存 trace snapshot | 源码扫描 + cloud-api 重启后 SSE cursor 恢复验证 |
| 后端 trace snapshot | `events.at(-1)` 推导 status/lastEvent | 源码扫描 + 矛盾 events 样本测试 |
| 后端 compact result | result/traceSummary/agentRun/memoryTrace 混合 | 源码扫描 + GET 路径不访问旧 compact path 验证 |
| 后端 message projection | trace-level assistant id 或 firstNonEmpty/fallback 选择 final response | 源码扫描 + 多 assistant part / running final 空值样本 |
| 前端 store | `events.at(-1)` 推导 activity/label/status | 源码扫描 + fake-server Playwright |
| 前端 subscription | turn poll + trace poll + event tail 合并 lifecycle | 源码扫描 + SSE gap/timeout Playwright |
| 前端 timing | `lastEventAgeMs` 作为相对时间权威 | 源码扫描 + fake clock Playwright |
| CLI renderer | `lastEvent.status` 推 trace lifecycle 或 DOM 行顺序选择 final response | 源码扫描 + CLI 同源语义验证 |

每层验收必须先用负向 fixture 形成红灯，再用修复后代码证明通过。负向源码扫描是辅助证据，不能替代 fake-server Playwright 与 D601 v0.3 web-probe。

## 6. 原子需求

### 6.1 WB-PROJ-REQ-001 durable facts store

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-001 | FactsStore | PJ2026-010401080301 FactsStore | [用户管理](PJ2026-0105-user-management.md)、[API契约](PJ2026-010403-api-contract.md) |

Workbench facts store 应持久化 session、message、part、turn、trace event、projection checkpoint 和 projection diagnostic，使 Workbench 用户态事实可在 cloud-api 重启、进程内任务丢失或后台 finalizer 重跑后恢复。

Facts schema 至少包含以下事实：

| fact | 必需字段 | 职责边界 |
| --- | --- | --- |
| session | sessionId/conversationId/threadId/projectId/workspaceId/owner/status/running/currentTurnId/summary/updatedAt | 会话可见性、rail 摘要和当前 turn 指针；不保存 provider Secret 或完整 prompt 原文之外的敏感负载。 |
| message | messageId/sessionId/role/status/turnId/sealedAt/createdAt/updatedAt | timeline message 事实；assistant final text 通过 part 表达，sealedAt 或等价字段表示主消息结果已封存。 |
| part | partId/messageId/type/status/text/tool/error/finalResponse/sealedAt/traceRef/order | 文本、工具、错误、final response 和 trace 摘要的可组合展示事实；final response part sealed 后不得被读侧 diagnostic 覆盖。 |
| turn | turnId/sessionId/traceId/status/terminal/sealedAt/failureKind/sourceRunId/sourceCommandId/messageIds/startedAt/lastEventAt/finishedAt/durationMs | 一次用户提交、steer、retry 或 cancel 的生命周期事实；运行中相对时间显示只消费这些投影时间戳。 |
| trace event | traceId/projectedSeq/sourceSeq/sourceEventId/type/status/timestamp/redactedPayload | Workbench trace page 的分页事实；projectedSeq 是唯一 cursor。 |
| checkpoint | turnId/traceId/sourceRunId/sourceCommandId/lastSourceSeq/lastProjectedSeq/sourceLatestSeq/status/attempt/lastAttemptAt/lastError/blocker/updatedAt | finalizer 追平位置、幂等边界、恢复状态和退避诊断。 |
| diagnostic | sessionId/turnId/traceId/projectionStatus/health/lag/blocker/sourceRunId/sourceCommandId/lastSourceSeq/lastProjectedSeq/sourceLatestSeq/updatedAt | GET 和运维可见的 projection 状态，不推进事实。 |

Checkpoint/projection state 唯一键必须包含 `sourceRunId`、`sourceCommandId` 和对应 `turnId`/`traceId`，或等价的 command-scoped 唯一键。同一 AgentRun run 的不同 command 不得共享 cursor；旧 command 的历史事件不得误投到新 turn。`lastSourceSeq` 表示已经成功投影的来源位置，`sourceLatestSeq` 只表示最近观察到的来源最新位置；二者差值可用于 lag/health 诊断，但不得让读侧推断 terminal。

AgentRun events 进入 Workbench trace facts 时，FactsStore 必须把 filtered trace event 写入和 projection cursor 推进放在同一 durable commit 中，或采用等价的幂等事务语义。cursor 只能在对应 source event 已成功持久化或被确认幂等去重后推进；不得先更新 `lastSourceSeq` 再异步 best-effort 写 trace event。cloud-api 重启后必须直接从 projection state 的 `lastSourceSeq` 继续请求上游 events，不能通过扫描 `agent_trace_events`、trace tail、session JSON、result payload 或长 trace 全量重放来反推出下一次 `afterSeq`。

Facts store 必须支持幂等 upsert、terminal commit、projection checkpoint、diagnostic update、open checkpoint 扫描、running/projecting turn 扫描和按 owner visibility 过滤的分页查询。`agent_sessions.session_json` 可作为迁移期 metadata 来源，但不得继续承载业务 projection 真相。

Facts store 必须把 assistant 文本、tool/command 行、diagnostic 和 final response 持久化为 message/part facts。对同一 turn/trace 的 assistant 输出，必须能按 part order 恢复 running 中间态、steer 后续输出和 terminal final part；不得只持久化 trace-level assistant message id，再让读取端从 `text/content/message/finalResponse` 的 first-non-empty/fallback 链条重建主消息。

### 6.2 WB-PROJ-REQ-002 projection writer

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-002 | Writer | PJ2026-010401080302 ProjectionWriter | [HWLAB接入](PJ2026-010205-hwlab-dispatch.md)、[Agent编排](PJ2026-0102-agent-orchestration.md) |

WorkbenchProjectionWriter 应作为唯一写入口，把 admission facts 和 normalized AgentRun facts 转换为 durable Workbench facts。Writer 不负责 HTTP route、鉴权、AgentRun fetch、UI transient state、旧 API envelope 或测试 fixture。

Writer 必须使用稳定幂等键写入 messageId、partId、turnId、traceId、sourceRunId、sourceCommandId、sourceSeq/sourceEventId 和 projectedSeq。重复 event、重放 result、重启恢复或 checkpoint replay/reprojection 不得创建重复 assistant message、重复 terminal event 或冲突 session summary。

Writer 必须在 admission 阶段为 user message、assistant message 和初始 part 分配稳定身份，并在后续 event/result/steer/cancel projection 中追加或封存对应 part。assistant 文本增量、工具输出、backend diagnostic、steer 确认、cancel 结果和 final response 不得写入同一个无 order 的文本字段；terminal final response 必须是明确 partKind/status/sealedAt 的 sealed part。

Writer 的 terminal commit 必须同时写入 assistant final text、finalResponse part、message/turn terminal status、trace terminal fact、session running=false、checkpoint 和 `sealedAt` 或等价 sealed 标记。Completed、failed、canceled、blocked 都是 terminal result；若 AgentRun 或 projection 给出可读 final error/blocker，它必须作为 sealed final response 的一种结果写入，而不是作为后续 transport diagnostic 写入主正文。

### 6.3 WB-PROJ-REQ-003 projection finalizer

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-003 | Finalizer | PJ2026-010401080303 ProjectionFinalizer | [HWLAB接入](PJ2026-010205-hwlab-dispatch.md)、[AgentRun核心](PJ2026-010201-agentrun-core.md) |

WorkbenchProjectionFinalizer 应以 checkpoint 为驱动追平 AgentRun facts。submit 后、event arrival 后、后台恢复和显式 checkpoint replay/reprojection 可以触发 finalizer，但这些入口必须调用同一幂等 finalization 逻辑。显式 replay/reprojection 是受控投影恢复入口，不是前端、GET、SSE 或 probe 的事后 repair；它不得改变 active session、route、当前消息区或用户显式选择。

Finalizer 不得伪造 terminal、不取消 AgentRun、不用日志尾部推断 completed，也不得让 GET route 临时同步执行 finalization。若 AgentRun 事实暂不可取或映射失败，finalizer 应写入 diagnostic blocker，保留 retry 所需 sourceRun/sourceCommand/checkpoint。

AgentRun rolling recovery 的缺口必须显式落入 projection diagnostic。若 AgentRun durable ledger 缺 terminal fact、runnerJob observation 尚未恢复、lease stale 但 Kubernetes phase 未确认、terminalReportState 仍 pending/outbox-retrying，或 AgentRun result/diagnosis API 返回 unrecoverable blocker，Finalizer 应把这些上游事实归一化为 Workbench projectionStatus、projectionHealth、blocker、sourceRunId、sourceCommandId 和 nextRetryAt 等诊断字段。Finalizer 不得根据 Kubernetes Job completed、stdout tail、最后一个 AgentRun event、elapsed timeout、session summary 或 Web 当前页面状态补造 Workbench completed。

Startup/resume authority 只属于 cloud-api boot/background scheduler。该 scheduler 启动后必须扫描 durable open checkpoint、`projectionStatus=projecting` 或等价 running/projecting turn，按 bounded concurrency、attempt、lastAttemptAt 和 backoff 推进；它只能调用同一 `WorkbenchProjectionFinalizer/Writer`，不得新增第二套 result polling、manager shortcut 或 GET repair。恢复任务失败时必须写入 `lastError/blocker` 和下一次 retry 所需 cursor，不能静默丢弃。

Finalizer 的 cursor 必须以 command 和 checkpoint 为边界追平。对同一 run 的第二个及后续 command，finalizer 必须从该 command 自己的 checkpoint 或 command-scoped event/result window 继续，避免每次从 run head 扫描大量旧事件；当只能获得 run-level event page 时，也必须用 `sourceCommandId`、稳定幂等键和 source checkpoint 过滤旧 command 事件。result 先到、events 后到、terminal event 缺失但 command result terminal、source event 缺口或 sourceSeq 回退都必须进入 diagnostic/blocker 或补洞路径，不得在读侧修正。

运行中投影必须以 AgentRun events 增量接口为主读源：finalizer/resume loop 使用 durable `lastSourceSeq` 调用上游 `events?afterSeq=<lastSourceSeq>&limit=<boundedPage>`，再按 commandId 和 seq window 过滤并提交 Workbench facts。running loop 不得为了恢复或刷新进度同步等待 `/result`，也不得在每次拉取时从 run seq 0、长 trace、result envelope 或历史 events 数组重新扫描。发现 terminal evidence 后，finalizer 可以把 projection 标记为 terminal/pending-result，并把 `/result` 同步交给后台低优先级归档；`/result` timeout 只能更新 `resultSyncState`、`lastError/blocker` 和 retry/backoff，不得阻止 events cursor 继续推进。

Projection resume 对 `projectionStatus=projecting/degraded/stalled` 或等价 running turn 超过受控阈值的 trace，必须周期性读取 AgentRun command result authority，不能只等待 events page 中出现 terminal event。Command result 已 terminal 时，finalizer 必须写入同一 terminal commit；若 result 拉取失败或超时，`resultSyncState=timed_out/failed/pending` 必须进入 projection state 并按 backoff 重试。长期 `terminal=0 failed=0` 的 resume pass 不能被记录成无问题完成；它必须暴露候选数、result sync 状态和下一次 retry 信息。

Cancel 是受控 mutation，不是终态仲裁。`/v1/agent/chat/cancel` 或 Workbench cancel mutation 在转发 AgentRun cancel 前，必须先检查本地 sealed terminal projection，并在可能时读取当前 run/command result 或 command state。若上游 command 已 `completed`、`failed`、`blocked`、`canceled` 或等价 terminal，cancel 不得把 Workbench turn 改写为 canceled；应返回 already-terminal/no-op 或触发 terminal projection，使 Workbench 保持真实 terminal final response。

Finalizer 对 sealed turn 的后续重试只能推进 trace detail completeness、result archive、diagnostic health 或受控 replay/reprojection；不得把 `/result` timeout、events gap、provider transport error、poll idle timeout 或 hydration failure 映射成新的 assistant 主正文、finalResponse、message status 或 turn terminal status。

### 6.4 WB-PROJ-REQ-004 read model and pure GET

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-004 | ReadModel | PJ2026-010401080304 ReadModel | [API契约](PJ2026-010403-api-contract.md)、[Web工作台](PJ2026-010401-web-workbench.md) |

WorkbenchReadModel 应作为唯一读模型，从 durable facts 组装 session list、session detail、message page、turn snapshot、trace event page 和 projection diagnostics。Read model 只读 facts store，不调用 AgentRun、Code Agent manager、legacy conversation manager、billing finalizer、workspace repair 或 trace/result polling。

Read model 可以在组装完成后读取或写入 Redis 派生读缓存，但缓存 payload 必须等价于同一 durable facts/read model DTO 的可重建快照。缓存 key 必须包含 cache authority input；命中缓存不得绕过 owner visibility、不得补造缺失 facts、不得把 stale payload 升级成 terminal truth。Redis unavailable、timeout、decode failed 或 revision mismatch 必须退化为 cache diagnostic，并继续走 durable facts read 或明确 degraded。

`GET /v1/workbench/*` 必须纯读。若 projection 滞后，响应应输出 `projectionStatus`、`lastProjectedSeq`、`sourceRunId`、`sourceCommandId`、`blocker` 或等价 diagnostic 字段，不能在 GET 内做 read-through repair。

MessagePage 是 Workbench timeline 事实，不是 facts 表写入批次的直接 dump。WorkbenchReadModel 必须按 turn timeline、aggregate seq 或等价事件时间组装用户可见顺序；同一 turn 的 user message 必须位于对应 assistant/agent terminal message 之前，跨 turn 不得因为 user message 的 `projectedSeq/sourceSeq` 先整体写入、assistant terminal 后整体写入而输出 `UUAA`、`UUUAAA` 等 role cluster。`projectedSeq/sourceSeq/sourceEventId` 必须保留用于审计和幂等，但不得让 role/source bucket 或投影批次覆盖用户可见 timeline。

Session summary 的 title/preview 必须由 durable message/part projection 生成脱敏摘要，并随 session list/detail 作为显式字段返回。fallback `Session ses_*` 只表示 read model 缺摘要，是需要通过 OTel/session_list_read、web-probe finding 或 reprojection 暴露的问题，不是正常 UI 文案来源。

MessagePage、session detail 和 session list 必须输出足够的脱敏诊断来证明同一 session 的 canonical timeline 一致性。推荐字段为 `timelineDigest`、`roleSequencePrefix`、`adjacentSameRoleCount`、`traceIds`、`messageCount`、`projectionRevision` 或等价组合；这些字段可以作为 API DTO、OTel attribute 或 web-probe summary 出现，但必须同源于 ReadModel。刷新、session 切换、双观察者、deep link 与 SSE 重连后，同一 projection revision 的 digest 不得变化；若变化，系统必须把差异暴露为 projection/read-model/transport diagnostic，而不是在 Web reducer 或 analyzer 中重排掩盖。

Read model 严禁读侧推理。它不得从 `result?.status ?? trace?.status`、最后一条 trace event `status=completed`、message text 是否为空、part/tool row 状态、session list summary、workspace selected state、localStorage mirror、轮询耗时或 elapsed timeout 推断 turn terminal、message terminal、trace terminal、session running、final response 或 projection caught-up。若 durable facts 中唯一投影对象缺少这些字段，Read model 必须返回未知、投影中、degraded 或 blocker 等显式 diagnostic，由 projection writer/finalizer 修复源头；不得在读取时临时合成“看起来正确”的状态。

Read model 读取到 sealed final response 时，必须把主消息正文、finalResponse、message status 和 turn terminal 从 sealed projection 输出；projection diagnostic、trace hydration diagnostic、transport diagnostic、result sync diagnostic 和 session health 只能作为独立字段或详情区数据输出。读取链路不得因为后续 turn snapshot 失败、trace event page 失败、SSE gap、realtime timeout 或 compat wrapper 失败而替换 sealed 主正文。

Read model 输出 running turn 时，finalResponse 必须为空值或等价 `(空内容)` 展示输入；诊断、后台状态解释、steer accepted 文案或用户 prompt 不能填入 finalResponse。Read model 输出 terminal turn 时，finalResponse 只能来自 sealed final part；若 sealed final part 缺失，应输出 projection blocker，而不是回退到 user message、assistant 中间文本、trace 最后一行或 result summary。

Projection health 必须只解释 durable diagnostic。`projectionStatus=projecting` 且 `lastProjectedSeq < sourceLatestSeq`、checkpoint stale 超过受控阈值、存在 `lastError/blocker` 或 attempt 正在退避时，不得返回 `caught-up`；应返回 `projecting`、`degraded` 或 `stalled`，并暴露 `blocker`、`lastProjectedSeq`、`sourceRunId`、`sourceCommandId` 和更新时间。`caught-up` 只能表示对应 command checkpoint 已追平可见 source facts，或 source 明确仍 running 且无新可投影 facts。

Trace event page 必须按 `projectedSeq` 单调返回，响应范围必须合法。非空页满足 `range.fromSeq <= range.toSeq`；空页必须用明确空集合和空 range/null range 表达，禁止出现 `fromSeq=5/toSeq=4` 这类非法区间。`projectedSeq` 是 Workbench 读取 cursor，`sourceSeq` 只作为来源审计字段；二者不得混用。

Trace event page 的可见性必须以 durable turn/message/session 关系和 owner visibility 判断，不能只依赖 session 当前 `lastTraceId`。同一 session 后续产生新 trace 后，旧 trace 只要对应 turn、message 或 projection facts 对当前 actor 可见，`GET /v1/workbench/traces/{traceId}/events` 仍应返回 200 和 trace page；若 trace facts 缺页或 projection 未追平，应返回 projection blocker/degraded，而不是 actor 不可见 404。真正 archived/deleted/not-found 的生命周期必须来自 canonical lifecycle projection。

Trace event page continuation 的空窗口不得升级成 404。若 page 没有新 event、但 trace checkpoint 或 projection diagnostic 显示 read model 尚未追到 `traceLastSeq/lastProjectedSeq`，响应应为 `200` 空页并标记 `projectionStatus/projectionHealth=projecting` 或等价 degraded/stalled，暴露 `latestProjectedSeq` 或 `lastProjectedSeq`、`traceLastSeq`、`fullTraceLoaded=false` 和 `diagnostic.code=workbench_trace_events_missing`。`fullTraceLoaded=true` 只能表示本次 page 实际 `toProjectedSeq` 已追到 `traceLastSeq` 且没有更多 page，不能仅由 `hasMore=false` 或 `range.total` 推断。

Trace event page 的 OTel/read-model 诊断必须能解释 web-probe 的 trace-row-order 与 completion/timing finding。每次读取至少应能关联 traceId、sessionId、range.fromProjectedSeq、range.toProjectedSeq、returnedEvents、totalEvents、hasMore、fullTraceLoaded、traceLastSeq、projectionStatus、projectionHealth、sourceRunId 和 sourceCommandId；这些字段应来自同一 ReadModel 响应或同源 OTel span，不能由 analyzer 在事后猜测。

Workbench facts/read model 的 PostgreSQL pool connect/query timeout 必须被转换为结构化 degraded/readiness/blocker 响应和日志，不能逃逸为 cloud-api 进程未捕获异常。Liveness 不应因单次 Workbench read query timeout 变为 EOF；readiness 或 route 响应应暴露 route、store method、timeout kind、runtime readiness、projection candidate count、相关 trace/run/command 摘要和 `valuesRedacted=true`。

### 6.5 WB-PROJ-REQ-005 compat wrapper

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-005 | CompatWrapper | PJ2026-010401080305 CompatWrapper | [API契约](PJ2026-010403-api-contract.md)、[HWLAB接入](PJ2026-010205-hwlab-dispatch.md) |

迁移期 `/v1/agent/turns/*`、`/v1/agent/traces/*`、conversation detail、result polling 和 legacy conversation path 可以作为 compat wrapper 暂存，但只能调用 WorkbenchReadModel 或正式 mutation。Compat wrapper 不得拥有第二套 session/message/turn/trace 写入逻辑，不得在读取时同步 finalizer，也不得用 result cache 或 trace tail 组装另一份 final response。

旧路径返回的 envelope 可以保留 legacy 字段名，但字段值必须来自同一 Workbench facts。若无法映射，应返回结构化 incompatibility/blocker，而不是回退到旧 manager 或空 stub。

### 6.6 WB-PROJ-REQ-006 Web server-state single path

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-006 | Web单路径 | PJ2026-010401080306 WebServerState | [Web工作台](PJ2026-010401-web-workbench.md)、[API契约](PJ2026-010403-api-contract.md) |

Cloud Web 应把 REST snapshot、SSE projection commit、trace event page 和 submit optimistic ids 归一化到同一 server-state/reducer/selectors。UI 组件只消费 selectors；组件、trace polling、result polling、localStorage、workspace snapshot 或 session list summary 不得直接写 final response、message terminal、turn terminal 或 trace authority。

Web server-state 必须分仓表达 `messageProjection`、`traceDetail`、`sessionStatus` 和 `transportDiagnostics`，或采用等价结构保证主消息投影与读侧诊断不会竞争同一字段。`messageProjection` 承载 role、status、text、finalResponse、turnId、traceId 和 sealedAt；`traceDetail` 承载 events、pagination、hydration 状态和 trace diagnostics；`sessionStatus` 承载 rail/card 运行摘要；`transportDiagnostics` 承载 SSE、poll、hydration timeout 或 realtime gap。Selectors 可以在消息详情入口汇总诊断，但主 timeline 的 sealed final response 不得被 diagnostic 文案替换。

active session 只能由 route sessionId 或用户显式选择产生。REST detail、session list、workspace summary、SSE event、trace page 或 turn snapshot 晚到时，只能更新其声明 sessionId 对应的 server-state bucket；不得重新选择 session、替换 URL、覆盖当前消息区、改变 composer 或把另一个 session 的 running/terminal 状态投射到当前页面。缺少 sessionId 或 session/trace 不匹配的 event 必须进入 authority mismatch diagnostic，不得猜测、fallback 或应用到当前 active session。

active turn/trace 只能来自 read model 声明的 current turn、用户显式选择或提交 mutation 返回的 stable ids。Web store 不得通过倒序扫描 messages、查找 running trace、`messageHasCompletedFinalResponse()`、最新 traceId 或 composer mode 推断 active trace；这类推断只能作为迁移期负向扫描对象。prompt、steer 和 cancel 的按钮 action 必须与当前 active command boundary 一致；没有 active turn 时，steer/cancel 应返回 no-op/blocker，不得 fallback 成普通 prompt。

SSE 只作为 projection commit 后的通知和加速，不是事实来源。SSE 断线、丢事件或重连后，Web 只能通过同一 REST snapshot 与 trace page 补洞。running trace 在 terminal 前 `hasMore=false` 只表示当前页追平，不得设置永久 `fullTraceLoaded=true`。

Web reducer 必须提供 sealed guard：对已经有 `sealedAt` 或等价 sealed 标记的 assistant message/turn，turn polling、trace hydration、SSE gap、late realtime diagnostic、compat wrapper error 或 elapsed timeout 只能更新所属 trace/session 的 diagnostic bucket，不得写回 message text、finalResponse、message status 或 turn terminal。需要主动修改 sealed 主正文时，必须来自同一 durable projection 的新 revision、受控 replay/reprojection 或明确用户 mutation，而不是读侧失败。

运行中相对时间显示必须保持单一权威：Web server-state 只保存 WorkbenchReadModel 返回的 `startedAt`、`lastEventAt`、`finishedAt`、`durationMs` 和 sealed 标记；浏览器 ticker 只在组件渲染时计算 `now - startedAt` 与 `now - lastEventAt` 文案。`Date.now()`、页面本地 `updatedAt`、`lastEventAgeMs` 快照、轮询间隔和 trace hydration 时间不得写入 reducer/store，也不得影响 lifecycle、terminal、session ordering 或 projection diagnostic。终态消息的耗时只使用 sealed projection 值，不能随浏览器 ticker 继续增长。

刷新和 session switch 恢复必须保持 active session bucket 不变量。Web 在重新 hydrate、切出再切回、双页面同时观察、SSE 断开后 REST 补洞或 optimistic submit reconcile 时，只能用 ReadModel 声明的 sessionId/turnId/messageId/traceId 更新对应 bucket；pending optimistic message 必须按 stable ids 与服务器 message 对账，不能按数组位置、最近 traceId、session rail 行或 DOM 到达顺序合并。web-probe 在第 1、5、10 轮刷新或切换后看到的主 timeline digest、role sequence、session rail title/preview 和 traceId 归属必须与同一 ReadModel 快照一致。

### 6.7 WB-PROJ-REQ-007 regression and validation

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-007 | 回归验收 | PJ2026-010401080307 Regression | [Web工作台](PJ2026-010401-web-workbench.md)、[平台运维](PJ2026-0106-platform-ops.md) |

Workbench唯一投影必须有后端和浏览器两层回归：后端用 Workbench-only HTTP 用例证明 submit 后不访问旧 `/v1/agent/turns` 或 result polling 也能自然 completed；durable recovery 用例证明进程内 sync 丢失或 cloud-api 重启后 finalizer 能从 checkpoint 追平；terminal atomicity 用例证明 session rail、session detail、messages、turn snapshot 和 trace event page 不出现 running/completed 组合矛盾。

Durable recovery 红灯必须覆盖长 run、多 command 和大量旧事件场景。fixture 应优先从 D601 v0.3 真实样本脱敏派生，至少覆盖同一 run 中后续 command 已 completed、eventCount 大于 2000、Workbench 只投影早期 seq、GET 保持纯读、随后后台 resume 从 command-scoped checkpoint 追平并展示 final response 的路径。用例还必须断言 GET 没有调用 AgentRun 或 result polling。

后端回归还必须包含不依赖 Web、CLI、数据库外部服务或其他模块的最小单元测试，直接覆盖 AgentRun events 拉取计划和 projection cursor 推进逻辑。该测试必须构造长 trace 或大量旧 source events 的反例，并断言下一次 `afterSeq` 来自 durable projection state/checkpoint 而不是遍历历史 trace event；测试应通过受控计数器、throwing iterable 或等价方式证明 cursor 计算复杂度不随历史 trace/event 数量线性增长。任何恢复到从 trace tail、result payload 或 events 数组扫描最大 sourceSeq 的实现都应让该单元测试红灯。

回归还必须覆盖读侧推理清零。后端和前端测试应构造最后一条 trace event 为非终态 `completed`、message text 为空、result/trace/session summary 字段互相矛盾或投影暂时滞后的样本，断言 API 与 DOM 不从这些原始字段推断 terminal/final/running。收口证据应包含负向源码扫描，至少覆盖 `result?.status ?? trace?.status`、`trace.status` 终结 turn、last event `completed` 终结 turn、message/session/list 状态覆盖 turn、elapsed timeout 改写 terminal 和 no-final-response 占位 final text 等读侧推理路径。

fake-server Playwright 应使用目标 node/lane 的真实采集脱敏 fixture，覆盖 session 切换、刷新、SSE 断线重连、REST 补洞、trace 分页和 terminal final response。D601 v0.3 public origin web-probe 应验证不切 session、不刷新页面、fresh deep link 和多观察者场景都通过同一 read model 收敛。

fake-server Playwright 还必须覆盖 timing display 红灯：在固定 projection timestamp、禁止 SSE/API 后续更新、使用浏览器 fake clock 推进的条件下，running 消息头部的“最近”和“耗时”应随 fake clock 前进；completed/failed/canceled/blocked 等终态消息的 sealed 耗时不得随 fake clock 前进。该测试不得读取内部 store，也不得通过更新 fixture、重新请求 API 或触发 projection repair 判定通过。

AgentRun rolling recovery 回归必须覆盖上游 stale ledger 与不可恢复 blocker 样本：manager rolling 后 AgentRun diagnosis 仍处于 pending observation 时，Workbench list/detail/messages/turn/trace 应一致展示 projecting/degraded/stalled diagnostic；terminal facts 后续提交后，同一 durable projection writer/finalizer 应自然推进 sealed final response。测试不得通过 GET read-through、Web reload、切换 session、result fallback 或 fake-server 后门把 pending 状态改成 completed。

Terminal outbox recovery 回归必须覆盖 events page 缺 terminal、command result 已 completed 且含 final response 的样本。测试应断言 projection resume 会强制读取 command result authority、写入 completed terminal commit，并让 session/detail/messages/turn/trace events 全部从 durable facts 看到同一 terminal；随后用户 cancel 应返回 already-terminal/no-op，不得把 completed 改成 canceled。历史 trace 可见性回归必须覆盖同一 session 后续新 trace 成为 `lastTraceId` 后，旧 trace events 仍以 turn/message 关系返回 200 或明确 projection blocker。

Read model degradation 回归必须覆盖 PostgreSQL pool connect/query timeout：Workbench facts query 应返回结构化 503/degraded 或 readiness/blocker，并记录脱敏诊断；测试不得通过未捕获异常、进程退出、liveness EOF 或 GET read-through repair 判定通过。

Sealed final response 回归必须构造 completed assistant 主正文已经展示后，`/v1/workbench/turns/{traceId}`、`/v1/workbench/traces/{traceId}/events`、SSE 或 realtime diagnostic 发生 timeout/500/gap/close 的样本，断言主消息正文、finalResponse、message status 和 turn terminal 保持 sealed 不变，诊断只出现在 trace detail、transport diagnostic、session health 或消息详情入口。回归还必须覆盖 trace detail 延迟返回、分页缺口和用户手动展开/折叠状态，确保 late update 不 remount 主消息卡片、不重置用户控制状态。

多 turn / running steer 回归必须覆盖连续 prompt、运行中 steer、terminal 后再次 prompt 和 cancel no-op 的组合。用例应断言 `turn-summary` 的每个用户消息绑定正确 traceId/runId/commandId；`trace-frame --turn N` 与 `trace-frame --trace-id <id>` 读取同一 turn；running frame 的 `Final Response` 为空；terminal frame 的 final response 是 sealed assistant final part；后续 steer/cancel 不会让 analyzer 把普通 prompt 标成 `prompt-routed-to-steer`，也不会让 trace-frame 误选用户 prompt、backend diagnostic 或 steer accepted 中间文案作为最终回答。

0repair 验收必须先在 fake-server + Playwright 中形成红灯：fresh context 直达 session A、普通点击切到 session B、A 的 delayed detail/turn/trace/SSE 晚到、用户没有提交 B 的新消息时，B 的 URL、active tab、message card、composer 和 trace detail 不得出现 A 的 user/agent 文本、running 状态或 terminal trace。web-probe 必须连续截图观察提交前、running、中间态、terminal 和晚到事件；不得用 reload、切换 session、`sessionRepair`、`realignFreshSession`、localStorage 修改或 helper 自动点击修正页面后再判通过。

D518 HWLAB v0.3 的 issue #1217 closeout 必须在真实 public origin 上完成 fake-echo 与 dsflash-go 两类 Workbench 哨兵。fake-echo 验收要求同一 session 多轮 prompt 后，刷新、切换 away/back、deep link 和双观察者场景的 message role sequence 保持 `UAUA...`，不得出现 user cluster、session rail title fallback、cross-page projection divergence 或 activeSession/routeSession 漂移。dsflash-go 验收要求 trace rows 按 Workbench projectedSeq/aggregateSeq 单调，completion row 与 sealed final response 属于同一 trace/turn，终态 duration 不继续增长，Code Agent 卡片耗时、完成行耗时与 ReadModel timing 字段一致。关闭 issue 前，两个哨兵都必须连续 3 次通过或给出明确未通过 blocker；不得通过减少轮数、跳过刷新/切换步骤、提高 120s 预算、忽略 WBC-011/WBC-046/trace-row-order-nonmonotonic/timing mismatch，或在 probe/analyzer 侧补偿来判绿。

### 6.8 WB-PROJ-REQ-008 code reference rule

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-008 | 代码引用 | PJ2026-010401080308 CodeReference | [规格治理](spec-governance.md) |

本专项范围内新增或重构的核心源码文件，文件头部必须标注遵循的 SPEC 编号、短名和实现引用版本，例如：

```text
SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p1-zero-split-durable-realtime
```

同一文件同时承载 API契约、HWLAB接入、Web工作台或 Agent编排职责时，应在同一头部追加对应 SPEC 编号与实现引用版本。实现文件不得只写 issue 编号、`latest` 或 `current`。自动生成文件、第三方 vendored 文件、纯配置、锁文件和无法承载注释头的二进制产物可例外，但对应生成器、渲染器或配置入口必须能追溯到本 SPEC。

### 6.9 WB-PROJ-REQ-009 derived read cache boundary

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-009 | 派生读缓存 | PJ2026-010401080309 DerivedReadCache | [Workbench性能](PJ2026-01060505-workbench-performance.md)、[API契约](PJ2026-010403-api-contract.md) |

WorkbenchReadModel 可以使用 Redis 派生读缓存降低高频读路径的 Postgres 尾延迟，但缓存只保存可丢弃、可过期、可重建的 read model DTO 或内部 read snapshot。优先允许缓存 `/v1/workbench/sessions` actor-scoped session summary page、terminal turn snapshot 和 terminal trace event page；running turn、active session、projectionStatus 非 terminal 或正在变化的页面应使用更短 TTL 或不缓存。

缓存 key 必须包含 schema version、cache key class、actor visibility input、sessionId/turnId/traceId/cursor、projection revision/seq 或等价 authority input。payload 不得包含 raw secret、Authorization header、cookie、DB DSN、完整 prompt、完整 provider payload、完整 stdout/stderr 或未脱敏身份信息。命中缓存前必须能确认 authority input 匹配；revision mismatch、decode failed、actor mismatch、stale 或 Redis unavailable 时，ReadModel 必须走 durable facts read 或返回结构化 degraded，不得把缓存内容作为 lifecycle、terminal、final response、not-found、deleted 或 archived 的判断依据。

失效策略必须跟随 projection commit。Projection writer/finalizer 成功提交 session、turn 或 trace revision 后，应精确失效相关 key；若阶段上只能依赖短 TTL，自然过期策略必须与 user-visible freshness SLO 匹配，并通过 API/diagnostic 暴露 `cacheAgeMs` 和 projection revision/seq。Redis 中是否存在 key 不得用于推断 session lifecycle；canonical deleted、archived、not-found、running、completed、failed、blocked 和 final response 仍只能来自 durable Workbench facts。

派生缓存不能成为 GET repair 或多来源仲裁路径。缓存 miss 不得触发 AgentRun、result polling、workspace repair、trace polling 或 projection finalizer；缓存 hit 不得掩盖 SQL/schema/nullability bug、projection blocker、`row_scan_failed` 或 facts corruption。测试和负向扫描必须覆盖 cache hit、miss、stale、unavailable、revision mismatch、actor 隔离和 running turn 不长 TTL。

### 6.10 WB-PROJ-REQ-010 aggregate event stream

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-010 | AggregateEventStream | PJ2026-010401080310 AggregateEventStream | [API契约](PJ2026-010403-api-contract.md)、[Web工作台](PJ2026-010401-web-workbench.md)、[Workbench性能](PJ2026-01060505-workbench-performance.md)、[Agent编排](PJ2026-0102-agent-orchestration.md) |

Workbench aggregate event stream 必须成为 Workbench 投影的唯一提交序。所有 admission、normalized AgentRun event、command result、terminal outbox、cancel mutation、diagnostic transition、checkpoint replay 和受控 reprojection 都必须先形成 durable event，再由同一 projector 生成 message、part、turn、trace event、session summary、projection checkpoint、diagnostic、read model cache invalidation 和 SSE outbox。任何直接写 message/status/trace/read cache 而不追加 aggregate event 的路径都属于第二写权。

每个事件必须携带稳定身份与排序字段：全局 `eventSeq`、`eventId`、`aggregateId`、`aggregateSeq`、`aggregateType`、`sessionId`、`turnId`、`traceId`、可选 `messageId/partId`、`sourceRunId`、`sourceCommandId`、`sourceSeq/sourceEventId`、`eventType`、`occurredAt`、`committedAt`、`projectionRevision` 和脱敏 payload 摘要。`eventSeq` 用于跨 aggregate SSE replay 和审计；`aggregateSeq` 用于同一 session/turn/trace/message 内的顺序；`sourceSeq` 只用于来源审计和幂等，不得作为 Workbench API cursor。

Projector 必须按 event stream 的提交序更新 read model。terminal event 必须在同一 projector revision 内原子写入 sealed final response、message/part terminal status、turn terminal、trace terminal marker、session running=false、checkpoint 和 outbox notification。terminal revision 之后的 late event、transport diagnostic、trace hydration result、result archive 或 performance diagnostic 只能追加新的 diagnostic/detail revision；不得重新打开 sealed 主消息、把完成行挪到非最后位置、让完成耗时继续增长，或让 trace/timeline 因到达顺序不同而重新排序。

SSE replay 必须以 event stream/outbox durable cursor 为权威。`/v1/workbench/events?afterSeq=N` 或等价入口只能返回 `eventSeq/outboxSeq > N` 的投影 commit notification，并在 replay 完成后切到同一 durable tail；不得订阅进程内 `traceStore`、浏览器本地 pending queue、result polling 或 session list diff 作为 live truth。客户端收到重复、乱序或缺口时，应按 cursor 触发 REST/read model 补洞和 transport diagnostic；不能在浏览器 reducer 内通过 arrival order 合成最终顺序。

Timing 字段必须由 aggregate event stream 和 projector 统一产生。`startedAt` 来自 admission/source start event，`lastEventAt` 来自最新已投影活动事件，`finishedAt` 和 `durationMs` 来自 terminal event 或等价 terminal result；browser local now 只参与渲染运行中相对文案。web-probe 若发现 trace 乱序、完成行不是最后、完成耗时和 trace 首尾耗时不一致、终态耗时继续增长或红色 timing finding，根因修复必须回到 event stream/projector/read model/SSE cursor，不得再通过 Web reducer、probe analyzer、fake-server fixture 或 CSS/DOM 文案做补偿。

迁移实现必须删除旧的多来源仲裁路径。`traceStore.subscribe`、result envelope fallback、session JSON lifecycle、last trace event heuristic、turn polling status priority、localStorage truth、session repair、fake-server 专用重排和 probe 侧完成行修正只能作为负向扫描对象或迁移输入；保留兼容 wrapper 时也必须调用 WorkbenchReadModel 或正式 mutation。负向回归必须构造 source event 乱序、terminal result 先到、trace hydration 晚到、SSE replay gap、cloud-api rolling restart 和浏览器 session 切换样本，证明完成行、final response、duration、trace page 和 session rail 均按 aggregate event stream revision 收敛。

### 6.11 WB-PROJ-REQ-011 CLI trace view and analyzer contract

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-011 | CLI视图 | PJ2026-010401080311 CliTraceView | [Web工作台](PJ2026-010401-web-workbench.md)、[Workbench性能](PJ2026-01060505-workbench-performance.md)、[API契约](PJ2026-010403-api-contract.md) |

`web-probe collect/observe` 是唯一采样保存入口，负责按 5 到 10 秒级别采集 DOM/API/console/network 等事实，并把 frame、sample、request、traceId、runId、commandId 和时间戳写入采样目录。`web-probe analyze` 的 trace 截图是从这些采样事实或同源 read model 渲染出来的文字视图，不得另存一份“截图事实”，也不得在 analyze 阶段重新访问页面来补采样。

CLI 视图必须提供两层：第一层 `turn-summary` 展示多 turn 摘要，至少包含用户消息摘要、turn index、traceId、runId、commandId、status、elapsed、recent、warning 和最终内容摘要；第二层 `trace-frame` 按 turn、traceId、sample seq 或时间点渲染单 turn 的有序事件行。`trace-frame` 必须固定包含 `Final Response` 分区；running 或未 sealed 时显示 `(空内容)`，terminal sealed 后显示 sealed final part 的实际内容。该分区不得从用户 prompt、backend diagnostic、中间 assistant text、steer accepted 文案或 DOM 最后一行回退。

Trace-frame 行顺序必须来自 Workbench projectedSeq、aggregateSeq、part order 或采样事实中的同源顺序字段；完成行、assistant part 和 diagnostic 行不得按 DOM 到达顺序、文本时间戳排序或 analyzer 私有排序重排。若同一 frame 中存在乱序、completion row 非最后、duration 不一致或缺少 order 字段，CLI 应直接标红或 warning，保留原始矛盾供人工判断，而不是把视图修正成看起来正确。

自动判别器必须以 CLI trace 视图为判据。finding 生成、降级和消除都要能在 `turn-summary` 或 `trace-frame` 中复核；不能只依赖机器规则把长期存在的问题误判为通过。任何单次 Workbench 操作、turn 投影、命令执行、finalizer catch-up、trace hydration 或 analyze 阶段超过 120 秒，都必须输出 severe timeout warning，并要求进一步调查；该 warning 不得被普通 poll retry、fallback route、自动刷新或 analyzer 补偿吞掉。

prompt、steer、cancel 和 retry 必须在 CLI 视图中保留 control command boundary。普通 prompt 的 POST `/v1/agent/chat`、运行中 steer 的 `/v1/agent/chat/steer`、cancel 的 `/v1/agent/chat/cancel`、以及 terminal/no-active no-op 必须分别显示 action、target turn/run/command、HTTP status、accepted/no-op/blocker 和相关 ids；analyzer 不得因为同一时间窗内出现 steer 请求就把此前 prompt 判定成 `prompt-routed-to-steer`。

D518/v0.3 business trace closeout 必须能从 node/lane YAML 解析 AgentRun namespace、control-plane route 和 public origin，而不是硬编码 namespace 或依赖当前 kubectl context。`diagnose-code-agent --target D518` 或等价受控入口应输出 traceId、sessionId、runId、commandId、runner identity、projection/read span、OTel span 状态和 redacted namespace/endpoint 摘要；缺少 namespace、OTel span、trace frame 或 ReadModel digest 时，issue 不能仅凭 PR 合并或构建通过关闭。

### 6.12 WB-PROJ-REQ-012 serve/session aggregate authority

| 编号 | 短名 | 主责模块 | 关联模块 |
| --- | --- | --- | --- |
| WB-PROJ-REQ-012 | 会话聚合权威 | PJ2026-010401080312 ServeSessionAggregate | [API契约](PJ2026-010403-api-contract.md)、[Agent编排](PJ2026-0102-agent-orchestration.md)、[Web工作台](PJ2026-010401-web-workbench.md)、[HWLAB接入](PJ2026-010205-hwlab-dispatch.md) |

Workbench session 必须是用户可见执行状态的 aggregate root。prompt、steer、cancel 和 retry 先写成 durable input/command fact，再进入 AgentRun run/command、runner job 创建、steer delivery 或 cancel delivery；admission route 只能返回 durable accepted、structured conflict、structured no-op 或 blocker，不得让浏览器请求等待 Kubernetes Job 创建、runner 启动、result polling 或 trace hydration。

每个 input/command fact 必须至少绑定 `sessionId`、`turnId`、`traceId`、`userMessageId`、`assistantMessageId`、`commandId`、action、target run/command、`admittedSeq`、`promotedSeq`、status、no-op reason 或 blocker。`turnId/traceId/messageId/partId` 必须在 dispatch 前或首次 projection commit 前稳定分配；后续 AgentRun event/result、steer accepted、cancel requested、terminal result 和 diagnostic 都必须回绑这些身份。提交 HTTP 500、AgentRun dispatch failure 或 delivery timeout 也必须绑定到本次 input fact，不得回落到旧 trace、旧 final response 或旧 active card。

同一 session 必须有一条受控 execution lane。wake、interrupt、steer、cancel、retry 和 runner terminal 之间必须按 session aggregate seq 判定新旧；stale wake、late runner event、旧 turn polling、DOM active card、session list summary、last trace row 或浏览器 local state 都不能覆盖更新 turn 的状态。没有 active turn 的 cancel/steer 必须返回结构化 no-op 或 error；已 terminal 的 cancel 必须返回 already-terminal/no-op 或触发 terminal projection catch-up，不能重新打开 sealed turn 或覆盖 terminal facts。

同一 session execution lane 必须有可审计的 AgentRun 承载机制。优先形态是同一个 AgentRun run 接收连续 command，并让已 claim 的 runner 在 idle timeout 内继续 poll 后续 command；若后续实现选择 session runner lease 或跨 run warm runner channel，也必须暴露稳定 session/channel/lease identity、command handoff、cancel/interrupt fencing 和 terminal diagnostic。仅在 `sessionPolicy`、metadata、thread id 或 PVC 上标记“reuse”，但每个 Workbench turn 都新建独立 run-scoped runner Job，不满足本要求，也不能作为 10x canary 或 120s 性能验收通过证据。

Message/part 是用户结果的显示权威。assistant final response 只能来自 sealed assistant final/text part，diagnostic、stdout/stderr、transport error、steer/cancel accepted 文案和 analyzer summary 必须分仓展示。trace rows、turn timing、session running、completion row 和 duration 必须从同一 session aggregate/event stream/read model 派生；`trace-frame --trace-id newTrace` 不得显示旧 trace 行。若缺少 order、缺少 terminal 或存在 projection 矛盾，CLI/API/Web 必须暴露 projection blocker 或 warning，而不是读侧补齐。

兼容 `/v1/agent/*`、conversation path、旧 Web route、CLI renderer、SSE 和 fake-server 都只能调用同一个 mutation/read model。保留 wrapper 时，它只能做鉴权、schema 映射和旧字段降级，不拥有第二套 prompt admission、status、final response、timing 或 trace authority。

本要求参考 OpenCode serve 的职责边界，但不复制其技术栈。HWLAB 应学习其 `SessionInput` admitted/promoted、session run coordinator、run-state、message/part event 和 abort route 分工：输入先成为可审计事件，run-state 集中仲裁 busy/idle/cancel/retry，message/part 按顺序追加并 sealed，读取路径只消费 page/cursor/read model。后续实现不得继续沿用 trace tail、result envelope、DOM、session list 或 analyzer fallback 作为架构补丁。

## 7. 过程控制

本规格的执行证据保留在对应 GitHub 执行 issue、PR、web-probe 报告和 Playwright artifact 中；规格正文不承载长证据、运行日志或一次性排障流水。

本专项关闭前，执行 issue 必须回写以下证据：专项 SPEC commit、后端写点/读点清单、旧路径最终状态、PR、D601 v0.3 发布证据、fake-server Playwright 结果、public origin web-probe 的 script SHA/runDir/report SHA/截图 SHA，以及父级 Web/API/HWLAB接入/Agent编排规格是否仍与实现一致。Workbench Redis 派生读缓存执行 issue 为 [#1870](https://github.com/pikasTech/HWLAB/issues/1870)，P0 SPEC-first 子 issue 为 [#1871](https://github.com/pikasTech/HWLAB/issues/1871)，后续实现必须引用 `draft-2026-06-22-p1-workbench-redis-derived-cache`。

web-probe 历史窗口 trace 乱序、完成行非最后、耗时不一致和红色 finding 的架构修复执行 issue 为 [#2055](https://github.com/pikasTech/HWLAB/issues/2055)。阶段跟踪必须以子 issue 为准：P0 SPEC-first [#2057](https://github.com/pikasTech/HWLAB/issues/2057)、P1 event schema/projector [#2058](https://github.com/pikasTech/HWLAB/issues/2058)、P2 read/SSE seq replay [#2059](https://github.com/pikasTech/HWLAB/issues/2059)、P3 Cloud Web trace/timing 补偿清理 [#2060](https://github.com/pikasTech/HWLAB/issues/2060)、P4 web-probe/OTel 验收固化 [#2061](https://github.com/pikasTech/HWLAB/issues/2061)、P5 D601/v03 长程验收与旧路径删除 [#2062](https://github.com/pikasTech/HWLAB/issues/2062)。后续实现必须引用 `draft-2026-06-24-p0-aggregate-event-stream`，并在关闭各阶段 issue 时说明是否仍存在读侧 repair、到达顺序排序、UI timing fabrication 或 probe 侧补偿。

Workbench turn/message/trace 投影 authority 与 CLI trace 视图收敛的架构修复执行 issue 为 [#2079](https://github.com/pikasTech/HWLAB/issues/2079)，正式 20 轮长程采样父 issue 为 [#2072](https://github.com/pikasTech/HWLAB/issues/2072)，运行中 steer 与前 1-5 轮采样子 issue 为 [#2078](https://github.com/pikasTech/HWLAB/issues/2078)。后续实现必须引用 `draft-2026-06-24-p1-opencode-message-part-authority`，并在各阶段子 issue 中说明是否已经消除 final response 误选、turn-summary/trace-frame traceId 误归因、completion row 非最后、duration 不一致、prompt/steer/cancel 边界误判和 analyzer-only 误判。

Workbench serve/session aggregate authority 架构修复执行 issue 为 [#2125](https://github.com/pikasTech/HWLAB/issues/2125)。阶段跟踪必须以子 issue 为准：P0 SPEC-first [#2128](https://github.com/pikasTech/HWLAB/issues/2128)、P1 durable input/command 与 session aggregate seq [#2127](https://github.com/pikasTech/HWLAB/issues/2127)、P2 message/part/turn/trace read model 与 terminal atomic commit [#2126](https://github.com/pikasTech/HWLAB/issues/2126)、P3 prompt/steer/cancel run-state authority [#2129](https://github.com/pikasTech/HWLAB/issues/2129)、P4 REST/SSE/CLI/Web 单源消费与 legacy wrapper 降级 [#2131](https://github.com/pikasTech/HWLAB/issues/2131)、P5 删除 fallback/repair/renderer 补丁路径 [#2130](https://github.com/pikasTech/HWLAB/issues/2130)、P6 fake-server 与 D601/v03 20轮原入口验收 [#2132](https://github.com/pikasTech/HWLAB/issues/2132)。后续实现必须引用 `draft-2026-06-25-p0-serve-session-aggregate-authority`，并在关闭各阶段 issue 时说明 prompt admission、steer/cancel、run-state、message/part、trace/timing、final response 是否已经共享同一 session aggregate authority。

dsflash-go 10x canary 超过 120s、同一 Workbench session 连续 turn 创建多个独立 AgentRun runner/job、DOM trace rows 与后端 trace API 不一致的证据 issue 为 [#2171](https://github.com/pikasTech/HWLAB/issues/2171)。后续实现必须同时引用 `draft-2026-06-25-p0-session-warm-runner-contract`，并在 P1/P3/P6 closeout 中证明同一 session 的连续 turn 已进入同一 execution lane 或等价 warm runner/session command channel；若仍出现每 turn 新建 run-scoped runner Job，必须保持验收 red，而不是提高 120s 预算、减少轮数或用 renderer/probe fallback 掩盖。

D518 Workbench 会话 timeline 与刷新一致性执行 issue 为 [pikasTech/unidesk#1217](https://github.com/pikasTech/unidesk/issues/1217)。后续实现必须引用 `draft-2026-06-28-p0-d518-session-timeline-consistency`，并在 closeout 中回写 UniDesk SPEC PR、HWLAB v0.3 PR、D518 control-plane rollout commit/PipelineRun/Argo revision、fake-echo 与 dsflash-go 三连哨兵报告、OTel business trace 诊断、ReadModel role sequence/digest 证据，以及是否仍存在 session title fallback、cross-page divergence、trace row nonmonotonic 或 timing mismatch。
