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
37 KiB
apiproxy 统一 API 层 · 设计(v2.0:四象限 RPC 消息模型)
2026-07-19 主会话起草;19:48 Q1–Q8 修订;20:15 RPC 命名体系(RpcResponse);20:41 形态 B 终选;20:56 RpcError 强类型化;21:04 rpcId+信封两层;21:13 整轮裁决(L1/L2/L5+审批问答+§8 预留);21:19 问答 id 复用 RpcId+帧信封对称;22:00-22:3x 四象限统一消息模型定型(v2.0):通道与消息解耦、四具名判别 union、签名显式收窄形 RpcRequest
、respond 重建模为 client-response、泛型工具改名 RequestPayload/ResponseValue。拍板全记录见同目录 README.md。 定位:
packages/host/apiproxy对 Web / Electron / TUI 暴露的唯一契约层;web client 的 HTTP/SSE 只是它的一种承载。
0. 总原则(已拍板)
- TS interface 是权威契约,HTTP/SSE 是载体。同进程形态(Electron main、测试)直接注入 handler 当 fetch,跨进程走真 HTTP——签名完全一致(opencode 同构点,已经调研证实其 fetch 面连 Worker RPC 边界都能过)。
- 透传 core 数据结构:wire 上的事件/消息/内容块就是
SessionEvent/ContentBlock等 core 类型,不自造第二套 DTO。类型经import type依赖链直达浏览器。 - RPC 风格,按业务域分组,一域一对文件(
sessions.ts类型 +sessions.schema.tszod)。 - 错误 = 类型化 RpcResponse 信封(
RpcResponse<T>+RpcError),方法不 throw 业务错误。 - zod 双向校验(C→S 命令、S→C 事件都 parse),schema 用
satisfies z.ZodType<T>锚定编译期防漂移——形态 B 下T是 infer 派生类型(satisfies z.ZodType<RequestPayload<'session.list'>>),锚定等价可行,代价(报错信息展开为字面量结构)已被用户接受(2026-07-19 20:41);不 passthrough。可 dev-only 开启(开关是实现细节不进签名)。实现修正(21:55,W1 落地发现):仓库exactOptionalPropertyTypes与 zod.optional()输出类型(T | undefined)不兼容,锚定统一写satisfies z.ZodType<Wire<T>>——Wire<T>是深度「| undefined」宽化(api/rpc.schema.ts 定义并注释),JSON wire 上缺席与 undefined 同形故不损失校验语义;透传宽分支(SessionEvent/ContentBlock/帧 union/RpcError discriminatedUnion)与 brand id schema 用显式 cast + 注释。 - 历史 = 事件重放:一套 fold(client 侧),历史分页拉 + live 增量贴同一条代码路径;server 不做物化快照第二套。
- 重连 = 重建:v1 不实现续传 cursor(签名留可选
since),断线重连一律重开流 + 重拉 history(opencode 同款)。
1. 分层与文件布局
packages/host/apiproxy/src/
api/ ← 契约层(纯类型 + zod schema,浏览器可 import)
index.ts ← export interface ApiProxy { sessions, host, events }
sessions.ts ← SessionsApi 接口(方法签名 = 出入参事实源)
sessions.schema.ts ← 上者的 zod schema(一域一对文件,同名 .schema.ts 后缀)
host.ts / host.schema.ts
events.ts / events.schema.ts ← 流签名 + 帧类型 + 帧 schema
approvals.ts / approvals.schema.ts ← 审批域(v1.5,§3.4)
questions.ts / questions.schema.ts ← 问答域(v1.5,§3.4;问题标识复用 RpcId,无新 brand)
rpc.ts ← RpcResult / RpcError / RpcId / 窄形 RpcRequest·RpcResponse / 四具名 wire 全形 + RpcMessage / RpcReceipt
rpc-map.ts ← RpcMethodMap + RequestPayload / ResponseValue
impl/ ← Node 侧实现(boot harness core、实现 ApiProxy)
fetch/
handler.ts ← toFetchHandler(api): (Request) => Promise<Response>
client.ts ← IApiClient + AbstractApiClient + InProcessApiClient(§4.1 类体系)
api/零 Node 依赖;impl/只在 host 进程加载;client 包只 importapi/+fetch/client.ts。- 新域(provider、approvals…)= 新的一对文件 +
ApiProxy根接口一个字段。
依赖方向
apps/dsc ──► apiproxy/impl ──► harness core
│ implements
▼
apiproxy/api(契约,唯一权威)
▲ import type + AbstractApiClient 子类
web-runtime ──► apiproxy/fetch/client ──► HTTP or 注入的 handler
id 纪律(branding,2026-07-19 拍板)
SessionId:复用 core 的 branded 类型(type-only import 自 dsh-session,浏览器零运行时)。契约中所有 sessionId 一律SessionId。id 全部源自 server 响应(list/create),client 只回传,无需构造器;zod parse 在 shape 校验后一次 cast 上 brand(每个.schema.ts一个 cast 点)。brand ≠ 存在性校验——session-not-found仍由 impl 判。- cursor 不 brand(2026-07-19 20:00 拍板):v1 未实现的预设占位不提前上 brand,签名用裸
string(cursor?: string);将来实现分页时再决定是否 brand。 RpcIdbrand(2026-07-19 21:04 随信封拍板;21:19 语义扩展):opaque + 跨界往返 → brand;与 cursor 占位不同,v1 实装即进签名。mint 方 = 交互发起方:unary 调用由 client mint(应答只回显);server 发起的交互由 host mint——问答 ask 的问题标识、每条 SSE 帧的推送标识(帧信封)。单一品牌单一构造函数RpcId()(coreSessionId()先例),谁发起谁构造。- 事件内部 id 免费:
CallId等随SessionEvent透传,core 已 brand,本层不重复定义。 - seq 一族有意不 brand(
beforeSeq/lastSeq/since值):非 opaque——要做大小比较、且从透传的event.seq(core 裸number)派生;只在本层 brand 会逼每处派生 cast,与透传相抵。v1 仅 seq 一族数字无混用风险;出现第二族(revision/generation)再上 BrandedNumber。 - 闭合 union(
mode、错误码)不 brand——union 是更强的约束。
命名 convention(2026-07-19 20:15 拍板,20:41 随形态 B 终选改写)
- 方向在消息 tag 上可辨识(22:00 四象限重写):wire 全形 =
ClientRequest/ServerResponse/ServerRequest/ClientResponse四具名判别 union(§2);签名窄形 =RpcRequest<P>/RpcResponse<T>;payload 派生 =RequestPayload<K>/ResponseValue<K>。帧是 ServerRequest 的 payload(具名帧 union 保留,§3.3)。 - 禁重复内联(20:15「每方法具名/签名禁内联」拍板随 B 放宽):参数/返回的字面量结构只住方法签名一处(事实源),签名之外——handler、client、store、测试——一律
RequestPayload<K>/ResponseValue<K>泛型引用,不复写字面量、不另起具名平铺类型。 - 空 request 写空字面量
{}:将来加字段就地扩展签名,泛型引用处零迁移。 AbortSignal不进 input:input 定义为 wire 载荷,与 schema 一一对应;signal 不可序列化,混入会迫使 schema omit 字段、破坏satisfies z.ZodType<T>锚定。流方法签名为(input, signal: AbortSignal)——signal 是本地控制参数,独立第二参。- 信封 =
RpcResponse<T>(rpc.ts):unary 一律Promise<RpcResponse<…>>;T是业务返回结构,信封管成败。 - RPC map key 用域单数:
session.list/host.describe/events.mux(events 本身无单复),wire 路径同步/api/session.list。单复数经用户授权由设计层定(2026-07-19 20:30)。 - schema 命名按 map key 推导:
sessionListRequestSchema/sessionListValueSchema(住<域>.schema.ts),锚定对应泛型引用(见 §0.5)。 - 消息层/业务层两层,签名显式感知窄形(21:04 两层分离拍板 → 22:00 四象限重写):wire 全形=四具名判别 union(§2),业务 payload 纯净内嵌;域接口签名收/吐窄形
RpcRequest<P>/RpcResponse<T>(rpcId 显式,业务 payload 内不混 rpcId);全形补全(type tag/method)收口在 fetch 载体层。
RPC map:函数签名即事实源(2026-07-19 20:41 终选形态 B)
方法签名是唯一权威:接口方法的参数/返回结构直接内联写在签名里;RpcMethodMap 登记方法本身;Request/Response 一律经条件类型从签名反推,任何签名之外的地方只引用泛型。
// rpc-map.ts —— map 只登记 client-request 方法(respond 是 client-response 不在此,22:00 四象限)
export interface RpcMethodMap {
'session.list': SessionsApi['list']
'session.create': SessionsApi['create']
'session.history': SessionsApi['history']
'session.prompt': SessionsApi['prompt']
'session.cancel': SessionsApi['cancel']
'host.describe': HostApi['describe']
}
// 22:1x 撞名重命名(wire 四具名占用原名 ClientRequest/ServerResponse):
export type RequestPayload<K extends keyof RpcMethodMap> = Parameters<RpcMethodMap[K]>[0]['payload']
export type ResponseValue<K extends keyof RpcMethodMap> =
Awaited<ReturnType<RpcMethodMap[K]>> extends RpcResponse<infer T> ? T : never
- map key 即 wire 路径段(
POST /api/session.list),toFetchHandler/AbstractApiClient对 map key 类型安全机械遍历。 - 流方法不进
RpcMethodMap(不是 unary RPC):events.mux/events.host的 input 结构同样内联在签名,帧类型是具名 union(§3.3)。 - 平铺具名 Request/Response 类型删除(非降级为派生别名):别名是同一事实的第二个名字,与「任何地方都引用泛型」相抵;zod 直接锚
RequestPayload<'session.list'>,不需要中间名。 - 备选未采用:形态 A(类型对 map,具名 interface 为事实源 + map 登记类型对),2026-07-19 20:30 曾并排呈案,用户终选 B。
2. RPC 消息模型:四象限统一信封(2026-07-19 22:00 定型,推翻 21:04「签名不感知信封」)
通道与消息解耦:HTTP = client→server 物理通道,SSE = server→client 物理通道,仅此而已。逻辑消息独立于通道,每个 wire 消息统一带 initiator(谁发起)× kind(request/response)——四象限:① client-request(经 HTTP body)② server-response(经 HTTP 应答,回填①的 rpcId)③ server-request(经 SSE 帧,server mint——审批/问答 requested 即此类)④ client-response(对③的应答,物理经 HTTP 发出,逻辑 kind=response、回填③的 rpcId)。kind/direction 在消息上而非靠通道推断——将来「client-request 的 response 走 SSE 送回」(订阅型/长回答)只是 ② 换了通道,信封不变。
// api/rpc.ts
/** 消息关联 id:request=发起方 mint(谁发起谁构造,RpcId() 照 core SessionId() 先例);response=回填对应 request 的 rpcId,不 mint 新 id。 */
export type RpcId = Branded<'rpc-id'>
export type RpcInitiator = 'client' | 'server'
/** 业务成败结果(原 RpcResponse 更名:RpcResponse 现在是消息层名字)。 */
export type RpcResult<T> = { ok: true; value: T } | { ok: false; error: RpcError }
/** 签名层窄形·请求(两个方向通用):rpcId 显式进签名,kind/initiator/method 由调用位置决定、载体层补全。 */
export interface RpcRequest<P> {
rpcId: RpcId
payload: P
}
/** 签名层窄形·应答(两个方向通用):rpcId 恒为对应 request 的回填。 */
export interface RpcResponse<T> {
rpcId: RpcId
result: RpcResult<T>
}
/** wire 全形 = 四具名类型的判别 union(22:1x 用户裁决字面四具名;判别子 = type 四字面量,initiator/kind 由 tag 自明不设冗余字段)。 */
export interface ClientRequest {
type: 'client-request'; rpcId: RpcId; method: string; payload: unknown
}
export interface ServerResponse {
type: 'server-response'; rpcId: RpcId; result: RpcResult<unknown>
}
/** server 发起的消息:需应答的交互(approval/question requested,rpcId 稳定)与纯推送(session/event 等,rpcId 标识该次推送)共用此形——是否期待应答由 method 静态区分(22:1x 用户采纳设计层严格二分,不设第三 kind)。 */
export interface ServerRequest {
type: 'server-request'; rpcId: RpcId; method: string; payload: unknown
}
export interface ClientResponse {
type: 'client-response'; rpcId: RpcId; result: RpcResult<unknown>
}
export type RpcMessage = ClientRequest | ServerResponse | ServerRequest | ClientResponse
/** 载体回执(非 RpcMessage——属载体层,同「HTTP status 只表载体」纪律):承载 client-response 的 POST 的 HTTP 应答体。 */
export type RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }
窄形与全形的关系:RpcRequest<P> / RpcResponse<T>(上文)是域接口签名视角的窄形(只含业务层必须感知的 rpcId+载荷);四具名是 wire 权威全形——载体层把窄形补全为全形(补 type tag 与 method),方向不靠通道推断。
撞名重命名(全链一致):wire 四具名占用 ClientRequest/ServerResponse 名字,rpc-map 的派生泛型工具改名——ClientRequest<K> → RequestPayload<K>(= Parameters<RpcMethodMap[K]>[0]['payload'],提取 payload 穿过 RpcRequest 窄形)、ServerResponse<K>/ServerValue<K> → ResponseValue<K>(= 返回的 RpcResponse<T> 中 infer T;原两名合一,中间形无消费者)。
四象限纪律:
- 签名显式收信封窄形(推翻「签名不感知」):unary 方法
method(request: RpcRequest<{…}>): Promise<RpcResponse<{…}>>——业务字面量仍只住签名(形态 B 不变),但包在RpcRequest<>里;impl 必须回显request.rpcId进返回的RpcResponse(server 感知 rpcId 是模型要求,不因 HTTP 自带信道而省略)。流方法 yieldRpcRequest<帧>(server-request 窄形)——可应答帧的 rpcId 是 client 回填应答所必需,必须暴露给业务层,不再有「载体层拆掉」一说。 - 纯推送 = 不期待应答的 server-request,严格二分不设第三 kind(22:1x 用户采纳设计层方案):是否期待应答是 method/帧型的静态语义(登记表可查),不是每条消息的动态属性;接收方对两者处理本就相同(处理、不回)。迟到应答走既有 late-response 丢弃路径(RpcReceipt not-pending)。
- rpcId mint 规则:client-request=client mint;server-request=server mint——其 rpcId 是稳定逻辑请求 id(ask 受理时 mint 一次,subscribed 基线重放时原样复用,client 以它回填应答);notify 的 rpcId 标识该次推送(每次发射新 mint)。response 一律回填、绝不 mint 新 id(对称性:谁发起谁 mint,应答方回填)。
- method 字段:request 全形必带(unary=map key;帧载 request=帧的 type,与 payload.type 重复是「帧保持 fold 可直接消费」的代价);response 无 method(rpcId 已关联)。handler 仍校验 unary 的 path==method。
- client-response 的 HTTP 应答 = RpcReceipt 载体回执,不是逻辑消息(response 不再有 response);迟到/重复应答 →
{accepted:false, reason:'not-pending'}+ server 日志,逻辑收敛面仍是 resolved 帧。approval-not-pending / question-not-pending 两错误码随之删除(其宿主——respond 作为 unary 方法的 RpcResult——已不存在)。 - rpcId 不承担 cursor:durable 事件续传/补缝锚仍是透传的
event.seq;prompt 的 rpcId 额外经MessageSource透传进user/message(provisional 关联机制,执行 v1 不做,§6)。 - zod 分层:wire 全形 schema 一个(kind 判别 + method 合法性)+ 业务 payload schema 按 method/帧型分派,两级 parse。
2.1 错误模型(details 强类型化,20:56 拍板;22:00 随四象限删两码)
export interface RpcErrorDetailsMap {
'bad-request': { issues: z.ZodIssue[] } // zod 校验失败明细
'session-not-found': { sessionId: SessionId }
'agent-busy': { reason: string } // core 拒绝原因透传
'internal': {} // 无结构化信息可给(message 已在信封)
}
export type RpcErrorCode = keyof RpcErrorDetailsMap
// map 展开的分布式 union(非泛型 interface):code 是判别子,switch 后 details 自动窄化。
export type RpcError = {
[C in RpcErrorCode]: { code: C; message: string; details: RpcErrorDetailsMap[C] }
}[RpcErrorCode]
- details 必填(internal 显式
{}):与「必须真填」纪律互锁(rpc-compare 2026-07-19),漏填=编译错误;bad-request 放 zod issues、session-not-found 放 sessionId、agent-busy 放 core 拒绝原因。 - zod:
rpcErrorSchema = z.discriminatedUnion('code', [...])逐支锚定;新码=map 加行+schema 加支。 - transport 故障(断网、进程没起)由 fetch 载体抛异常,与业务错误两层不混;流正常结束=server 关流,中途错误以
stream/error帧收敛,断线由载体抛异常。
3. ApiProxy 根接口
export interface ApiProxy {
sessions: SessionsApi
host: HostApi
events: EventsApi
/** 对 server-request 的应答入口(client-response,回填其 rpcId);不是域方法(22:00 四象限,§3.4)。 */
respond(message: ClientResponse): Promise<RpcReceipt>
}
3.1 SessionsApi(sessions.ts)
export interface SessionSummary {
sessionId: SessionId
updatedAt: number // 持久化文件 mtime(v1 不建索引,list 时 readdir+stat)
running: boolean // attached agent 的 status;冷 session(未 attach)恒 false
parentSessionId?: SessionId // fork/spawn 谱系(session.header.parentSession 透传);根 session 缺省(v1.5,L1)
cwd?: string // session 工作目录(header.cwd 透传);未记录则缺省(v1.5,L2)
}
// 方法签名即事实源(形态 B):参数/返回结构只住在这里,
// 其余一切引用 RequestPayload<'session.*'> / ResponseValue<'session.*'>。
export interface SessionsApi {
/** 列出已持久化 session(updatedAt 倒序)。v1 全量返回;cursor 留座不实现。 */
list(request: RpcRequest<{ cursor?: string }>): Promise<RpcResponse<{ items: SessionSummary[] }>>
/** 创建新 session(并创建对应 agent,空闲待命)。 */
create(request: RpcRequest<{ cwd?: string }>): Promise<RpcResponse<{ sessionId: SessionId }>>
/**
* 读取历史事件窗口,**页边界对齐消息边界**:一页 = 整数条消息所辖的全部原始事件
* (含其 chunk / tool 事件),绝不从一条消息中间截断。尾页(beforeSeq 缺省)额外
* 含「进行中 partial」——最后一条未定稿消息已有的 chunk 事件。
* 返回仍是原始 SessionEvent[] 透传,client 用统一 fold 重建。
*/
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
Promise<RpcResponse<{ events: SessionEvent[]; hasMore: boolean }>>
/** 发送。content 直接用 core 的 ContentBlock[];mode 1:1 映射 queue→send、steer→steer。 */
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
Promise<RpcResponse<{ accepted: true }>>
/** 停止:清两条 FIFO + abort 当前 step(agent.cancel 的 1:1)。 */
cancel(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ accepted: true }>>
}
(22:00 签名信封化:一切 unary 收 RpcRequest<P>、返 RpcResponse<T>(rpcId 回填);业务字面量仍只住签名(形态 B),impl 感知并回显 rpcId。)
- 冷 session 隐式 resume:
history()/prompt()命中未 attach 的 session 时 impl 内部自动 resume/attach,client 无感;attach 与否不对客暴露(running已覆盖 UI 所需)。实现注记:并发触发同一 session 的 resume 必须去重(Map<SessionId, Promise>在途表,照 jsonrpc serversessionCreations先例;rpc-compare 2026-07-19)。 SessionSummary保持三字段最小面;title/eventCount/待处理计数等后续按需 additive。- prompt 幂等(commandId)v1 不做;input 加可选字段即是接缝。
- history 分页实现注记:server 从尾向前扫 surface 消息事件(
user/message/assistant/message/steering/message)计数到maxMessages,在消息组边界切beforeSeq;chunk 归属其定稿消息(sourceEventSeqs锚定),页内事件保持原始 seq 序。
3.2 HostApi(host.ts)
export interface HostApi {
/**
* host 一次性快照。空 request 用空字面量 `{}`(将来加字段就地扩展)。
* version = host 应用(apps/dsc)package.json 版本;cwd = host 进程工作目录
* (session 持久化与工具执行的根);provider/model = 新建 agent 未显式指定时
* 生效的默认值,host 未配置显式默认则缺省(adapter 内部兜底);
* attachedSessions = 当前已 attach(有活 agent)的 session 数。
*/
describe(request: RpcRequest<{}>): Promise<RpcResponse<{
version: string
cwd: string
provider?: string
model?: string
attachedSessions: number
}>>
}
- 不设协议版本(2026-07-19 20:02 拍板):client/host 绑定发布,wire 兼容判断无消费者;将来若出现独立发布的 client 再引入 protocolVersion。
- provider/model 形状对齐 core
AgentOptions(可选裸string,非 branded),透传原则;缺省表示 host 未配置显式默认(adapter 内部兜底)。
3.3 EventsApi(events.ts)——两条流
export interface EventsApi {
/**
* 全 session 聚合 mux 流。打开即对每个 attached session 发 subscribed 控制帧。
* since:续传接缝,v1 不实现(传了也忽略);重连走「重开流 + 重拉 history」。
* signal 是本地流控制参数,独立于 input(不上 wire)。
* 22:00 四象限:yield 的是 server 消息窄形 { rpcId, payload: 帧 }——rpcId 必须暴露给业务层
* (approval/question requested 帧的应答要回填它),不再有「载体层拆掉信封」。
*/
mux(request: RpcRequest<{ since?: Record<SessionId, number> }>, signal: AbortSignal): AsyncIterable<RpcRequest<MuxFrame>>
/** host 级信息流:session 创建/销毁、运行状态翻转。空 payload 用 `{}`。 */
host(request: RpcRequest<{}>, signal: AbortSignal): AsyncIterable<RpcRequest<HostFrame>>
}
// ---- Frame 族(server→client 推送,具名 union 保留,不适用 infer) ----
export type MuxFrame =
| { type: 'session/event'; sessionId: SessionId; event: SessionEvent } // 核心:纯透传
| { type: 'session/subscribed'; sessionId: SessionId; lastSeq: number } // 控制帧(lastSeq 保留:缝检测)
// ---- 审批/问答控制帧(v1.5,§3.4):requested 下行提问,resolved 收敛 ----
| { type: 'approval/requested'; sessionId: SessionId; approvalId: ApprovalRequestId; toolName: string; callId?: CallId; reason?: string }
| { type: 'approval/resolved'; sessionId: SessionId; approvalId: ApprovalRequestId; outcome: ApprovalOutcome }
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] } // 问题标识=信封 rpcId(帧 payload 无独立 id)
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
| { type: 'stream/error'; error: RpcError }
export type HostFrame =
| { type: 'host/session-added'; sessionId: SessionId; parentSessionId?: SessionId } // 谱系锚(v1.5,L1)
| { type: 'host/session-removed'; sessionId: SessionId }
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
| { type: 'host/agent-error'; sessionId: SessionId; message: string } // 无 turn 位置的 live 失败诊断(agent/error 无 session 事件时的唯一出口;v1.5,L5)
| { type: 'stream/error'; error: RpcError }
透传纪律:session/event payload 就是 core SessionEvent(自带 seq;assistant/chunk 原样过——token 流即事件流,无独立 delta 帧)。SessionEventMap 是 merge-extensible:client fold 对未知 type documented-default(计数忽略);事件 schema 在 union 层面留「合法信封 + 未知类型」分支,信封(seq/type 结构)仍严格——这不是字段级 passthrough。
subscribed.lastSeq 的用途(已拍板保留):client 拉完 history 后对比 history 尾 seq 与 lastSeq,有缝(开流与拉历史之间 session 前进了)就再补一次 history;一个字段消掉一类竞态。
tool presentation(已拍板:先透传,标注遗留):v1 卡片直接渲 tool/call / tool/result 原始 args/result;presentCall/presentResult 的 render intent(generic/terminal/diff/locations)在 Node 侧才有,后续以 additive 附件帧或旁挂字段引入,不动透传主体。
3.4 审批/问答域(approvals.ts / questions.ts,v1.5 采纳,2026-07-19 21:13)
形态(21:00 拍板;22:00 四象限重建模):审批/问答的 requested 帧 = server-request(rpcId=server mint 的稳定逻辑请求 id);client 的回答 = client-response(回填该 rpcId,物理经 POST /api/respond 发出,逻辑上是应答不是新调用——不再是 unary 方法,不 mint 新 rpcId)。*/resolved 帧是收敛面——多 client、tool 取消、policy 决掉都靠它撤卡片;client-response 的 HTTP 应答体是载体回执 RpcReceipt(见 §2),最终结局统一看 resolved。
// approvals.ts / questions.ts —— 应答 payload 形状(client-response 的 result.value 位)
/** 审批应答:outcome 只收 client 可给的二值(cancelled/unavailable 是 host 侧结局)。 */
export interface ApprovalResponsePayload {
sessionId: SessionId
approvalId: ApprovalRequestId // core 审计关联(impl 对账 asked/decided 用);wire 关联以回填的 rpcId 为准
outcome: 'allowed-once' | 'rejected'
}
/** 问答应答:整批回答一次 ask(core:一次 ask 多题一个 answer,不拆单题)。 */
export interface QuestionResponsePayload {
sessionId: SessionId
answer: AskUserQuestionAnswer
}
- respond 不进 RpcMethodMap(map 只登记 client-request 方法):client-response 是对 server-request 的应答,wire 承载
POST /api/respond(单端点,body=ClientResponse 全形,rpcId 即路由键——host 从 pending 表查该 rpcId 属审批还是问答再按对应 payload schema parse)。ApiProxy 根接口相应无 approvals/questions 域方法;client 侧发应答走IApiClient.respond(message: ClientResponse)载体级入口(AbstractApiClient 实现,§4.1)。 - id 双层:wire 关联 = requested 帧的 rpcId(server mint、重放复用、client 回填);
approvalId(coreApprovalRequestId透传)保留在审批 payload 内层供 impl 对账 durable 审计事件approval/asked/decided——它是 core 已 brand 的透传非本层自造(21:19 拍板的不对称理由继续成立)。问答无 core id,payload 不含资源 id(rpcId 已足)。 - 竞争语义:先到先赢,host 内存 pending 表(keyed by rpcId)是唯一裁判,一个 rpcId 只 settle 一次。竞争方:client-response vs tool
signalabort(→cancelled)vs 另一 client。policy'never'在 answerer 链之前解决,requested 帧根本不发。core 无审批超时,不发明。迟到/重复应答 →RpcReceipt {accepted:false, reason:'not-pending'}(载体回执,非业务错误码——两个 not-pending 错误码已随四象限删除)。 - 刷新恢复:subscribed 基线重放——mux 重开后,host 在每个 session 的
session/subscribed帧后立即重放该 session 仍 pending 的*/requested帧(rpcId 原样复用,来源=内存 pending 表)。单一事实源走 mux;不从 history 推(问答不落日志;审批 crash 后 asked-without-decided 是悬案)。 - impl 结构:host 注册「wire answerer」进
approval/requestwaterfall 链(收请求→mint rpcId 发 server-request 帧→等 client-response/abort→返回 outcome,链上其他 answerer 组合语义不变);问答侧 host 代理 provider 是userInteraction.registerProvider的唯一注册者。approval/asked/decided审计事件照旧透传——帧=live 控制面,事件=durable 审计,职责分离。
4. fetch 载体(RPC 映射,机械可推导)
| 逻辑消息(四象限) | wire 承载 |
|---|---|
| client-request | POST /api/<map key>(即 /api/session.list,域单数),body=ClientRequest 全形 JSON |
| server-response | 上述 POST 的 HTTP 应答体,ServerResponse 全形 JSON,HTTP 200 恒定 |
| server-request / 纯推送 | SSE 帧:GET /api/events.mux / GET /api/events.host,data: = ServerRequest 全形 JSON |
| client-response | POST /api/respond(单端点),body=ClientResponse 全形 JSON;HTTP 应答体=RpcReceipt 载体回执 |
toFetchHandler(api):unary 路径查方法 → 全形 zod parse(type/rpcId/method 结构 + path==method 校验)→ payload 按 method 分派 schema parse(拒收 =bad-request)→ 调 api 方法(传窄形RpcRequest<P>)→ 回填 rpcId 封ServerResponse;/api/respond→ ClientResponse 全形 parse → rpcId 查 pending 表路由到审批/问答 → payload schema parse → 返回RpcReceipt;流方法包 SSE Response,帧以ServerRequest全形发出(method=帧 type;可应答帧 rpcId=稳定逻辑 id,纯推送=每次新 mint)。- HTTP status 只表载体:404 路径不存在 / 400 body 非 JSON / 500 handler 自身炸;业务错误一律 200 + ServerResponse(RpcResult error 位)。
4.1 client 载体:AbstractApiClient 类体系(2026-07-20 shape-a + abstract-base 拍板;commit 893421d50 落地,取代 createApiClient 工厂形)
协议不变量住抽象基类,平台差异是两个继承切面:抽象方法 doFetch(url, init)(传输)+ 可覆写 onEnvelope(观测)。
IApiClient(caller 视图,shape a):与 ApiProxy 同域树,但 unary 方法收业务 payload 直传——载体 mint rpcId 并包信封,业务代码永不 mint;需要本次调用 rpcId 的从返回RpcResponse回显里读。三者关系:ApiProxy = impl 侧实现的窄形签名契约;IApiClient = client 侧消费的 payload 直传视图;AbstractApiClient 桥接两者。域方法逐 key 从 RpcMethodMap 派生,map 加行即机械更新。AbstractApiClient持有的协议路径:callUnary(mint→tap→POST 全形→ServerResponse parse→rpcId 回显校验(不符 throw)→tap→吐窄形);readSse(streaming fetch 非 EventSource、\n\n分帧、ServerRequest 全形 parse、tap、吐窄形RpcRequest<帧>);respond透传(rpcId 是回填不 mint);unary 超时AbortSignal.timeout(默认 30s 构造参数可调,流不设超时);resolveBase(浏览器=同源 origin,无 location 环境=http://dsh.internal假 authority)。callUnary/openMux/openHost是 protected virtual——假载体(fixture)在协议层覆写,不再需要信封包装器。- 实例级 envelope 观测切面:四象限全形均过
onEnvelope;基类实现=实例持有的微任务合批缓冲(帧风暴不逐帧惊扰消费者;实例持有防跨实例/测试泄漏)。观测者经subscribeEnvelopes(listener)批量订阅(收readonly RpcMessage[],返回退订函数);listener 异常隔离(观测不得反噬载体);无订阅者零缓冲成本。原onEnvelope构造选项与ApiEnvelopeTapEvent四支形状废除——tap 事件即 RpcMessage 全形本身,kind=type tag。rpcLog 调试面板降级为纯订阅者(连接主体身份取消)。 - 子类表:
InProcessApiClient(apiproxy 本包;doFetch=注入的{fetch}handler;同构点新写法new InProcessApiClient(toFetchHandler(api))全程不过网络——dsc -p headless 即此);WebApiClient(web-runtime;doFetch=globalThis.fetch 同源);FixtureApiClient(web-runtime;协议层覆写四虚方法,自己就是假 server、帧 rpcId 由它 mint);将来 Electron IPC 桥=又一子类只换 doFetch。
5. web client 侧(web-runtime)架构
WebApiClient(AbstractApiClient 子类,§4.1)
│
ConnectionController ← boot 开两条流;断线指数退避重连;重连后对每个打开的
│ session 重拉 history 重建(v1 无续传)
SessionFold(纯函数) ← SessionEvent[] → UI 树;历史与 live 同一 fold;
│ tool call/result 按 CallId 合并;优先复用 core foldSurface
Session/SessionManager ← 业务对象层(step-session 设计);React 经 uSES 订阅对象快照,
store 只承载跨视图展示态
- 打开 session 主路径:开 mux(收
subscribed.lastSeq)→history()拉尾页 → 比对 lastSeq 补缝 → live 帧续贴。 - 单客户端互斥(ClientSlot)v1 不实现:第二个页面各自收流,行为未定义但不崩。
- unary 请求 client 侧设超时(
AbortSignal.timeout在 AbstractApiClient.callUnary 内,默认 30s 构造参数可调):浏览器 fetch 默认无超时,host hang 会让请求永久 pending(rpc-compare 2026-07-19)。流不设超时(长连接本性)。
6. 不做清单(v1)
- mux
since续传实现(签名留座) - rpcId 幂等去重的执行(rpcId 已全量上 wire 是其接缝,2026-07-19 21:04);provisional 转正的关联机制已就位(rpcId 经 MessageSource 透传进
user/message),client 侧转正逻辑 v1 不做 - SessionSummary 索引(eventCount/title/待处理计数)、keyset 分页
审批/问答域(v1.5 升格进契约,§3.4;实现排期仍可后置);provider 配置事务、模型切换(类型接缝见 §8)- tool presentation 附件(§3.3 标注)
- ClientSlot、connectionGeneration/streamId fencing
7. 裁决记录
三批问题(Q1–Q8 等)已全部拍板,无开放问题;全记录见 README.md 拍板表。opencode 对照结论见 opencode-crosscheck.md(重连砍 cursor 的建议已被采纳,即 §0.7)。core 覆盖度盘点与漏判裁决(L1-L7)见 core-coverage.md;L1/L2/L5 已合入本文(v1.5),L3/L4/L6/L7 类型预留见 §8。
8. 预留接缝类型(L3/L4/L6/L7,2026-07-19 21:13 裁决:类型写全,暂不实现)
纪律:以下签名是将来实现时可直接照抄的定稿形状,但不进 RpcMethodMap、不进 ApiProxy 根接口——map 只含已实现方法,未知 method 在信封 parse 即 fail loud(bad-request),优于 not-implemented 兜底码:后者要求每个方法实现「假在场」,让「契约有」和「能用」脱钩,违反 misconfiguration-fails-loud 家规。实现某条时:把签名抄进对应域接口 + map 加一行 + schema 加一对,即完成升格。
// ---- L3 fork(session 域;core 原语 SessionStore.fork 完整,错误码 SessionForkErrorCode) ----
// map key(届时):'session.fork'
fork(input: { sessionId: SessionId; boundary?: number; childSessionId?: SessionId }):
Promise<RpcResponse<{ sessionId: SessionId }>>
// RpcErrorDetailsMap 届时加:'fork-rejected': { code: SessionForkErrorCode }(core 五码透传:
// SESSION_NOT_FOUND/SESSION_NOT_LIVE/SESSION_ALREADY_EXISTS/INVALID_BOUNDARY/OPEN_TURN)
// ---- L4 inject(prompt.mode union 扩展;core Agent.inject 第三输入原语) ----
// 非新方法——现 prompt 签名的 mode 加一值:
prompt(input: { sessionId: SessionId; mode: 'queue' | 'steer' | 'inject'; content: ContentBlock[] }):
Promise<RpcResponse<{ accepted: true }>>
// inject 语义:注入上下文不触发模型回复(idle 时 core 包一次性 turn);impl 分发 agent.inject()。
// ---- L6 后台任务面板(新 task 域;core ctx.tasks TaskSnapshot) ----
// map key(届时):'task.list';域文件 tasks.ts / tasks.schema.ts
list(input: { sessionId?: SessionId }): // 缺省=全部;带 sessionId=按 owner agent 过滤(core list(caller) 语义)
Promise<RpcResponse<{ tasks: TaskSnapshot[] }>> // TaskSnapshot 透传 core(dsh-tasks)
// 完成通知(届时)HostFrame 加:
// | { type: 'host/task-done'; taskId: TaskId; status: 'completed' | 'killed' | 'failed' }
// ---- L7 provider/model 枚举(host 域;与模型切换域一起实现) ----
// map key(届时):'host.listModels'(独立方法,不并入 describe——describe 是轻快照,
// 枚举可能触发 adapter 查询,成本与缓存策略不同)
listModels(request: RpcRequest<{}>):
Promise<RpcResponse<{ providers: { provider: string; models: string[] }[] }>>
// ---- host 实例标识(ui-design 2026-07-19 22:1x 提出;v1 重连=重建故零影响,将来续传/缓存需要) ----
// describe 返回加可选字段(届时):
// hostInstanceId?: string // host 进程每次启动 mint(uuid),client 据此察觉 host 重启、废弃本地缓存
// 砍 protocolVersion(20:02)的连带缺口,实装时同批评估是否需要 brand。