Files
deepseek-harness/missions/ui-tech.md
T
imccyu 0681ac47de chore(gui): mission work logs
chore(gui): mission work logs — cordis design finalization, tool-card wire archive, incident records

chore: missions

chore: missions

chore(gui): mission ledger — batch-2 answers, parallel dispatch state, jsdom coverage re-scope

chore(gui): ledger — night-mode standing orders (self-commit small, no push, 5-min refresh)

chore(gui): ledger — jsdom batches 2-4 landed (233 green), coverage probe next

chore(gui): ledger — web-ui coverage probe 65%, four-tier fill plan approved

chore(gui): ledger — cordis-impl B1 state after third API drop, decisions on file

chore(gui): ledger — 01:32 patrol snapshot (jsdom tier-1 landed, coverage-fixer probed)

chore(gui): ledger — 01:37 patrol (peer src trio landed, coverage-fixer still silent)

chore(gui): ledger — 01:42 patrol (jsdom tier-2 landed, peer committed x2, coverage-fixer 2nd probe)

chore(gui): ledger — coverage diagnosis complete (6-file gap list), web-ui at 91.4%

chore(gui): ledger — 01:46 patrol (B1 done, jsdom tier-3 landed, coverage fix batch running)

chore(gui): ledger — 01:51 patrol (jsdom tails x2 landed, B2 underway)

chore(gui): ledger — 01:56 patrol (gateway.ts 337 lines, checkpoint T-7min)

chore(gui): ledger — hold/pending split ruling, coverage-fixer externalize-or-restart ultimatum

chore(gui): ledger — 02:01 patrol (B2 done, jsdom final arms, coverage ultimatum pending)

chore(gui): ledger — 02:03 checkpoint executed (fixer2 respawn, four lanes released, three owners cold-started)

chore(gui): ledger — all six lanes acked, type isolation first live proof (client closure clean)

chore(gui): ledger — 02:08 patrol (all seven lanes active, wire carrier assembled)

docs(gui): respond-design task checkpoint — apiproxy wire-layer recon done

chore(gui): ledger — P0-2 contributor AGENTS.md landed (dd28a5019)

docs(gui): respond-design checkpoint 2 — host-side recon (stub respond, frame types, approval seam, ACP answerer precedent)

docs(gui): OOP debt inventory — seven territories, 2 real debts (createApiProxy, createFixtureApi), rest ruled keep-as-is

docs(gui): disambiguation note on the archived i18n design task

chore(gui): ledger — 02:12 patrol (exclude removed, mixed-knife incident under reconciliation)

chore(gui): ledger — 02:15 wave (jsdom mission closed, OOP audit done, B3 isolation proof, mixed-knife resolved)

chore(gui): ledger — 02:17 patrol (attribution reversal filed, arch-session probed)

chore(gui): ledger — 02:22 patrol (B4 done, B5+B6 merged batch, arch-session deadline set)

docs(gui): respond-design checkpoint 3 — client-side recon (pending map, PendingCard onRespond stub, AbstractApiClient.respond ready, bootHost missing approval mounts)

docs(gui): peer carrier territory review — 2 fixes (SSE cancel leak, route-reservation guard), 1 ruling ask (RPC-log visibility), compliance ledger

chore(gui): ledger — 02:27 (arch-shell respawn, territory review verdicts routed, ask-deny finding flagged)

docs(gui): P1-5 respond design page complete — pending registry, wire answerer, client state machine, first-wins arbitration

chore(gui): ledger — 02:31 patrol (respond design complete, B5 wire smoke green, shell knife 1 underway)

docs(gui): respond design — add §0 status warning (web host ask defaults to deny), mount-behavior delta, no-timeout ruling with Config discipline

chore(gui): ledger — 02:42 patrol (respond line closed pending review, B7 last piece underway)

docs(gui): respond design contract review — direction pass, 2 doc fixes (settle-order contradiction, answering-state race), A/B/C compliance ledger

docs(gui): respond design — contract review fixes (R1 verify-before-delete arbitration, R2 answering+resolved-frame transition, ask dual-source wording, rejected-is-ok-value note)

chore(gui): ledger — 02:46 patrol (respond line final, two user decisions distilled, arch-shell deadline)

docs(gui): respond review addendum — contract-gap ruling: approve plan A (ApprovalRequest.id), wire unchanged, drop plan B backscan

chore(gui): ledger — cordis B7 summit: real-browser 10/10 green, user acceptance criterion proven

docs(gui): respond design — contract gap #4 approved as plan A (ApprovalRequest.id), backscan fallback retired, blade 0 prepended

docs(gui): respond design — final polish (owner-approval vs user-go-ahead wording, implementation handoff notes)

chore(gui): ledger — 02:51 (shell-exec third respawn with operational script, respond line 4-knife final)

chore(gui): ledger — 02:53 wave (respond five-knife true final, B7 closed 12/12, client.ts green)

chore(gui): ledger — 02:56 patrol (cordis closeout bounced pending R1/R2/N1, shell-exec first sign of life)

chore(gui): ledger — 03:01 patrol (R1/R2/N1 remediation in flight across four files)

chore(gui): ledger — cordis line officially closed and archived, verified on disk (24 knives, 12/12, reviews closed)

chore(gui): ledger — 03:06 patrol (shell-exec final window, lowered first-knife bar)

chore(gui): ledger — 03:11 patrol (shell line iced-broken: three registries on disk)

chore(gui): ledger — 03:15 patrol (quiet window, both active lanes within threshold)

chore(gui): ledger — 03:20 patrol (shell five files up, api-proxy plan reported)

chore(gui): ledger — 03:25 patrol (shell migration in flight with history-preserving moves, webserver green)

chore(gui): ledger — 03:30 patrol (shell knife-1 in verification, api-proxy patching)

chore(gui): ledger — shell knife 1 accepted (694cecc53), knife 2 released

chore(gui): ledger — 03:40 patrol (knife 2 pre-move stage, cold-list spec appears)

chore(gui): ledger — 03:45 patrol (rpclog moves staged, api-proxy two specs in flight)

chore(gui): ledger — 03:54 patrol (knife-2 code done, coverage full-run final check)

docs(gui): coverage-fixer task ledger — fixer2 takeover, per-file fix log, isolated reportsDirectory pitfall

chore(gui): ledger — coverage lane closed and accepted (a19f069a5), the PR #443 CI fix knife

chore(gui): ledger — 04:04 patrol (knife-2 calibration, sole active lane)

chore(gui): ledger — shell knife 2 accepted (f8fb77b95), knife 3 released as final night task

chore(gui): ledger — 04:19 patrol (knife-3 past half: callback chain through, ToolCallDetail up)

chore(gui): ledger — night closeout summary: seven lanes closed, wake-up decision sheet

chore: missions

chore: missions

chore: missions

chore(gui): mission-local browser/probe verify scripts under missions/scripts/

The six acceptance/probe scripts move here as mission-side working
material (headers and relative imports adjusted for the new location):
carrier-errors, rpclog-panel, session, session-real,
webserver-backpressure, webserver-hardening.

chore(gui): verify-relocate mission log

chore(gui): verify-relocate mission log — R1 guard addendum

chore(gui): gates-continue mission log — CI-equivalent sequence all green

