Files
deepseek-harness/missions/tasks/20260720-0300-web-dev-2-onboarding/audit.md
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

91 lines
28 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.
# GUI 代码审计(web-dev-2,2026-07-20)
> 五维度:①严谨 ②面向对象 ③可扩展可维护 ④资源生命周期 ⑤一致性。只审不改;file:line 均已盘上核实(comment-sweep 在途,行号以本次审计时点为准)。与设计文档冲突的标 `doc-mismatch`。severity:must-fix > should-fix > nice。
## 批 1:packages/host/apiproxy(api/ + fetch/)
| # | 问题 | 所在 | 为什么是问题 | 建议改法 | severity |
|---|---|---|---|---|---|
| A1 | **`stream/error` 帧全仓零生产者,但载体注释声称靠它收敛** | 契约声明 api/events.ts:34,42;handler.ts:69-70 的 catch 注释「Mid-stream failures converge via the stream/error frame」;消费侧 connection.ts:99、session.ts:214 均防御它 | grep 全仓(runtime impl、fixture)无任何 `type:'stream/error'` 构造点:impl 流中途 throw 时 sseResponse 的空 catch 静默吞掉并正常 close 流——client 只见「流正常结束」,触发重连循环但**永远不知道 host 侧出错**。注释描述了一个不存在的机制,属「空 catch 未如实说明吞掉什么」 | sseResponse 的 catch 里真发一帧 `stream/error`(错误折成 RpcError internal)再 close;或改注释如实声明「v1 静默断流=重连语义」并在 impl TODO 登记 | must-fix |
| A2 | **S→C 方向 zod 校验缺位:帧与 Value schema 全部零消费者**(doc-mismatch:契约 v2.0 §0-5「zod 双向校验,C→S 命令、S→C 事件都 parse」) | client.ts:159 `JSON.parse(data) as ServerRequest`(帧无任何 schema parse);client.ts:128 `full.result as ...`(result.value 无 Value schema parse);muxFrameSchema/hostFrameSchema(events.schema.ts:22,32)与 6 个 `*ValueSchema` 在 src 内零 import | 契约总则第 5 条只兑现了一半:C→S 两级 parse 齐全,S→C 只 parse 了 ServerResponse 信封层。坏帧(host bug/版本漂移)会以任意形状直插 fold/store,错误暴露点远离源头;同时 6+2 个 schema 成了「写了没人用」的死代码,维护者会误以为有校验 | client readSse 对 payload 过 muxFrame/hostFrame schema(可 dev-only 开关,契约 §0-5 已预留「开关是实现细节」);callUnary 按 method 分派 ValueSchema parse result.value;或明确拍板砍掉 S→C 校验并删 8 个死 schema + 改契约文 | must-fix |
| A3 | **UNARY_ROUTES 类型面失锁:`Record<string, UnaryRoute>` 擦掉了 key 与 schema/invoke 的关联** | handler.ts:25-37(interface UnaryRoute + Record<string,...>),:139 `payload.data as never` | ①漏行不报错:RpcMethodMap 加了 key 而 UNARY_ROUTES 忘加,编译期静默、运行时 404(违反「两处必须同步改要有编译期锁」);②schema 与 invoke 的 payload 类型互不约束——放错 schema(如 prompt 行贴 cancel 的 schema)照样编译过,`as never` 把最后一道检查也关了 | 改成 mapped type:`const UNARY_ROUTES: { [K in keyof RpcMethodMap]: { schema: z.ZodType<RequestPayload<K>>; invoke(api, r: RpcRequest<RequestPayload<K>>): ... } }`——key 覆盖性与 per-key payload 类型双锁,`as never` 可删 | should-fix |
| A4 | **`RpcId('')` 违反本包自己的 min(1) 校验** | handler.ts:128(信封 parse 失败时回 `errorResponse(RpcId(''), ...)`);rpc.schema.ts:26 `rpcIdSchema = z.string().min(1)` | 这条 wire 上的 ServerResponse 过不了本包 serverResponseSchema——本客户端 callUnary:125 会 parse throw,把「服务端明明白白告诉你 bad-request」变成 client 侧 ZodError 异常(错误通道降级);任何按契约实现的第三方 client 同样拒收。自家 wire 形自相矛盾 | 定一个哨兵形并让 schema 接受(如 rpcId 允许空串仅限 error response——不佳),更干净的是:信封 parse 失败时若 body 里能捞到 string rpcId 就回填原值,捞不到用固定哨兵 `RpcId('invalid-request')` 且 schema 不设 min(1)(min(1) 挡不住真攻击,只造成自伤) | should-fix |
| A5 | **askUserQuestionItemSchema 手抄 core 形状且无 satisfies 锚定**(一致性:全包唯一没锚的业务 schema) | events.schema.ts:14-20 | 同文件其他 schema 或 `satisfies z.ZodType<Wire<T>>` 或显式 cast+注释;此处是 plain z.object,core `AskUserQuestionItem` 加字段时静默漂移(编译不报),恰恰是锚定纪律要防的 | 补 `satisfies z.ZodType<Wire<AskUserQuestionItem>>`(type-only import 已有先例);对不上就说明手抄已漂移 | should-fix |
| A6 | **流方法的 request payload 上不了 wire:`since` 签名承诺无载体通道**(doc-mismatch,笔记 §7-4 并入) | 契约 events.ts:20(`mux(request: RpcRequest<{since?}>, …)`);client.ts:132,137(`_payload` 直接丢弃);handler.ts:98,101(GET 无 body,服务端自 mint rpcId+空 payload 调 impl) | 「签名留座 v1 不实现」是拍板,但载体层连**承载方式都不存在**:将来实现续传时不是填 impl 就完事,要先动载体协议(query 编码或升 POST)。此外 client 侧 mux 调用在 wire 上没有 client-request 象限痕迹——四象限模型在流打开这一步是残缺的,RPC 面板台账里看不到「谁发起了流」 | 在契约 design §6 不做清单里补一行「since 载体通道未定(query vs POST)」;或现在就定 query 编码(`?since=...`)让签名与载体对齐,实现仍可后置 | should-fix |
| A7 | 双 POST 路径重复:callUnary 与 respond 各写一遍「POST+headers+timeout+!ok throw」 | client.ts:118-124 与 190-196 | 同构代码两份,改超时/加 header(将来鉴权)要记得改两处——无编译期锁的隐式耦合 | 抽 `protected postJson(path, body): Promise<Response>`(协议不变量仍在基类,子类切面不受影响) | nice |
| A8 | `serverResponseSchema.parse(...) as ServerResponse` 的 as 冗余 | client.ts:125 | schema 已是 `z.ZodType<ServerResponse>`,parse 返回类型就是 ServerResponse;多余 cast 弱化「本仓 as 必须有理由」的信号密度 | 删 as(typecheck 可证) | nice |
| A9 | SSE 解析对坏帧零容忍:单帧 JSON.parse throw 杀整条流 | client.ts:159(在 for-await 体内,throw 逃逸出 generator) | 一帧损坏(代理截断/编码问题)→ 整条流断 → 全量重连重拉(成本被放大 N 倍)。与 fold 侧「未知事件 documented-default 跳过」的宽容哲学不同构 | try/catch 单帧:坏帧 console.error+跳过(配 A2 的 schema parse 一起做) | nice |
| A10 | unary 调用无外部取消通道:AbortSignal 只有内部 timeout | client.ts:122,194(`AbortSignal.timeout(...)` 独占 signal 位);IApiClient 各 unary 签名无 signal 参数 | 组件卸载/切换 session 后在途 history/prompt 无法取消,只能等 30s 超时或响应到达(Session 层靠状态机挡住了 UI 影响,但请求本身白跑)。契约 §0 有「AbortSignal 不进 input、独立第二参」的先例,流方法有 unary 没有——不对称 | IApiClient unary 加可选第二参 `signal?: AbortSignal`,与 timeout 用 `AbortSignal.any` 合并;非急(v1 请求都幂等且轻) | nice |
**批 1 小结**:契约类型体系(四象限/brand/错误 map/派生泛型)质量高,问题集中在**载体层兑现度**——S→C 校验缺位(A2)与 stream/error 空头支票(A1)是同一主题:「wire 进来的东西被无条件信任,出错路径没人真走过」。fixture 全绿掩盖了这类问题(假载体不产坏帧)。
## 批 2:packages/host/runtime + webserver + apps/dsc
| # | 问题 | 所在 | 为什么是问题 | 建议改法 | severity |
|---|---|---|---|---|---|
| R1 | **webserver 请求回调是无兜底 async——一个畸形 URL 就能打崩整个 dsc web 进程** | webserver/src/index.ts:45(`createServer(async (req,res)=>{...}` 无 try);三条可达 throw 路径::56 `decodeURIComponent(rawPath)`(`curl 'http://host/%'` → URIError)、:87 `for await (const chunk of req)`(client 半途断 body → 迭代 throw)、static.ts:44-46(catch 分支里 `readFile(distIndex)`——dist 运行期被删则二次 throw) | async 回调 reject = unhandledRejection,Node ≥15 默认**进程退出**。任何外部请求可 5 字节触发(无需恶意构造);SSE 全断、host 陪葬。step1 验收测过「403 编码变体」但都是合法编码,畸形序列没人走过 | createServer 回调包顶层 try/catch:catch 里 `res.writeHead(400).end()`(headersSent 则 destroy socket)+ console.error。一处兜底覆盖三条路径 | must-fix |
| R2 | **SSE 链路两跳皆无背压、无上限缓冲** | ① FrameQueue.buffer 无界(runtime/src/api-proxy.ts:59-90,push 只进不看长度)② sseResponse enqueue 不看 `controller.desiredSize`(apiproxy fetch/handler.ts:57-77)③ bridge `res.write(chunk)` 忽略返回 false 不等 drain(webserver/src/index.ts:100) | 慢客户端/挂起的 tab 收不动时,事件继续全速生产:内存增长 ∝ 事件速率 × 挂起时长(chunk 风暴下很快)。三层各自「转发就完事」,没有任何一层拥有「消费者跟不上」的决策 | GUI 期单机可先记台账;正式修法:FrameQueue 设上限(超限断流→client 走「重连+重拉」既有恢复路径,语义已免费);bridge 尊重 write 返回值 await drain | should-fix |
| R3 | **agentFor 把一切 resume 失败抹成 undefined → 误报 session-not-found** | runtime/src/api-proxy.ts:128,138(`resume.catch(() => undefined)`)→ :167,:176 统一回 `session-not-found` | 持久化文件损坏、boot 配置错、并发 dispose——全部伪装成「session 不存在」,诊断被引向完全错误的方向。空 catch 家规要求「名其所吞」,这里吞了整个错误族且换了罪名 | agentFor 返回 `Agent \| RpcError`(或 throw 分型):真 not-found(store 无此 id)与 resume-failed(internal + reason 透传)分开回 | should-fix |
| R4 | **impl stub 现状台账**(非漂移,TODO 已标注;列入供 TOP 排期权衡) | runtime/src/api-proxy.ts:256-259 respond 恒 not-pending、审批/问答帧零发射(pending registry 未建);:144 list 不 merge 持久化目录(冷 session 列表不可见——**真 host 首屏空列表的直接原因**);:203 describe.version 占位 '0.0.1' | web 侧 PendingCard/pendingBuffers/subscribed 重放消费端全部就位空等;列表缺冷 session 让「打开历史会话」这个主场景在真 host 上走不通(只能靠新建) | 按 TODO(step2) 排期:冷 session merge(readdir+stat)优先级最高(解锁主场景),pending registry 次之 | should-fix |
| R5 | dsc web 打印 URL 与实际监听面不符 | webserver listen `0.0.0.0`(index.ts:67);web.ts:67 打印 `http://127.0.0.1:${port}` | 本项目明确场景是远程容器+局域网浏览器访问(0.0.0.0 就是为此拍板的),打印 127.0.0.1 误导使用者复制即用 | 打印行补一句实际绑定面(或列出首个非环回地址);纯壳层文案改动 | nice |
| R6 | err() 帮助函数条件类型是无效噪音 | runtime/src/api-proxy.ts:54(`RpcResult<T> & {ok:false} extends never ? never : Extract<...>` ——前半永假,等价于直接 `Extract<RpcResult<T>,{ok:false}>['error']`) | 读者要花两遍才确认它不做任何事;违背「每个 as/复杂类型要有理由」的信号密度 | 简化为 `error: RpcError` 或 Extract 直写 | nice |
| R7 | createApiProxy 是 260 行闭包工厂,状态(resumes 表、将来的 pending 表)散在闭包变量 | runtime/src/api-proxy.ts:120-261 | 现在可接受;但 pending registry(respond 实装)落地时闭包会再膨胀一截,私有状态无 class 字段可见性管束。仓库同层对象(Session/SessionManager/AbstractApiClient)均已 class 化——OO 一致性 | respond 实装时顺势升 class(HostApiProxy implements ApiProxy),resumes/pending 成 private 字段;本轮不动 | nice |
| R8 | FrameQueue 将是 pending registry 的公共依赖但目前 module-private | runtime/src/api-proxy.ts:59-90 | respond 实装要在 waterfall answerer 里等 client-response,大概率复用同款队列/信号原语;到时复制一份就是克隆债 | 实装 pending registry 时抽到独立文件导出;本轮记录即可 | nice |
**批 2 小结**:拆包结构(apiproxy 前置/runtime 装配/webserver 承载/dsc 拼装)边界干净、依赖方向纪律执行到位;风险集中在**进程级健壮性**(R1 一击致崩)与**错误通道保真度**(R3 与批 1 A1 同主题:错误在传播链上被静默降级)。R4 的冷 session merge 是功能面最痛的一条。
## 批 3:packages/client/web-runtime — session/ 对象层
| # | 问题 | 所在 | 为什么是问题 | 建议改法 | severity |
|---|---|---|---|---|---|
| S1 | **liveBuffer 缝合是死循环:open 期间到达的 live 事件被永久卡在缓冲里**(doc-mismatch:§D.3-3「历史就绪后按 seq 合并」实际未发生) | session.ts:282-291(installWindow 经 acceptLiveEvent 回放 buffered)× :294-297(acceptLiveEvent 首行 `openState==='loading'` 时**又推回 liveBuffer**);而 doOpen 里 installWindow(:264,:269)先于 `openState='open'`(:271)执行 | 回放循环把每条 buffered 事件原样塞回新 liveBuffer,之后无任何代码再排空它(唯一另一处 drain 是 resync 直接丢弃 :175)→ **open 窗口期的 live 事件丢失到下次重连**;且后续 live append 因中间缺段产生 seq 洞 → SurfaceManager 连续性断言 throw → fold 静默降级(degraded 线性扫描 + padded[seq] 错位跳节点)。fixture 的 history 同步返回、窗口≈0,验收测不到;真 host 慢 history + 正在流式的 session 必现 | installWindow 在 stitch 前先置 `openState='open'`(或绕开 acceptLiveEvent 用带 seq 过滤的直接 append 路径);补一个「open 期间来帧」的 fixture 时序用例 | must-fix |
| S2 | **cancel/abort 后 partial 与 runningCalls 变僵尸:无 turn/end 清理** | session.ts:310-337(applyEventSideEffects 只在 `assistant/message` 清 partial、`tool/result` 清 openCalls,**无 turn/end case**——grep 全文件无 turn/end);core 已核实:abort 路径不补发 assistant/message(agent-loop/loop.ts:630-636 流循环 signal.aborted 即 throw;:420-435 error 分支 closeStep 后直接 break,recordAssistantMessage 不执行),但 **turn/end{aborted} 一定会发**(loop.ts:221) | 真 host 上每按一次「停止」:脉冲 partial 永驻、执行中工具卡永转,直到下次重连才被 resync 冲掉。fixture 掩蔽(fixture 的 finish(aborted) 会补「(已中断)」assistant/message 清掉 partial);verify-session-real 没测 stop——**两级验收都测不到** | applyEventSideEffects 加 `turn/end` case:清同 turn 的 partial 与 openCalls(清理钩子 core 已白送);verify-session-real 补 stop 用例 | must-fix |
| S3 | **live append 不校验 seq 连续性,洞靠 fold throw 兜底而非主动补拉** | session.ts:298-307(acceptLiveEvent 只做 `seq<=tail` 丢重叠,`seq>tail+1` 的洞照 push);重连后 resync 前的窗口(connection 泵开流即下发帧,manager.handleConnected 的 resync 在 describe 成功后才跑)必然产生洞 | 洞进 padded 数组后 SurfaceManager throw→degraded=true,直到下次 reset 才恢复——用「异常+降级视图」处理一个**可预期**的时序,而非用既有的缝检测语义(subscribedLastSeq 已在手 :200)主动修复 | acceptLiveEvent 检 `seq !== tail+1` 时不 push:进 liveBuffer 并触发一次 resync-lite(重拉尾页缝合)——与 S1 修法同一套路径 | should-fix |
| S4 | **resync 复用在途 openPromise:重连后可能定格在断连前的失败结果** | session.ts:167-178(resync 置 cold 后调 open())× :120-127(open() 见 `openPromise !== null` 直接返回**旧 promise**)——断连时在途的 doOpen 其 history 请求已死,将以 transport error 收场并把 openState 写成 'error';resync 等到的就是这个旧结果,不再重拉 | 重连本该「重建」,却可能以 error 态收场且无自动重试(UI 无 retry 按钮,只能重新点选 session)。触发窗口=初次 open 与断线重叠,越慢的网越容易 | resync 引入 generation/取消语义:作废在途 openPromise(记 generation,doOpen 收尾时 generation 不符则丢弃写入),或 resync 处 `openPromise=null` 强起新一轮 | should-fix |
| S5 | **快照子结构引用稳定性承诺整体未兑现,memo 全灭**(doc-mismatch:§A.9.4「pending/runningCalls 未变沿用旧引用」、§C.2「未变条目引用稳定」) | session.ts:354-372(buildSnapshot 每次 `[...openCalls.values()]`、`[...pending.values()]` 新数组新对象);manager.ts:191-193 + lineage.ts:44(每次 rebuild 全量 `{...s, depth}` 新条目);连带 ConversationView 内联 `call={{...}}`/`result={{...}}` 新对象 | chunk 风暴下每微任务批一次 rebuild:所有 SessionListItem、所有 ToolCallCard、PendingCard 的 React.memo 因 props 引用恒变而 100% miss——设计立了 memo 边界(§C.2/§C.4 明文),实现把前提拆了。列表小时无感,长会话+流式期是主要重渲成本 | 按设计的 revision 计数器方案:各子结构维护版本号,rebuild 时版本未变即复用旧数组/旧条目引用;View 层内联对象改由快照直供稳定引用 | should-fix |
| S6 | padding 哨兵盗用真实事件类型 `todo/write`(doc-mismatch:设计写 `noop/padding`,笔记 §7-6) | fold-adapter.ts:22-25(`{type:'todo/write', data:{todos:[]}}` as SessionEvent) | 与设计字面不符事小;用**真类型+伪数据**事大:将来任何人给 fold/调试面加 todo/write 处理,万级哨兵就会现形为垃圾节点。反正非 surface-eligible 的任意字符串都被同样跳过,没有理由选真类型 | 换 `'noop/padding'`(效果等价、语义自明),cast 保留一处并注释 | nice |
| S7 | pendingBuffers 无清理路径:session-removed 不删、未实例化 session 的缓冲无上限 | manager.ts:24,130-141(只增);:163-167(removed 分支不触 pendingBuffers) | 长跑页面的慢泄漏;且 removed session 的僵尸审批帧会在其将来意外实例化时回放出无意义卡片 | session-removed 时 `pendingBuffers.delete(id)`;缓冲加上限(每 session 几十条足够,审批帧低频) | nice |
| S8 | F.6 预埋要求未落:Session 无 dispose() no-op 预留 | session.ts 全文(grep dispose 无果);design §F.6 预埋列明「新增 dispose() 预留为 no-op 方法」 | 台账逐条预埋要求里唯一没兑现的一条(其余 F.2/F.7/F.10/F.11 抽查均落实);将来上逐出策略时无收口点 | 补 no-op `dispose()` + 注释指 F.6 | nice |
**批 3 小结**:对象层骨架(Notifier 合批/懒 build、FoldAdapter 降级收口、draft 在途锁+乐观清空)质量高于平均;但 **S1/S2 是两条实锤运行期 bug**,共性是「fixture 时序太理想 + 真 host 验收没覆盖 stop/慢 history」——修复时应连带补验收用例,否则会复发。S3/S4/S5 都是「设计承诺了、实现只落一半」:缝检测、重连重建、引用稳定,建议作为一个「对齐 §D.3/§A.9 设计语义」的批次一起修。
## 批 4:web-runtime 其余(connection/boot/store/rpc-log/intents/fixture)+ web-ui hooks/容器
| # | 问题 | 所在 | 为什么是问题 | 建议改法 | severity |
|---|---|---|---|---|---|
| C1 | **连接状态对 UI 不可见:断线/重连中用户零感知** | connection.ts 全文(generation/attempt 实例私有,无任何对外读口);store.ts(无 connection 切片);conversation.ts ConversationSnapshot(无连接位);唯一出口是 console.warn(connection.ts:86) | 断线期间界面完全正常——列表还在、输入框可打字,prompt 发出后要么 30s 超时要么 transport error,用户以为是「卡了」。「连接状态」正是 zustand 应承载的跨视图全局展示态(store 红线管的是业务对象,不是它);契约 design §5 图里也画了「连接状态」进 store | ConnectionController 加 `onStateChange?(state: 'connected'\|'reconnecting')` sink,boot 接入 store 新 `connection` 切片,UI 顶部细条即可。severity 按用户体验定 should | should-fix |
| C2 | **describe 与流打开之间无就绪握手,onConnected 可能早于 subscribed 帧** | connection.ts:74-77(describe 是独立 unary,与两条 SSE 并发;成功即 onConnected)→ manager.ts:186-189(立刻 refreshList+resync)→ session resync 的 doOpen 里 subscribedLastSeq 大概率仍是 null(session.ts:265 缝检测直接跳过) | describe 只证明「unary 通」,不证明流已建立(GET SSE 握手更慢是常态);resync 抢跑 → 缝检测这一层保护在**每次重连后的关键窗口**恰好失效,回到纯 liveBuffer 去重路径(S1 修好后此路径才成立,但基线语义仍缺)。设计 §A.1「两条流开启且 describe 成功之后」的"两条流开启"没有实现对应物 | pumpStream 收到首帧/首字节时 resolve 一个 opened promise,`await Promise.all([mux开启, host开启, describe])` 再 callSink(onConnected);SSE 开流即有 `: connected` 注释行(handler 已发),client 侧可感知 | should-fix |
| C3 | **useConversation/useSessionList 的操作句柄引用每快照重建,破坏纯 props 组件的 memo 潜力** | useConversation.ts:26(外层 `useMemo(..., [snapshot, ops])`——handle 对象每 snapshot 换新,虽然 ops 稳定,但 InputBar 等收到的 onSend/onStop 经容器再包一层后引用仍随渲染变);SessionListContainer.tsx:13-16(`useCallback(..., [h.create, onSelect])` 依赖 h.create——恰好稳定,但 onCreate 又内联 `() => void create()` 每渲染新引用) | 与 S5 同主题的 UI 侧一半:设计承诺「操作句柄引用稳定」(§B.1),实现里句柄本体稳定但**沿途每层都再包新箭头**,到叶子组件时引用已不稳。memo 生效前提被层层削弱 | 容器直传稳定引用(onCreate={create} 不再包箭头);handle 拆成 `{snapshot}` 与恒定 `ops` 两个返回位或直接返回稳定 ops 对象 | nice |
| C4 | **rpc-log 的 inflightMethods 表只删成功对,孤儿条目永不清** | rpc-log.ts:19,32-36(client-request set;仅 server-response get+delete) | 超时/传输失败的请求没有 server-response(transport error 是 throw 不是消息)→ 表条目永驻。量级小(每失败请求一条 string 对),但它是模块级 Map,与「批量泵收进实例」纠偏走反方向——AbstractApiClient 实例可多个(测试/多 boot),模块表跨实例串味 | 表挂进 ingest 闭包或加容量上限(LRU 几百条);顺手把 nextId 一起收进去(display-local 的辩解成立,但同一动作可一起收) | nice |
| C5 | **fixture 的 host 流泵每轮循环叠加一个 abort listener** | fixture.ts:288-291(while 循环体内每次 `new Promise` 都 `signal.addEventListener('abort', ..., {once:true})`——once 只保证 fire 一次,不 fire 就常驻;每个帧批一个新 listener)同型 :259-261 | 长开页面的 fixture 会话数小时后 listener 数千计(MaxListenersExceeded 警告级,非泄漏实害);真实包 FrameQueue(api-proxy.ts:77)就做对了:一次 addEventListener+finally remove——同类问题两包处理不同构(维度⑤) | 照 FrameQueue 模式:循环外挂一次 abort listener,wake 机制复用 | nice |
| C6 | **React 侧无错误边界:单组件 render throw 白屏整个应用** | web-ui/src 与 apps/web/src 全 grep 无 ErrorBoundary/onCaughtError;App.tsx 直渲两大区块 | 对话流渲染的是**透传的任意事件数据**(unknown 节点/loose ContentBlock),一条意外形状(如 text 块 text 非 string)在叶子组件 throw 即整页白屏——与 runtime 层「sink 隔离/降级视图」的防御纵深不匹配,最后一层没兜 | SessionsScreen 与 RpcLog 各包一层 ErrorBoundary(React 18 class 版或 react-error-boundary),fallback 显示错误细条 | should-fix |
| C7 | intents 的 `api === null` 静默 return 与 getSessionManager 的 fail-loud 不同构 | intents.ts:38(pingHost 里 `if (api === null) return`)vs manager.ts:207-209(未 init throw) | 同一「boot 前被调」错误,两个出口一个吞一个炸;吞的那个在 bindIntents 漏接线时永远查不到(按钮就是没反应) | pingHost 也走 fail-loud(或两者统一经一个 `requireApi()`);一致性小修 | nice |
| C8 | boot 可重入但产物互相踩:二次 bootWebRuntime 覆盖单例却不 stop 旧 controller | boot.ts:18-30(每次 new 全套;initSessionManager 覆盖单例、旧 ConnectionController 无人 stop,旧泵继续跑且 sink 还指向旧 manager) | 现产品单次 boot 无害;但 HMR/测试重复 boot 时旧泵+新泵并行双倍请求,且旧 manager 还在吃帧——排查成本高的隐性坑 | boot 记模块级 prev handle,重入先 prev.stop()(或文档明示「仅可调一次」+ dev 断言) | nice |
**批 4 小结**:连接层的**代际管理**(generation+同代收敛 abort+退避)本身写得干净;缺的是两端的可观测性——对上(C1 用户看不见)与对下(C2 流就绪不可感)。web-ui 的 hooks/容器分界执行得好(组件零 store/manager import 抽查属实),遗留问题集中在引用稳定性(C3,与 S5 合修)与最后一层防御(C6)。
---
## 改进点 TOP 清单(severity × 影响面排序,供裁决)
| 排名 | 条目 | 一句话 | 修复归组建议 |
|---|---|---|---|
| 1 | **R1** webserver async 回调无兜底 | 一个 `%` 畸形 URL 即 unhandledRejection 崩掉整个 dsc web 进程 | 独立小修(~10 行),立即 |
| 2 | **S1** liveBuffer 缝合死循环 | open 期间 live 事件永久滞留缓冲:丢事件+seq 洞+fold 降级,设计 §D.3 缝合从未真正生效 | 与 S3/S4 组成「打开/重连时序」批 |
| 3 | **S2** 无 turn/end 清理 | 每按一次停止,partial 脉冲与执行中工具卡永驻到下次重连(core 已核实 abort 不补 assistant/message) | 独立小修(一个 case),立即;连带 verify-session-real 补 stop 用例 |
| 4 | **A1** stream/error 零生产者 | host 侧流中错误被空 catch 吞成「正常断流」,注释声称的收敛机制不存在 | 与 A2 组成「载体错误通道」批 |
| 5 | **A2** S→C zod 校验缺位 | 契约「双向校验」只兑现一半,帧/Value schema 共 8 个是死代码;坏帧直插 fold | 同上批;或拍板砍单向+删死码 |
| 6 | **R4** 冷 session 不进 list | 真 host 首屏空列表,「打开历史会话」主场景走不通(TODO 已标注,纯排期问题) | impl step2 批之首 |
| 7 | **C2** onConnected 不等流就绪 | 重连后 resync 抢跑,subscribed 缝检测在最需要它的窗口失效 | 「打开/重连时序」批(与 S1/S3/S4 同修同验) |
| 8 | **S5+C3** 引用稳定性承诺未兑现 | 快照子结构+操作句柄层层新引用,全部 React.memo 形同虚设;流式期重渲成本 | 独立「性能对齐设计」批,可后置到样式/组件重做轮一起 |
| 9 | **C6** 无 React 错误边界 | 透传任意数据的渲染树没有最后一层,单点 throw 白屏 | 独立小修;组件重做轮也可 |
| 10 | **C1** 连接状态不可见 | 断线用户零感知;connection 切片本就是 store 的正当职责 | 与 C2 同批或独立小功能 |
| 11 | **A3** UNARY_ROUTES 失锁 | map 加方法忘加路由静默 404;mapped type 可双锁 | 独立小修(类型改写零行为变化) |
| 12 | **R3** resume 失败伪装 not-found | 诊断误导型错误降级;配合 A4(RpcId('') 自相矛盾)一起理错误通道 | 「载体错误通道」批 |
| 13 | R2 SSE 无背压 | 慢消费者内存增长;GUI 期可缓,正式化前必须 | 转正前批 |
| 14 | A5/A6/S6/S7/S8/C4/C5/C7/C8/A7-A10/R5-R8 | 一致性与预埋小项 | 顺手修/组件重做轮 |
| — | doc-mismatch 汇总 | A2(双向校验)、A6(since 载体)、S1(§D.3 缝合)、S5(§A.9 引用稳定)、S6(哨兵类型)+ 笔记 §7 的 8 条 | 转 RFC/文档线(rfc-consolidation) |
**总评**:架构分层(契约/载体/装配/对象层/hook/组件六层)与依赖纪律执行得好,OO 归属经两次纠偏后基本正位(批 4 抽查组件零越界属实);系统性弱点集中在两条线——**错误与异常路径的兑现度**(A1/A2/R1/R3/S2:正常路径精心设计,出错路径要么吞要么没人走过)和**时序竞态**(S1/S3/S4/C2:fixture 同步理想时序掩盖了慢网/重连窗口)。建议修复顺序:R1+S2 两个立即小修 → 「打开/重连时序」批(S1/S3/S4/C2)→「载体错误通道」批(A1/A2/A4/R3)→ impl step2(R4 优先)→ 性能与杂项。每批修完跑 verify-session + verify-session-real 并按发现补用例。