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
28 KiB
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 并按发现补用例。