chore(gui): VS Code 扩展体系双边设计调研报告

chore(gui): 调研追加 4.5 节——git 扩展数据面与 scope 绑定

docs(gui): web plugin system RFC — walkthrough + design notes

docs(gui): RFC — restore existing SSE/POST as the v1 transport; envelope rides on it (D16)

docs(gui): RFC — envelope demoted to chan-dispatch, scope out of envelope, rpc-log cut, peer deferred, scope tree is native cordis (D17-D21)

docs(gui): RFC — hooks re-derived from component needs: useWatch/useAction only, useService removed; sessionHub cut, projections user-space, router rename, loader-only root (D22-D25)

docs(gui): RFC — drop stale fork vocabulary (vendored cordis has Fiber only; scope = mintScope pattern), hook idempotence contract (D26-D27)

docs(gui): RFC — session precision seam: plugins read scope key (host paradigm), React gets it from tree position via SlotOutlet (D28)

docs(gui): RFC — domain hooks owned by plugins over framework primitives; useConversation paradigm carried over (D29)

docs(gui): RFC — ctx services are the inter-plugin API (cordis proper); declarations are wire-only; get(id) returns scoped ctx (D30)

docs(gui): RFC — full ctx.conversation walkthrough: root-singleton scope-sensitive service, caller-ctx scope key, get(key) as scoped ctx (D31)

docs(gui): RFC — no client-side agents collection: session state machine already expresses the duality; agent resolution stays host authority (D32)

docs(gui): RFC — v1 stays session-precision, no agent-level isolation; incarnation/agent-axis designs archived in ledger (D33)

docs(gui): RFC walkthrough — full rewrite to final state (D16-D33 consolidated), end-to-end chain restored

docs(gui): RFC — apiproxy demoted to generic channel routing; domain RPCs dissolve into owner plugins (D34)

docs(gui): RFC — TS-interface-first wire contract (zod internal), conversation owns the dialogue frame with pluggable views (D35)

docs(gui): RFC — page skeleton (sidebar+conversation), projects as plugin not service, nested slots via owner registries (D36)

docs(gui): RFC — SlotMap declaration-merging slot model: single register API, inject-as-ownership, FC-typed registration, typed outlets (D37)

docs(gui): RFC — slot props whitelist: identity, display params, materialized snapshot slices, stable UI callbacks (D38)

docs(gui): RFC — end-to-end data flow: three transforms, equality protocol table, immer placement; i18n/theme kept standard (D39-D40)

docs(gui): RFC — full external-injection model: props carry values + stable injected hooks; shared/client/react example rewritten (D41-D43)

docs(gui): RFC final trio — modules.md (agent implementation spec), architecture.md (human walkthrough), plugins.md (business plugin inventory)

docs(gui): RFC — props three-source merge (scope-standard useSession auto-injected); keyed key vs list id disambiguated (D44)

docs(gui): RFC — inject comment says what it is (the React-facing props bundle); SessionHandle rename; snapshot-production story unified on buildSnapshot

docs(gui): RFC architecture — full React component tree walkthrough: props three sources, slot vs plain children, hook taxonomy per node

docs(gui): RFC — module map finalized (ui-slots/web-react/connection/runtime/ui-*/web); slots onChange replaced by cordis events; toolcall dimension; detail sidebar default-collapsed with toolName-keyed routing

docs(gui): RFC plugins — openDetail relay chain: toolcard calls chat-view injected action, chat-view relays to conversation sidebar

chore(gui): progress ledger — full archive rewrite: RFC outcome digest, open gaps, dispatch plan, cold-start entry

docs(gui): RFC grill pass 1 — SlotScope axis (root/session) on declares, Gate dependency inversion, inject handle by scope, W5 acceptance list, gantt relay chain fixed

docs(gui): figma analysis — sidebar/projects/sessions 区域交互视觉理解报告

docs(gui): figma 解析报告 — details 面板/多视图 tabs/未来功能区盘点 + slot 需求清单

docs(gui): figma 对话主区解析报告 — 消息流/tool calls 变体/审批接管输入框/Header tabs/视觉 token

docs(gui): plugins.md rewritten from figma analysis — three-column layout, full slot reservation table, selection channel, composer-takeover approvals, phased scope

docs(gui): layout dynamics ruled (drag+collapse both rails, details yields first, composer swap-panel, same-component transition); toolviews promoted to named scope-aware registry

docs(gui): P-I scope locked (details minimal, dual theme, chat-view, custom toolview sample); teammate dispatch plan — 6 owners by package, dependency-driven waves, contract arbitration

docs(gui): P-I api-contracts (full inter-package API spec) + dispatch plan (T0 skeleton knife, 7-dev roster, task briefs, milestones)

docs(gui): api-contracts v2 — scope tree in P-I, bundle loader + per-plugin CSS isolation in P-I, agent-scoped toolviews live, zustand engine, renames (SessionProvider/ObservableSnapshot/SessionBinding), router owns all shell view-state

docs(gui): services roster + progressive loading (no blocking loadAll), SlotsService as real cordis Service, renderSlot/renderSuspenseSlot duo, ui-traj teammate

docs(gui): loading-chain gaps ruled — dev=rebundle no HMR, ui-primitives package, externals on globals (no import map), host injects __DSH_BOOT__ into HTML (zero round-trip)

docs(gui): api-contracts v3 + dispatch v2 final — 12 packages, services merged in, progressive loader, global externals, __DSH_BOOT__ injection, 8-dev roster with convo split and ui-traj

docs(gui): v3 amendments — router renamed ctx.layout, ui-trajectory has no service (pure consumer sample), wait-for-settled loading (no Suspense in P-I, ledger 6b)

chore(gui): progress — pre-compact final state: v3 revision chain, 8-dev roster, T0 procedure, doc authority order

docs(gui): authority banners — modules/architecture get v3 term-mapping headers, walkthrough marked as archived process doc

docs(gui): cssdesign token set is THE theme source (--dsw-* variables, data-ds-dark-theme switch); recorded in contracts + progress

docs(gui): architecture.md full v3 rewrite — loading chain, 12-package map, service roster, slot/inject/toolviews, data flow, component tree, perf model, all current

docs(gui): contracts — UI plugins are dual-entry host plugins (node half serves client asset via ctx.webPlugins; __DSH_BOOT__ derives from it; client-closure gate back in scope)

docs(gui): contracts — closure-factory bundles with DI require (no globals), package.json dshWeb declarative discovery (no serve ritual), create-then-send empty state with project picker, ancestry() for breadcrumb, unload stubbed until HMR, props.renderSlot confirmed

docs(gui): dshClient declaration (inject/platform/immediately, exports./client), closure-DI require loading — synced across contracts/dispatch/modules/architecture/walkthrough

chore(gui): progress — record final loading-chain rulings (dshClient declaration, closure-DI require, startSession) before compact

docs(gui): architecture.md — developer-facing whole-web architecture on master baseline 6b16a67cb: what exists, what is new, no process narrative

docs(gui): architecture.md — self-contained whole-web architecture: absorbs still-valid substance from the branch RFCs (host layering, four-quadrant RPC, object layer, testing tiers) under the new plugin system as the override

chore(gui): progress — final pre-compact snapshot: contracts digest, apiproxy purity ruling, T0 procedure with first-action list

