Files
deepseek-harness/missions/tasks/20260719-1902-opencode-api-research/findings.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

73 lines
12 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.
# opencode「统一 API 层」调研(fetch 同构 / SSE 事件流 / OpenAPI 类型链)
调研对象:`/weka-hg/prod/deepseek/permanent/ys/private/workspace/github/opencode`(HEAD `f5573281`,2026-07-19)。下文 file:line 均相对该仓库根。
**先纠正一个背景认知**:TUI 已不是 Go。Go/Bubbletea TUI 于 2025-11-02 被删(commit `f68374ad2` "DELETE GO BUBBLETEA CRAP HOORAY"),现 TUI 是 TypeScript + solid-js(opentui 渲染,`packages/tui`),与 server 同进程不同线程(Bun Worker)。全仓已无 `.go` 文件。
## 1. server 定义
**框架不是 Hono,是 Effect 的 `effect/unstable/httpapi`**(声明式 HttpApi DSL)。Hono 只在 `enterprise`、`function`(云端/SST 部分)用到(`packages/enterprise/package.json:25`、`packages/function/package.json:17`),本地 server 完全不用。
- 入口:`packages/opencode/src/server/server.ts`
- `import { HttpRouter, HttpServer } from "effect/unstable/http"`、`OpenApi from "effect/unstable/httpapi"`(server.ts:6-7)。
- 监听:`Server.listen(opts)`(server.ts:73)→ `listenEffect` → `listenerLayer`(server.ts:100-116)用 `HttpRouter.serve(...)` + `NodeHttpServer.layer(() => createServer(), {port, host})`(server.ts:199-214,node:http 的 createServer)。端口回退:显式 0 时先试 4096 再随机(server.ts:118-123)。
- 谁调 listen:`serve` 命令(`packages/opencode/src/cli/cmd/serve.ts:19`);TUI worker 仅在用户给了 `--port/--hostname/--mdns` 时调(`packages/opencode/src/cli/tui/worker.ts:56`)。
- 路由声明与实现分离,全部在 `packages/opencode/src/server/routes/instance/httpapi/`:
- `groups/*.ts` = API 形状声明(路径、params、payload/success/error Schema、OpenAPI 注解),如 `groups/session.ts`、`groups/global.ts`、`groups/event.ts`。
- `handlers/*.ts` = 实现(`HttpApiBuilder.group(Api, name, handlers => ...)`),如 `handlers/session.ts`、`handlers/event.ts`。
- `api.ts` 组装:`RootHttpApi`(control/control-plane/global)+ `InstanceHttpApi`(config/file/session/provider/... 15 组)→ `OpenCodeHttpApi`(api.ts:56-80)。
- **分层关系**:handler 是薄壳,业务在 Effect Service 里。如 session handler 注入 `SessionPrompt.Service` 后 `promptSvc.prompt(...)`、`promptSvc.cancel(...)`(handlers/session.ts:52、233、301);server.ts 只负责把几十个 service Layer(Session、Provider、Permission、MCP、LSP……见 routes/instance/httpapi/server.ts:8-57 的 import 清单)组进 HttpApi 运行时。
## 2. API 契约与类型:OpenAPI codegen(@hey-api/openapi-ts),非 hono/client
- SDK 在 `packages/sdk/js`,生成链(`packages/sdk/js/script/build.ts`):
1. `bun dev generate > openapi.json`(build.ts:15)——即 CLI `generate` 命令调 `Server.openapi()`(`packages/opencode/src/cli/cmd/generate.ts:10`),后者 `OpenApi.fromApi(PublicApi)` 从 Effect HttpApi 声明直接导出 OpenAPI 文档(server/server.ts:67-69)。**单一事实源是 server 端的 Effect Schema 声明**。
2. `createClient({input: openapi.json, output: src/v2/gen, plugins: [@hey-api/typescript, @hey-api/sdk(instance: OpencodeClient, paramsStructure: flat), @hey-api/client-fetch]})`(build.ts:19-72)。产物:`types.gen.ts`(全部请求/响应/事件类型)+ `sdk.gen.ts`(OpencodeClient 方法树,如 `sdk.session.prompt(...)`)+ `client/`(fetch 客户端)。
- **自定义 fetch 注入**:`createOpencodeClient(config)` 的 `config.fetch` 就是 @hey-api client 的标配选项。v1 版 `packages/sdk/js/src/client.ts:33-42`(不传 fetch 则用包一层 `req.timeout=false` 的全局 fetch);v2 版 `packages/sdk/js/src/v2/client.ts:50-61`,另支持 `baseUrl`、`headers`、`directory`(转成 `x-opencode-directory` header,再由 request 拦截器改写成 query 参数,v2/client.ts:18-48、69-76)。
- SDK 还提供 `createOpencodeServer()`:spawn `opencode serve` 子进程、等 stdout 打出 "opencode server listening" 再解析 URL(`packages/sdk/js/src/v2/server.ts:23-60`);`createOpencode()` = server + client 一把梭(v2/index.ts:10-20)。
## 3. fetch 同构:`Server.Default().app.fetch` 直调,零网络
server.ts 导出一个**不监听端口的 app 对象**:`Server.Default()`(lazy 单例,server.ts:56-65)——`HttpApiApp.webHandler().handler` 包成 `{fetch(Request): Promise<Response>}`,即 WHATWG Request→Response 纯函数。所有同构点都是把它塞进 SDK 的 `fetch` 选项:
| 场景 | 位置 | 做法 |
|---|---|---|
| `opencode run`(非交互 CLI)| `packages/opencode/src/cli/cmd/run.ts:943-955` | `fetchFn = (input, init) => Server.Default().app.fetch(new Request(...))`,`createOpencodeClient({baseUrl: "http://opencode.internal", fetch: fetchFn})`——baseUrl 是假域名,仅用于构造 URL |
| `run` 交互本地模式 | run.ts:905-917 | 同上,传给 `runInteractiveLocalMode` |
| 插件运行时 | `packages/opencode/src/plugin/index.ts:141-146` | 给插件的 `client`:有真 server 时用 `Server.url`,**没有则 `fetch: (...args) => Server.Default().app.fetch(...args)`**——插件代码不感知区别 |
| TUI(默认模式)| 见下 | 跨 Worker 线程 RPC,仍不走网络 |
**TUI 连接方式**(`packages/opencode/src/cli/cmd/tui.ts`):主线程起 `new Worker(worker.ts)`(tui.ts:210),server 核心跑在 worker 线程里。
- 默认(无 `--port/--hostname/--mdns`,tui.ts:234):**不起 HTTP server**。transport = `{url: "http://opencode.internal", fetch: createWorkerFetch(client), events: createEventSource(client)}`(tui.ts:245-249)。`createWorkerFetch` 把 Request 序列化成 `{url, method, headers, body}` 经 Worker RPC 发过去(tui.ts:24-40);worker 端 `rpc.fetch` 还原成 Request 后 `Server.Default().app.fetch(request)`(worker.ts:31-49)。即**同构面是 fetch 签名,传输是 structured-clone RPC,非 socket**。
- 显式要求网络暴露时:worker 端 `Server.listen` 起真 HTTP(worker.ts:54-57),TUI 改用真 URL + 默认 fetch + SSE(tui.ts:238-244)。
- `opencode attach <url>` 连远端:纯 HTTP,`createOpencodeClient({baseUrl: args.attach, headers: auth})`(run.ts:349-355)。
- desktop(Electron):renderer 通过 IPC 拿 server URL(`packages/desktop/src/main/ipc.ts:53-54`),走真 HTTP,不做 fetch 直调。
## 4. 事件流:单一全局 SSE 总线 + 实例级过滤流,重连靠全量 bootstrap
两个 SSE 端点,都是 GET、`text/event-stream`:
- **`GET /global/event`**(`groups/global.ts:85-92`)——**全局单总线**,TUI/桌面默认订阅这个。handler(`handlers/global.ts:33-52`)把进程级 `GlobalBus`(Node EventEmitter,`src/bus/global.ts:12-22`)的所有事件 + 10s 心跳推给客户端。事件从核心到总线的路径:Effect 内部 `EventV2.publish` → `EventV2Bridge` 监听后 `GlobalBus.emit("event", {directory, project, workspace, payload:{id,type,properties}})`(`src/event-v2-bridge.ts:36-46`),即事件自带 directory/workspace 归属,**由客户端按需过滤**,不分 session 订阅。
- **`GET /event`**(instance 级,`groups/event.ts:7-28`)——按当前 instance directory/workspace **服务端过滤**(`handlers/event.ts:34-40`),首包发 `server.connected`,10s 心跳 `server.heartbeat`,实例销毁时发 `server.instance.disposed` 后终止流(handlers/event.ts:60-66、70)。
- 事件类型全集在 `packages/schema/src/event-manifest.ts`(`Definitions` 聚合 30+ 模块,:64-82)。主要类别:
- v1 UI 面(TUI store 实际消费的,`packages/tui/src/context/sync.tsx` switch,:171-441):`message.updated`、`message.removed`、`message.part.updated`、**`message.part.delta`**、`message.part.removed`、`session.updated`、`session.deleted`、`session.status`、`session.diff`、`permission.asked/replied`、`question.asked/replied/rejected`、`todo.updated`、`lsp.updated`、`vcs.branch.updated`、`server.instance.disposed`。
- v2 内核事件(`session.next.*`,schema/src/session-event.ts):`session.next.text.delta/started/ended`、`tool.called/success/failed/input.delta`、`reasoning.*`、`step.*`、`compaction.*`、`prompt.admitted` 等 ~40 种。
- **断线重连 = 全量重取,无 cursor**。TUI SDK 层:SSE 断开后指数退避(1s→30s 封顶)无限重连(`packages/tui/src/context/sdk.tsx:82-117`);状态恢复不靠事件回放,而是收到 `server.instance.disposed` 时整体 `bootstrap()`(sync.tsx:172-173)——并行重拉 providers/agents/config/session.list/messages 等十几个 REST 端点重建 store(sync.tsx:445-541)。事件 payload 里有 `id`(ascending 标识,bus/global.ts:15-17)和 durable 事件的 `seq`(event-v2-bridge.ts:47-60,"sync" 通道),但那是给实验性 workspace 同步用的(`sdk.sync.start()`,sdk.tsx:99),主 UI 路径不做 cursor 续传。
## 5. 命令面:session 创建 / prompt / abort
路径常量集中在 `groups/session.ts:79-104`(`SessionPaths`),全部带 `?directory=`(workspace 路由 query,middleware 解析):
- **创建**:`POST /session`,body 可空或 `Session.CreateInput`(parentID/title 等),返回 `Session.Info`(groups/session.ts:203-214;handler `handlers/session.ts:155-175`,空 body 走 `create({})`)。
- **发消息(同步)**:`POST /session/:sessionID/message`,body = `PromptPayload`(`SessionPrompt.PromptInput` 去掉 sessionID:parts、model、agent 等),**响应是阻塞到整轮 agent 循环结束后一次性返回的 message+parts JSON**(groups/session.ts:316-328;handler 里 `promptSvc.prompt(...)` 完成后 `HttpServerResponse.stream(Stream.make(JSON.stringify(message)))`,handlers/session.ts:295-309——用 stream 包装只是让连接保持,不是增量协议)。
- **发消息(异步,TUI 实际用法)**:`POST /session/:sessionID/prompt_async`,同 payload,**立即 204**,prompt 在服务端 fork 执行,错误也转成 `session.error` 事件发总线(groups/session.ts:329-342;handlers/session.ts:311-329)。
- **中止**:`POST /session/:sessionID/abort`,无 body,返回 boolean;handler 调 `promptSvc.cancel(sessionID)`(groups/session.ts:253-264;handlers/session.ts:232-234)。
- 相邻端点:`GET /session`(list,支持 start/search/limit)、`GET /session/:id/message`(历史,limit/before 分页)、`POST .../fork`、`POST .../command`、`POST .../shell`、`POST .../revert`、`permissions/:permissionID` 回复等(groups/session.ts:111-433 逐个声明)。
- **token 级增量不走 prompt 响应,全走事件总线**:处理器在生成过程中发 `message.part.delta`(字段级 append:`{sessionID, messageID, partID, field, delta}`,schema/src/v1/session.ts:632-641)和 `message.part.updated`(整 part 替换);TUI 收到 delta 后往 store 里对应 part 的 field 追加字符串(sync.tsx:392-441)。即“命令走 REST、数据走 SSE”的 CQRS 形态:prompt_async 只负责触发,渲染完全由事件驱动。
## 对 DSH 统一 API 层的可借鉴点(简评)
1. **同构面选在 WHATWG fetch(Request→Response)**是整个设计的支点:server 框架只要能产出 `fetch(Request): Promise<Response>` 纯函数(Effect httpapi 的 webHandler、Hono 的 app.fetch、我们未来的 dsc web server 均可),SDK 就能通过 `fetch` 选项零改动切换 in-process / Worker RPC / 真 HTTP 三种传输。
2. **类型打通靠 server-first OpenAPI**:路由声明用带 Schema 的 DSL → 导出 openapi.json → @hey-api codegen 出客户端。契约测试只需盯 openapi.json diff。
3. **事件设计取舍**:全局单 SSE 总线 + 事件自带归属字段 + 客户端过滤,简单但重连无 cursor,恢复靠 REST 全量 bootstrap——请求面与事件面正交,客户端 store 是唯一 join 点。