docs(gui): api-contracts v3 §3.1 — apiproxy purity principle with three-way existing-code verdicts

docs(gui): api-contracts v3 — immediately reinterpreted as static-infra group (8-package dshClient scope, boot manifest reconciliation)

docs(gui): api-contracts v3 — immediately corrected to early-load dynamic group (prod shell must not rebundle); loader shell-held; bundles register their export surface into module table

docs(gui): architecture — align with immediately=early-load dynamic group ruling; loader shell-held; module-table registration of loaded bundles

docs(gui): T0 checklist — 12-package skeleton table, 4-cut sequence, mv/attic/rewire rules (pre-drafted, awaiting go)

docs(gui): t0-checklist — pin figma-flows findings (missing font-family base vars, three alias vars behind upstream)

docs(gui): dispatch v2.1 — drop cordis-web salvage wording, two-wave staffing, loader/immediately boundary updates

docs(gui): progress + t0-checklist ledger — T0 landed, staffing status, execution accounting

docs(gui): api-contracts v3 — arbitration round 1: renderBody deps, RootBindingProvider, flush default sync, prune current, loader subpath, config-source P-I bar

docs(gui): v3 §3.2 connection 导出清单附录(rt-core 对账)+ rt-core 实现计划档案

docs(gui): progress — T1 milestone, arbitration round 1 ledger, fw-react timeout escalation

docs(gui): progress rolling update — per-line battlefield state at 00:2x, mailbox-vs-contract lesson, small-batch discipline reinforced

docs(gui): fw-react notes — v3 §2 complete, seven knives, T1/T2 follow-ups

docs(fw-slots): archive — four packages landed, open tails logged

docs(gui): progress — framework layer complete (web-react five, fw-slots four packages), T2 gated on rt-core runtime knife only

docs: api-contracts

docs: style-spec

docs

docs(gui): tsconfig convergence ruling — no host.json, root resumes host-aggregate duty, typecheck = root + client aggregates

docs(gui): missions 根三份 07-18 世代档案加「已被取代」头注——指向 web-plugin-rfc 现行权威并注明新旧对应

missions

docs(gui): progress rewritten for post-closeout state — wave ledger, architecture finale, teammate roster with handover notes, pending-user-command queue
2026-07-22 21:30:30 +08:00

310 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Harness UI 技术架构
> **【已被取代——历史档案】** 本文是 07-18 世代定稿,描述的 dsh-ui-host/ui-server/web-runtime/web-ui 分层已整体重构:现行=host 三包(apiproxy/runtime/webserver+ 12 个 packages/client/* 插件包(双入口 host 插件+bundle loader 动态装载)。现行权威=`missions/tasks/20260721-1520-web-plugin-rfc/` 的 architecture.md(架构)+ api-contracts.md v3(接口);本文引用的 docs/ui-design/ 已删除。wire 四象限消息模型是本文存活演进的设计(现居 architecture.md §3)。本文仅作决策演变考古用。
> 状态:**2026-07-18 定稿,供最终 review**。产品行为见《[UI 产品设计](ui-product.md)》。**wire 字段级权威 = `docs/ui-design/01-protocol.md`**;本稿 §4 只描述协议形状与不变量,冲突处以 01 为准。关键决策的演变过程见文末附表;完整裁决档在 `missions/tasks/20260718-final-review/`。
---
## 0. 范围与硬约束
- **一期只实现 Web localhost launcher**Electron 为**架构同构预留**(协议/注入接缝/进程模型设计保留,见 §9,一期不写实现);TUI 只留协议位置。
- **协议跨端一致、一次定全**:不因只做 Web 丢 Electron/TUI 流程;传输是可替换 adapter,语义层不变。
- **连接下限承诺:单 WebView + 刷新恢复绝对可靠**(含进行中消息 partial,见 §3/§4);多 WebView 并连/读写分离不做、不为其复杂化。
- **一期不改 harness core**:不动任何现有包(唯一已批例外:`dsh-brand``BrandedNumber`)。凡需 core 的能力:**协议按目标形状冻结(稳定 id + revision + CAS 不变量),实现走 UI 层合成,port 按未来 core 原生能力的形状设计**——core 就位后只换 port 实现、wire 与上层零改(迁移清单 §13)。
- **实现可留空、架构必须完整**:定稿即冻结契约;实现按 §10 工作包地图多 worktree 并发填空。
- 核心原则:**Host 权威,UI 是可重建投影**(关页/刷新/断网 ≠ 停任务;transport 断开、client detach、command cancel、session interrupt、launcher stop 五种动作互相独立);**显式 id 强关联**(协议与 reducer 出现"按第 N 个/最近一条/按到达顺序对应"即违规);**模型可见 ⟺ 已日志**。
## 1. 架构总览
```
harness coresession 事件日志 / agent / tools / 审批 / 交互 / persistence,已存在,一期不改)
│ 只读消费(唯一读 ctx 处 = HarnessRuntimeAdapterroot context 订阅)
dsh-ui-host(新增,域层 = ui-apiproxy):窄 port + 投影 P + SessionIndex + 审批/问答 Registry + Provider 事务 + metadata store
│ UiHostPort = ui-apiproxy 的 API 面:进程内接口、零 HTTP 语义,形式上对等 acp-demo「把数据桥出来」但仍在 node 进程内;
│ 每个方法显式标注 unary(单次调用单次返回)/ stream(持续推送),供各平台传输层按此对接(Web HTTP/SSE、Electron 进程内、TUI worker
dsh-ui-server(新增,传输层 = dsc 的 web 后端包):HTTP/SSE front door + AuthGate + ClientSlot + HostStream + SessionMux
│ 承接 `dsc web` 起的 Web 服务器,把 HTTP query/command + Host SSE + SessionMux SSE 转发到 UiHostPortunary→request/response、stream→SSE);
│ 由 apps/ 引入启动,真正对等 stdio-demo / acp-demo 的启动形态;传输可替换(TransportAdapter
dsh-web-runtime(新增,浏览器内 React-free 部分):transport client(对接上述 HTTP/SSE 对等接口)+ Controller + Zustand store(数据状态唯一归属)+ reducer 接线
dsh-web-ui(新增,Web 内 React 骨架):ReactRuntimeBridge + 组件 + slot seam(预留)
apps/web · apps/server(一期,`dsc web` 入口);apps/electron(目录预留)
```
### 关键决策表
| # | 决策 | 结论 |
|---|---|---|
| 1 | 端 | 一期 Web localhostElectron 架构预留(renderer≡WebUI 约束保留);TUI 协议位 |
| 2 | 技术栈 | React + ViteCSS Modules + CSS 变量;Radix 原语;**无 router 库**(一期视图态在 storeURL 深链后置) |
| 3 | 传输 | HTTP query/command + **两条 SSE**Host SSE root + SessionMux SSE),streaming fetch 非 EventSource |
| 4 | wire 契约 | zod 单一事实源,`z.strictObject` + closed discriminatedUnion**出入双向 parse**;先 limit 后 parse;不上 codegen |
| 5 | 对话流协议 | 自定义 Web 协议:复用 harness 事件结构 + tool 状态机合并;弃 ACP 线形状 |
| 6 | 历史加载 | 物化树快照(GET 分页)+ 共享 `applyMutation` reducer**快照含进行中 partial** |
| 7 | 命令语义 | **无 receipt/台账**:普通 HTTP status + typed error;仅 prompt 带 `commandId` 做有界去重;其余 mutation 用 expectedRevision 天然幂等 |
| 8 | 命令↔turn 关联 | **MessageSource 透传 `commandId`**merge-extensible source,随 turn/start、user/message 落日志),provisional 按 commandId 转正 |
| 9 | 错误模型 | **五 category 两级结构**transport / client-ownership / domain / control(resync·reset) / plugin-rpc+ HTTP status 分层;Browser 本地失败不进 wire union |
| 10 | 状态层 | Zustandslice/selector+ 纯 TS ControllerCordis root Context 只承载基础 Service,不进数据态;无 SWR,统一 Controller+store |
| 11 | React 边界 | 组件只经 `ReactRuntimeBridge`selector + typed intent),**不导出裸 Cordis Context**;写路径唯一(§8 |
| 12 | 单客户端 | Server 级 `ClientSlot`;同 `clientId` 刷新=换 generation 接管;异 clientId 拒 `server-in-use`(不泄露占用者) |
| 13 | 多 session 收流 | SessionMux SSE 内多路复用 N 个 channel;切换只改焦点 |
| 14 | 发送 | **steer / queue / stop**1:1 映射 agent 现成两条 FIFO;纪律:不假设一发送一 turn、无 cancelling 产品态 |
| 15 | 审批/问答 | 两类独立资源;稳定 id + revision(1→2) + 单一 CAS + anchorSeqHost 存活期重连可恢复、重启只读 interrupted |
| 16 | Provider | test-before-save 串行事务 + **CommitSlot busy gate** + **操作不随断连死**202+operationId+ probe/discovery 失败切分 + generation lease |
| 17 | Markdown | react-markdown + shiki + **KaTeX**;Mermaid 后置;流式三件套后置移植 |
| 18 | 鉴权 | **一期裸免登录**`dsc web` 监听 0.0.0.0、打印 127.0.0.1+ OriginPolicy(拒域名 Host 防 rebinding);`AuthGate` 接缝预留 |
| 19 | 包结构 | **五库包 + apps**,一设计层一包一 worktree(§10);host 域层不 import 传输层(port 反转) |
| 20 | 插件 | **一期只做:服务端插件列表展示 + Browser Cordis Context 地基**UI 插件(tool renderer/侧栏页)slot seam 仅预留不实装;PluginPort/双端加载留 future |
## 2. 对 harness core 的消费事实(源码已核,file:line 见 08
- session 事件日志:每 session 单调唯一 `seq``assistant/message.sourceEventSeqs` 提供定稿消息↔chunk 锚点;`StreamChunk` 只有 block index(稳定 part id 由投影层从事件 seq 派生)。
- agent 三原语:`send`queued FIFO/ `steer`steering FIFO/ `cancel`(清两 FIFO + abort 当前 step);`whenIdle`/`status` 可观测。Inbox 无按 id 删除——排队项级操作等 core 提供队列项 ID(§13)。
- 审批(`ctx.approval`):同步 waterfall **答复者链**——UI answerer 对非 UI 发起的请求 `next()` 委让;请求仅限 open turn 内;`'never'` 策略下 UI 收不到 ask。resolve 回调是内存态(跨重启恢复需 core,§13)。
- 问答(`dsh-user-interaction`):`ask()` 不写 session 日志(跨重启恢复需 core,§13);一次 ask 可含多题。
- `ctx.tasks.list()` 按 caller session 过滤——活动聚合必须逐 agent 查询或用 `onTaskDone` 计数。
- session/agent 事件是 scope-filtered dispatch——**事件订阅必须在 root context**。
- tool 呈现:`dsh-tools` 提供 `presentCall/presentResult` 原语;callId 合并逻辑由 ui-host 原生实现(acp 包内有同语义实现可参照,共享上提留 future §13)。
- LLM`registerAdapter` 重名 model 抛 `DUPLICATE_ADAPTER` → 替换唯一合法顺序 = **同步 dispose 旧 → register 新**`ctx.llm.stream` 开始时解析 adapter、流中不换(generation lease 天然成立)。
- 原子写文件:ui-host 本地 helperfs-local 的实现未导出且带包内依赖;共享提取留 future)。
- 无现成 HTTP server seam——dsh-ui-server 自带 `node:http`
## 3. 连接模型与生命周期
### 3.1 建连与防护
- **启动 = `dsc web`**:监听 **`0.0.0.0`**(用户定),控制台打印 `http://127.0.0.1:<port>`。局域网设备因此事实可达,但一期不为 LAN 设计(免登录 + 单 ClientSlot 照旧,不承诺多设备体验)。
- **裸 localhost 免登录**:不设 credential/cookie,浏览器直接访问即用。已知代价:同机任何进程、同网段设备可访问 Host(单用户开发机接受)。
- server 侧保留零体验成本防护:**Host header 必须是 `IP字面量:实际端口`**(拒域名形式 Host——DNS rebinding 攻击必经域名,此检查在 0.0.0.0 监听下依然完整拦截);mutation 要求 IP 字面量 Origin + JSON content-type + 协议版本 header;无 wildcard CORS。校验顺序:Origin → 协议版本/generation → admission。
- **鉴权层是架构预留**:HTTP 入口有 `AuthGate` 接缝,一期实现 = allow-allLAN/远程/Electron sidecar 需要时换实现(一次性链接换 HttpOnly cookie 的完整设计已收档),端点集与协议不变。
- `GET /health`:只回 `{ hostInstanceId, protocolVersion, status }`,不回投影。
### 3.2 ClientSlot 与刷新接管
- `clientId``sessionStorage`logical 身份,非凭据)。**bootstrap 前先做最小 version-probe**:协商双方最高共同版本,无交集在 claim slot **之前**拒 `protocol-version-unsupported`
- bootstrap 请求带:`clientId`、支持版本、已知 `hostInstanceId`、已开 session 的 cursor;响应带:`hostInstanceId`、单调 `connectionGeneration``ProtocolLimits`(载荷/深度/条数上限的部署生效值)、Host 快照 + revision、`resumedChannels`(各 channel 恢复结果——**刷新后 cursor 事实源**client 不假设本地留有 cursor)。
- 槽位规则:空 → 原子取得;**异** `clientId` 占用 → `server-in-use`(不泄露 holder);**同** `clientId` → 刷新接管:**同步 current 指针 swap(唯一 fencing 点)** → 旧代 SSE 与未进域层的迟到请求失效(`RequestLease` 在路由进域层前验 generation)→ 旧对象**异步**释放(对象身份 CAS,迟到 disposer no-op)。
- bootstrap 后设 **root attach deadline**:超时未开 Host SSE 即终止该 generation,防"只 bootstrap 不开流"永久占位。
### 3.3 两条 SSE
| 流 | 承载 | 断开语义 |
|---|---|---|
| **Host SSE**root | `host.ready` / `host.mutation`fromRevision→toRevision 严格相邻)/ `host.resync` / `heartbeat` / `stream-error`host 投影 = session 摘要变更、Provider、插件状态、待处理计数 | clean EOF / 写失败 / 心跳超时 → 一次 root teardown:拒迟到请求、关同代 mux、**释放 ClientSlot**Host 已接受任务继续跑) |
| **SessionMux SSE** | N 个 session channel 帧多路复用 | 单独断开**不**释放 slot;按各 channel cursor 重建;连接存活只由 Host SSE 判定 |
- 每条 SSE record 外层信封带 `hostInstanceId / connectionGeneration / streamId`**streamId 每次物理 open 新发**,旧 reader 迟到帧按 streamId 丢弃(generation 不换但 mux 重连的场景靠它 fence)。
- **Host 投影不内嵌完整 session 列表**:列表数据只由分页查询取得;Host SSE 只推单条摘要变更与计数,client 把变更贴到已加载的页上。
- SSE open 失败用 **content-type 分流**:失败 = 200 json `stream.not-opened`(带 category/code),成功 = `text/event-stream` 首帧 readyBrowser 先看 content-type 再选 decoder。
- 重连恢复起点**只信 Browser 侧 cursor**server 的 last-sent 不作恢复依据)。
- `hostInstanceId` 变化 = Host 重启:client 丢弃全部 store 与 cursor,完整重 bootstrap。
### 3.4 channel 生命周期与 replay 窗口
- `channel.open`HTTP)→ server 在**单同步 tick 内**先订阅后读快照(harness 单线程保证快照与订阅之间无空洞)→ 后续帧上 mux。
- 带 cursor 重开(刷新/mux 重建)走显式 replay 窗口:`channel.opened``replay.started` → durable `session.event`* → `replay.completed`**附锚定最终 durable seq 的完整 partial 快照**——进行中消息已生成部分随之恢复)→ live delta。replay 期间**严格隔离** live delta,无交错。
- cursor 过旧/缺口/单 channel 溢出 → 只对该 channel 发 `resync.required`(保留可见内容、暂禁该 session mutation、重取快照),其他 channel 不动;物理失活/协议不兼容才重建整条 mux。
- **mux 重建 = 逐 channel 重走 open**server 不猜续发起点):client 带各 channel 自持 cursor 重开;无可信 cursor 的 channel 直接全量基线。**一期实现允许统一降级为"恒全量基线"**(等价于 cursor 恒过旧),wire 不变。
- 显式 `channel.close` 只停该 session 下行投影,不停 HostSession/任务;**切换路由不隐式关 channel**(无 LRU)。
### 3.5 Web 有序停机(SIGINT/SIGTERM
固定链(Electron 预留共用):关 mutation/新连接/task admission → 等已进入域层的调用返回 → 各 session 取消交互(审批/问答 abort)+ `agent.cancel` → 分别等 `Agent.whenIdle()` **且** task producer done → **flush 各 session(必须在 agent dispose 之前**——`SessionStore.flush` 只接受 attached session)→ retire adapter → 关连接 → dispose Cordis root。busy 判定含后台任务与 pending 交互;任一来源读不到 → `unknown` → 保守处理,不伪装空闲。
## 4. 协议(形状与不变量;字段级见 01)
### 4.1 三条数据流
| 流 | 方向 | 传输 | 承载 |
|---|---|---|---|
| 快照/历史 | C←S | HTTP GET | session 列表(keyset 分页)、历史树窗口(向上翻页)、设置、catalog |
| 实时增量 | S→C | 两条 SSE | host 投影变更;session channel 帧 |
| 命令 | C→S | HTTP POST/PATCH | prompt、stop、channel、审批/问答回复、session 创建/重命名/model、Provider |
### 4.2 端点
| 端点 | 方法 | 说明 |
|---|---|---|
| `/health` | GET | 探活,不占槽 |
| `/bootstrap` | POST | version-probe + claim + 快照 + generation |
| `/stream/host` | GET(SSE) | Host SSE |
| `/stream/sessions` | GET(SSE) | SessionMux SSE |
| `/sessions` | GET / **POST** | keyset 分页列表(cursor 绑 hostInstanceId+index revision+规范化 query,错误三分型 INVALID/QUERY_MISMATCH/STALE/ **创建 session**201+DTO |
| `/sessions/:id` | GET | 历史树窗口分页(cursor 绑 corpus·mode·beforeSeqExclusive,尾部 append 不失效向上翻页) |
| `/sessions/:id/metadata` | PATCH | **重命名等 metadata**expectedMetadataRevision CAS |
| `/sessions/:id/model` | POST | per-session model(下一 request 边界生效,随 request header 落日志) |
| `/sessions/:id/prompt` | POST | 发送 `{commandId, mode: steer\|queue, text, …}` ⇒ 204 |
| `/sessions/:id/cancel` | POST | 停止(整体)⇒ 204 |
| `/channels` | POST | channel open/close |
| `/approvals/:id` | POST | 审批回复(expectedRevision)⇒ 204 = CAS 已赢 |
| `/questions/:id` | POST | 问答整批原子回复(expectedRevision + answers 全覆盖必答项)⇒ 204 |
| `/provider` | GET / POST | 读(脱敏投影)/ 保存 ⇒ **202 + operationId**(事务不随断连死,终态走投影) |
HTTP status 分层(纯 transport 语义):200/201/202/204 成功族;400 解码失败、401/403AuthGate 预留)、404 未知资源、**409 revision 冲突或 busy**、413/415 limit、422 域校验、500/503。错误体 = strict 脱敏 `{ category, code, message?, details? }`Browser 只 switch `category/code`(未知 code 按 category 缺省路径 + assertNever 家规)。
### 4.3 SessionMux 帧(kind 闭合集)
控制帧(不进 reducer):`channel.opened` / `replay.started` / `replay.completed` / `resync.required` / `channel.closed` / `error`;流级 `mux.ready` / `heartbeat`
可 fold 帧(进 `applyMutation`**durable**,带源事件 seq 作 cursor):`message.start`(含 `turnId``role``source.commandId?`/ `part.add` / `tool.update` / `message.end` / `approval.requested|resolved` / `question.requested|resolved`
**live-only** 帧(断线可丢、由 partial 快照收敛):`text.delta` / `reasoning.delta`
durable/live 判别 = **kind 闭合分类**(无 boolean 标记);01 导出 `DurableFrame` / `LiveDeltaFrame` 两个子 union + 分类器函数,其余各层只准 import 分类器、禁止自行按 kind 字符串再分。durable cursor 与 live 无序号耦合(live 靠 TCP 序 + streamId fence)。
### 4.4 幂等与命令关联
- **无 AdmissionReceipt / 无回执台账 / 无 clientToken**。命令一律普通 HTTP status + typed errorresponse 丢失 = 结果未知 → 重读权威投影。
- **prompt 是唯一需要防重的命令**(副作用不可逆且无 revision 保护):client 预生成 `commandId`server 每 session 维护**有界**已受理 commandId 集合去重;HTTP 超时 → **同 id 重试,禁换新 id 盲发**。其余 mutation 全部带 expectedRevision,重放天然失败为 409。
- **命令↔turn 显式关联**server 调 `agent.send/steer` 时通过 merge-extensible `MessageSource` 携带 `{kind:'host-user', commandId}``turn/start``user/message` 事件原样记录,投影把 `commandId` 带上 `message.start` 帧——前端 provisional 项按 commandId **原地转正**。(透传路径以 harness 现有 source 机制为据,实现期首个 spike 验证;不通则回退"user message 到达即替换 provisional"wire 不变。)
- 三层状态即够:本地 draft → provisional(乐观)→ 权威 timeline 项;provisional 不是事实,`hostInstanceId` 变更后标"需用户确认"。
### 4.5 横切不变量(每层 review 对表)
① transport abort / client detach / command cancel / session interrupt / launcher stop 五动作分离;② HTTP 2xx 只表示"已受理/已提交",领域终态只走投影;③ AbortSignal 默认只取消调用方等待;④ disposer 先 fence 再 await quiescence、重复调用共享 promise;⑤ generation/epoch 只 fence 迟到结果,**不承担业务幂等**;⑥ timeout 是独立观察结果,不覆盖真实 outcome(**谁等待谁拥有 timeout**Browser 拥有 watchdog、Host 拥有 drain deadline、transport 拥有 attach/heartbeat/write-stall);⑦ provisional/draft/toast 不是 Host 事实;⑧ mutation 方法禁返回 `Promise<void>`(必须返回 typed 结果);⑨ 错误体禁止推进 revision(必须走 query/resync)。
## 5. server 侧设计
### 5.1 dsh-ui-host(域层,无 HTTP 依赖)
**唯一读 `ctx` 处 = `HarnessRuntimeAdapter`**(组合期装配,root context 订阅事件),下游只见窄 port:`SessionCorpusPort`(单 tick openChannel:快照+订阅无空洞)/ `SessionLifecyclePort`open/resume/**openHistory 只读不激活 Agent**load 可能触发存储 repair 需 per-id 串行)/ `AgentDriverPort`queue=send、steer、cancel、whenIdle、status/ `ToolPresentationPort` / `ApprovalPort` / `QuestionPort` / `AdapterRegistryPort` / `TaskActivitySource`(逐 agent 聚合)/ `PluginStatusSource`(插件状态诊断——HostProjector 不绕 port 直取 cordis registry)。每 port 可窄 fake 单测。
- **投影 P**:直接消费原始事件流(不走 `deriveMessages`);id 派生:消息 id=首条 chunk seq / user·message seqpart id=首个引用该 block index 的 chunk 事件 seq`turnId`=turn/start seqwire 不暴露 block index。tool 按 `CallId` 合并 call/result 为单节点(分支完备:result 先到暂存、只 call 无 result 保持 running、unknown tool 走 generic fallback),card 一致性由投影层保证。**system/上下文注入投诊断投影**,不进普通 timeline。物化快照 = fold(P(log))**天然含进行中 partial**。
事件覆盖纪律:P 的事件表**必须覆盖 `steering/message`**(用户插话进普通 timeline);compaction 产生的合成事件(`source:plugin` 的 replace 类 user/message、compact 后的 assistant/message 改写)**documented-ignore 或投诊断**,不投成用户气泡;harness `SessionEventMap`**merge-extensible union——P 用 documented default 落底**(未知事件计数+忽略,不用 assertNever)。
- **审批/问答 Registry**(一期内存实现,port 形状=未来 core 原生形状):**Host 级单例**harness 的 answerer/provider 注册只许一次,且审批可在无 channel 时到达;per-channel 只做纯投影读取)。稳定 id + revisionpending=1→terminal=2+ **单一 CAS**answer/cancel/timeout/shutdown 竞争,winner 写终态)+ `anchorSeq`(收到 ask 时的 session seq,供时间线插位)+ pending 计数入 Host 投影;channel 重开时 pending 注入基线快照(Host 存活期恢复);`expiresAt` 的唯一 owner = Registry(到期即 CAS 终态)。UI answerer 对 foreign 请求 `next()` 委让。`UiQuestion` 是多题数组(一次 ask 多 item,整批原子答)。
- **SessionIndex**:内存 materialized view + index revision + keyset 切片(backend 只有全量 list 时的第一版实现;range read 留 `SessionLogReader` seam)。`updatedAt` 只随 durable 边界推进;title 状态机 `default→generated(一次)→manual(永不覆盖)`metadata CAS per-session 串行。metadata 存 ui-host 自有 sidecar store`~/.dsc/` 下按 host 数据目录),**不写 session 日志**title 非模型可见)。
- **`applyMutation` reducer** 在 `dsh-ui-protocol`(纯函数);`SessionTree = Record<id,node> + order[]`server fold ≡ client 贴 live(等价律进一致性测试)。
### 5.2 dsh-ui-server(传输层)
`HostEndpoint`(普通 class,非 Cordis service+ AuthGate + `ClientSlot` + `HostStream` + `SessionMux`per-channel 有界队列,durable 溢出→该 channel resynclive 溢出→丢弃;round-robin 防单 channel 霸占为实现期附注)+ `NodeSseWriter`tryWrite→written/backpressured/closed + waitForDrain)。**域层入口二次 generation 检查**HTTP 入口检查不够,admission commit 的无 await 临界段内复查)。**同 session 写命令串行 lane**(防 rename/model/prompt 交错)。依赖 `dsh-ui-protocol` + 自声明的 `UiHostPort` 接口(依赖反转,apps/server 组装),不 import dsh-ui-host。
**`UiHostPort` 方法逐一标注 `unary` / `stream`**ui-apiproxy 的 API 面约定,零 HTTP 语义):unary = 单次调用单次返回(prompt/cancel/审批问答回复/创建重命名/model/provider/列表分页/历史窗口),stream = 持续推送订阅(host 投影流、session channel 帧流)。本层的职责就是把 unary 映射到 HTTP request/response、stream 映射到两条 SSEElectron 进程内 fetch、TUI worker RPC 是同一 port 的另两种承载(预留)。
## 6. 输入与发送
- steer=`agent.steer`(当前轮插话)、queue=`agent.send`(排后续轮)、stop=`agent.cancel`(清两 FIFO + abort 当前 step);空闲发送即开轮。1:1 映射,无中间队列。
- 纪律:不假设一发送一 turn(开轮可 drain 多条);无 `cancelling` 产品态;`agent/queued``whenIdle` 之间 server 维护 busy bit(覆盖 send 后 status 未切 running 的短窗)。
- 排队项刷新后不可见(产品 §4 已声明边界);队列上限由 Host config,超限回 Host 明确拒绝码。
## 7. Provider、模型与 adapter generation
- 单活动 Provider**`ProviderCommitSlot`**:同时只一个保存事务,第二个立即 409 busy 不排队。保存 = **Host-owned operation**202+operationIdHTTP 断开继续跑,终态走投影)。
- 事务序:校验 expectedRevision → 候选隔离 → **连接 probe(判据=真实 drain 一次最小 stream 见合法 terminal finishHTTP 200 不算)** → catalog 准备(discovery 失败 ≠ 事务失败,标 stale)→ stage 临时文件 → rename 前完成全部可失败步 → **原子 rename = 持久线性化点** → 同一同步调用栈内完成 catalog activate + 活动指针 swap + **同步 dispose 旧 generation → register 新 generation**(同 tick 内无解析空窗)→ 发布投影。
- generation lease`retiring` 拒新用、在飞 stream 持旧代到自然结束、引用清零才 dispose(取 lease 时机 = stream 首次迭代)。Catalog 五级:remote→builtin→last-success+stale→builtin+stale→empty+stale。legacy 精确 `registerAdapter` 与 UI 活动 Provider 撞 model 路由 → fail loud `AMBIGUOUS_ADAPTER_ROUTING`
- API Key 明文 `~/.dsc/` owner-only0600);公开投影只含 `hasApiKey`;settings 读接口可回明文(同 realm 可信);日志/事件/诊断统一脱敏;不读环境变量。原子写用 ui-host 本地 helper。
- 持久记录 `PersistedProviderRecord` **含 last-good catalog 字段**;表单只改 baseURL 不重输 key 走显式 `keepExistingKey` 分支(空 key ≠ 清除);`~/.dsc/` 文件布局在 04 给出。
## 8. 前端架构
- **Cordis root Context** 只承载 4 基础 ServiceTransport/Config/PlatformCapability/Clock);数据态在 **Zustand slice****Controller**(纯 TSContext 下发)消费流写 storeConnection / HostProjection / SessionChannel / Submission / Approval / Question / ProviderSettings / Scroll。
- **写路径唯一**Controller 暴露 typed intent 方法 = 唯一命令面;React 组件只经 `ReactRuntimeBridge``useSelector`/`useIntent`,不直呼 controller、不拿 Context、不碰 transport。
- **store 纪律**:写入必带 generation/epoch 参数,过期写只记 debug(五层迟到 fenceattempt / hostInstanceId / connectionGeneration / channel / operation);禁跨 await 持 Immer draftselector 禁读 wall clock(倒计时走共享 tick store)。
- **刷新恢复主路径**bootstrap 响应的 `resumedChannels` 是 cursor 事实源;`focusedSessionId` 经 StoragePort 持久恢复;provisional 按 §4.4 commandId 转正或按边界丢弃。
- **ScrollController**:命令式持 DOM ref,只把「贴底/有新内容」写 store,不逐 token setState;向上翻页前插锚定(useLayoutEffect + 稳定 item id)留分页实装时启用。
- 三端注入面(Electron 预留接缝,一期定型):`mount(el, bindings)``HostBindings = { endpoint, platform: PlatformCapability, storage: StoragePort, clock? }``PlatformCapability` 必选 `openExternal/notify/getAppVisibility/onVisibilityChange` + 可选桌面能力 + `capabilities` 探测。
## 9. Electron / TUI(架构预留,一期不实现)
设计全量保留在 `07-electron.md`(预留蓝图):四 realmmain/preload/renderer/sidecar),**Session 业务只走 localhost Client Protocol 不走 IPC**sidecar 自己 `listen(0)` 回报(拒预占端口 TOCTOU);main 预置 HttpOnly cookieAuthGate 的 cookie 实现),renderer 不持长期 token`QuitCoordinator` single-flight 状态机 + **if-idle 原子退出**fence admission → 复查权威活动 → busy 则恢复;**活动权威在 Host 侧 control channel,不信 renderer 自报**unknown ≠ 零活动);parent channel EOF → sidecar 自行 cancel-active 停机(防孤儿);updater 仅 graceful completed 后 handoff;强杀 await 真实进程树退出。TUI:换 worker front door`UiHostPort` 反转就是接入位)。
## 10. 包结构与并行填空工作包地图
### 包(五库 + apps;一设计层 ↔ 一包 ↔ 一 worktree
| 包 | 对应设计层 | 内容 | 依赖 |
|---|---|---|---|
| `packages/host/dsh-ui-protocol` | 01 | zod wire schema、branded id(分族:identity / 各资源专属 revision / cursor×2 / generation / streamId)、`applyMutation`、帧/端点/错误码 | dsh-brand |
| `packages/host/dsh-ui-host` | 03/04/08 | HarnessRuntimeAdapter + 9 窄 port + 投影 P + SessionIndex + 审批/问答 Registry + Provider 事务 + metadata store | protocol + harness 各包 |
| `packages/host/dsh-ui-server` | 02 | HTTP/SSE front door、AuthGate、ClientSlot、HostStream、SessionMux;自声明 `UiHostPort` 接口 | protocol**不依赖 ui-host** |
| `packages/client/dsh-web-runtime` | 05 | transport client、Controller、Zustand store、reducer 接线(React-free,零 react import | protocol |
| `packages/client/dsh-web-ui` | 06 | ReactRuntimeBridge 消费、组件、slot、markdown、`mount(el, bindings)` | web-runtime |
| `apps/web` / `apps/server` | — | 组装入口(apps/server 把 ui-host 实现接到 ui-server 的 UiHostPort);`apps/electron` 目录预留 | leaf |
runtime↔ui 的唯一跨包面 = `ReactRuntimeBridge`runtime 暴露 store 订阅(`subscribe/getSnapshot``useSyncExternalStore` 兼容)+ typed intentui 只消费 bridge。
开发期只保各包 `build` 过;PR 前跑 CLAUDE.md 全量门禁;前端测试策略另定(不套 per-file 100%)。
### 并行批次
- **B0(唯一串行,定稿后立即做)**:`dsh-ui-protocol` 全量冻结(含一致性测试脚手架:fold≡live 等价律、cursor 重放、乱序容忍、resync、断开语义、刷新接管 fencing 序)+ 五包脚手架/冻结 barrel/tsconfig + MessageSource 透传 spike。
- **B1(全并行 worktree,互不改同文件)**W1 ui-host 投影+portfixture 事件流驱动,离线可测)|W2 ui-host 审批/问答/metadata/SessionIndexW3 ui-host Provider 事务|W4 ui-server 传输+conformanceW5 web-runtime 连接/store/controllerfixture SSE)|W6 web-ui 对话流组件(fixture 树,完全离线)|W7 web-ui 列表+设置页|W8 markdown 栈|W9 ui 原语/主题|W10 apps 组装+e2e 脚手架。
- **B2**:垂直集成(真 harness 起流跑通对话)→ 审批/问答/steer 批次 → 打磨。
- 冲突控制:跨包只经 protocol 与 UiHostPort 两个冻结契约;包内共享 barrel 在 B0 冻结;发现新契约缺口只准报告主线,不准私改。
## 11. 插件
一期只做两件事(产品 §13 口径):
- **服务端插件列表展示**`PluginStatusSource` port 读 Node 侧插件加载状态/错误 → Host 投影 → 设置区诊断展示;不提供安装/移除/启停。
- **Browser Cordis Context 能力地基**:前端运行时以 Cordis root Context 组织(§8),为未来 UI 插件提供挂载面;slot seamtool 卡片 renderer、侧栏功能页)只留架构位与类型约定,**不实装注册机制**。第一方 renderer 与 unknown fallback 始终存在。
Future(设计已收档,直接可用):slot 注册实装(受限子上下文 + 受限 selector props);单文件 self-registering CJS factory + SRI + 白名单 external 注入 + 严格 CSP(无 unsafe-eval+ `PluginPort` 双工 typed RPCHTTP 只回 frame admission、逻辑响应走反向帧、按 generation fence)。
## 12. 实施节奏与验证
节奏 = §10 B0→B1→B2。验证四层:①协议一致性测试(transport adapter 必过同一套:无空洞基线、乱序、cursor 重放、live 丢失、单 channel reset、断开语义、刷新接管 fencing 序);②ui-host fixture 单测(port 窄 fake);③Browser e2ePlaywright:刷新恢复承诺清单逐条、双 SSE 断连矩阵);④snapshottranscript 变更走仓库既有 snapshot 机制)。首个 spike = MessageSource 透传验证(§4.4)。
## 13. Future Work 与 core 迁移 seam
| 项 | 一期状态 | core 支持后迁移点 |
|---|---|---|
| 问答/审批跨 Host 重启恢复 | 内存 pending,重启只读 interrupted | Question 落日志(照抄 approval `asked/decided` 事件对模板);`ApprovalPort/QuestionPort` 换持久实现 |
| 排队项可见/单项取消/撤回 | 不做(产品已声明边界) | harness 队列项 ID + 单项删除 → `AgentDriverPort` 扩展 |
| mutation 幂等重放合同 | 仅 prompt 有界去重 | 领域 owner 提供 operation id + 幂等合同后在 `UiHostPort` 加重放(禁无界台账) |
| adapter 原子 replace | dispose→register 同步编排 | registry 原子 replace(改动量极小)→ `AdapterRegistryPort` 换实现 |
| `after=cursor` 精确续传 | 快照 + partial 收敛 | SQLite `WHERE seq>?` 一条查询;JSONL 跳行扫 |
| tool progress | 无 | harness 新 `tool/progress` 事件 |
| ToolPresenter 共享 | ui-host 原生实现 | 上提 `dsh-tools`(豁免裁决后),ACP 与 UI 共用 |
| writeFileAtomic 共享 | ui-host 本地 helper | 提取 `dsh-util` |
| 消息分支/编辑重生成、虚拟列表、LAN/多 client、双端 PluginPort | 不做 | 各自独立 seam,设计已收档 |
## 14. 小项决议(review 时可翻)
| # | 议题 | 采用 | 备选 |
|---|---|---|---|
| T-2 | MessageSource 透传 commandId | 采用,首 spike 验证;不通回退"user message 到达即替换 provisional"wire 不变) | 直接用回退方案 |
| T-3 | ToolPresenter | ui-host 原生实现(消费 dsh-tools 原语) | clone 报警时上提 dsh-tools(需豁免) |
| T-5 | 设置页表单 | 手写 | schema→form |
---
## 附 A:关键决策演变(一次看全过程,正文不再重复)
> 本设计经历:本仓两轮(8 层类图 + 接缝对齐 + 合并修正)+ 与平行仓 harness-1(独立设计了同一系统三版)的平权综合 + harness core 源码逐条核实。每行 = 一个决策的走向;完整证据在 `missions/tasks/20260718-final-review/`。
| 决策 | 早期方案 | 最终结论 | 变化原因 |
|---|---|---|---|
| 一期范围 | Electron + Web 同期 | 只 WebElectron 架构预留 | 用户收窄范围;同构约束保留使后补零改业务 |
| 命令回执 | CommandEnvelope + AdmissionReceipt + Host-lifetime 台账 | 全删;普通 HTTP status;仅 prompt 有界去重 | 两仓独立评审同向:台账换来自身未决的 eviction 复杂度,一期只有 prompt 真需防重 |
| 乐观 UI 缝合 | clientToken echo 进 receipt | MessageSource 透传 commandIdspike 验证) | receipt 已删;透传方案同时解决 command↔turn 显式关联,零改 core |
| durable/live 判别 | 帧上显式 `durable: boolean` | kind 闭合分类 + 01 导出分类器 | boolean 可与 kind 冲突;分类器单点定义防各层漂移 |
| 刷新恢复 partial | 不恢复进行中消息 | 快照/replay.completed 含 partial | 纯 server 内存可实现,直接抬高下限体感 |
| replay 窗口 | 无显式窗口(隐式约定) | replay.started/completed 帧 | 重放与 live 交错需要显式无空洞保证 |
| 错误模型 | 单层 9 个错误码 | 五 category 两级 + HTTP status 分层 | category 决定恢复动作(重连/重 bootstrap/呈现/重同步),比平铺码更严谨 |
| 鉴权 | 随机口令 Basic → 一次性链接换 cookie | 裸 localhost 免登录 + AuthGate 接缝 | 用户拍板体验优先;cookie 设计收档,接缝保留 |
| 审批/问答 Registry 位置 | per-channel 投影内 | Host 级单例 + per-channel 纯读 | harness 注册只许一次;审批可在无 channel 时到达 |
| Registry 排序锚 | 按到达顺序 append | anchorSeq 显式锚 | 显式 id 强关联原则 |
| session 列表 | 全量返回 + 本地筛选 | server keyset 分页;Host 投影不嵌全量列表 | 量大时性能/一致性;两处数据源矛盾消除 |
| ToolPresenter | 上提 dsh-tools 共享 | ui-host 原生实现,上提留 future | 源码核实:并未上提过;一期不动 core 组包 |
| adapter 替换 | 先 register 新再 dispose 旧 | 同步 dispose 旧 → register 新 | 源码核实:重名注册抛 DUPLICATE_ADAPTER |
| 后台任务活动 | `ctx.tasks.list()` 直读 | 逐 agent 聚合 / onTaskDone 计数 | 源码核实:list 按 caller session 过滤 |
| 事件订阅 | 未指定位置 | 必须 root context | 源码核实:scope-filtered dispatch |
| 包结构 | 三组粗切(host/client/apps | 五库包 + UiHostPort 反转 + bridge 唯一跨包面 | 一层一包一 worktree,最大化并行填空;TUI 接入位 |
| SWR | 列表/设置用 SWR | 不用,统一 Controller+store | 仅剩两个用例不值引入第二数据通路 |
| Submission 队列 | harness-1 曾设计 SubmissionQueue/durable facts 全家) | 不建第二队列,AgentDriverPort 薄映射 | 两仓独立终裁同向:不复制 core 状态;差距钉成迁移边界 |
## 附 B:术语
| 术语 | 含义 |
|---|---|
| 投影 P | harness 原始事件 → SessionMux 帧 / 物化树(含 partial)的 ui-host 逻辑 |
| `applyMutation` | 可 fold 帧 → UI 树 reducerserver fold ≡ client 贴 live |
| ClientSlot / connectionGeneration / streamId | 单客户端槽位 / 刷新换代 fence / 单次物理流 fence(三层各司其职,均不承担业务幂等) |
| replay 窗口 | `replay.started…completed` 之间的 durable 重放段,completed 附 partial 快照 |
| UiApproval / UiQuestion | 两类独立交互资源 wire DTO:稳定 id + revision(1→2) + 单 CAS + anchorSeq |
| MessageSource commandId | send 时随 merge-extensible source 落日志的命令关联 idprovisional 转正依据 |
| HarnessRuntimeAdapter / UiHostPort | 唯一读 ctx 的组合期装配 / ui-server 消费 ui-host 的反转接口(TUI 接入位) |
| AuthGate | 传输层鉴权接缝;一期 allow-allElectron/LAN 换实现 |
| ProviderCommitSlot / AdapterGeneration | 保存事务互斥闸 / 不可变 adapter 代(dispose 旧→register 新,lease 到自然结束) |
| steer / queue / stop | agent 两条 FIFO 的 1:1 映射 + 整体取消 |
| partial 恢复 | 快照/replay.completed 携带进行中消息已生成部分 |