agent-Specialization/cache_research/gateway/opencode_study/opencode_architecture.md
JOJO 6e043389b9 docs(research): Gateway 化改造工作计划与范围复杂度评估
Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
Co-authored-by: Codex powered by ChatGPT-6-Astra <codex@example.com>
2026-09-07 12:27:59 +08:00

26 KiB
Raw Blame History

opencode server/client 架构研究报告

研究对象opencode 仓库(版本 1.18.29,克隆位于 <local-clones>/opencode,只读) 研究目的:为 AstrionPython Agent 项目)的 "gateway 化" 改造提供借鉴 研究方法:源码阅读,所有结论均附文件路径与关键代码证据(行号以本次阅读时为准)


总览30 秒版)

opencode 有两个并存的 HTTP 服务层:

  1. 主服务packages/opencodeTUI 实际使用)Effect HttpApi 定义 + HttpRouter.serve 运行在 Node http server 上,路由分 Root/global/*/control/*、Instance/session/*/tui/*/event、v2 protocol/api/*、PublicUI 静态资源。OpenAPI spec 从 Effect HttpApi 定义代码生成bun dev generateopenapi.json@hey-api/openapi-ts → TS SDK
  2. 精简实验服务packages/server + packages/protocol:同样是 Effect HttpApi把协议定义下沉到 @opencode-ai/protocolserver 只注入 middleware/handler 实现,挂载 /api/event/api/session/* 等 v2 路由。

核心设计:TUI 是 server 的瘦客户端,通过生成的 SDK + SSE/global/event/event消费事件server 通过事件总线反向驱动 TUI/tui/*)。


1. Server 架构框架、入口、路由、OpenAPI、SDK 生成

1.1 用的是什么框架

不是 Hono也不是 Elysia而是 Effect 生态的 HttpApi / HttpRouterEffect 自带的声明式 HTTP 框架,effect/unstable/httpapi),底层走 @effect/platform-nodeNodeHttpServernode:http

证据:

  • packages/server/src/api.tsimport { HttpApi, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi"HttpApi.make("server").add(...) 组装所有 group。
  • packages/server/src/routes.tsHttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" })webHandler()HttpRouter.toWebHandler(...) 导出 fetch handler。
  • packages/opencode/src/server/server.tsHttpRouter.serve(HttpApiApp.createRoutes(opts), ...) + NodeHttpServer.layer(() => server, ...)createServer 来自 node:http)。
  • 全局搜 hono 仅命中 server.ts 一处注释性匹配,无 Hono 依赖。

1.2 入口在哪、谁调用它

三个入口,同一套实现:

入口 文件 说明
opencode serveheadless packages/opencode/src/cli/cmd/serve.ts Server.listen(opts),监听 --port(默认 4096/--hostname
TUI 进程内嵌 worker packages/opencode/src/cli/tui/worker.ts worker 内 Server.listen(input)rpc.server 方法)或纯内嵌 Server.Default().app.fetchrpc.fetch
独立精简 server packages/cli/src/commands/handlers/serve.ts HttpRouter.serve(createRoutes(password), ...)opencode serve(老 CLI

packages/opencode/src/server/server.tslisten() 实现端口回退:startWithPortFallback 先试 4096失败再随机端口。

1.3 路由如何组织

分组HttpApiGroup+ 累积式组装,两层:

  • packages/opencode/src/server/routes/instance/httpapi/api.ts
    • RootHttpApi = Control + ControlPlane + Global/global/*
    • InstanceHttpApi = Config/Experimental/File/Instance/Mcp/Project/Question/Permission/Provider/Session/Sync/Tui/Workspace/session/*/tui/*/permission/* 等)
    • OpenCodeHttpApi = Root + Event(/event) + Instance + Server(/api/*) + PtyConnect(WS)
  • 每个 group 一个文件,如 groups/session.tsgroups/permission.tsgroups/tui.tsgroups/event.tshandler 对应 handlers/*.ts

路由路径以组内常量定义(如 groups/session.tsSessionPaths = { permissions: "/session/:sessionID/permissions/:permissionID", ... })。

1.4 OpenAPI spec 是手写还是代码生成?

纯代码生成。Effect HttpApi 的每个 HttpApiEndpointSchema 声明入参/出参/错误,用 OpenApi.annotations 附加 summary/description/identifierEffect 在运行时把整个 Api 编译成 OpenAPI 文档:

  • packages/opencode/src/server/server.tsexport async function openapi() { return OpenApi.fromApi(PublicApi) }
  • schema 定义集中在 packages/schema/srczod 风格由 Effect Schema 实现,无 TypeBox/zodsession.tssession-message.tssession-event.tspermission.tstui-event.ts 等。
  • 产物:packages/sdk/openapi.jsonopenapi 3.1.0188 个 operationId

1.5 SDK 如何从 spec 生成

packages/sdk/js/script/build.ts 全流程:

  1. bun dev generate > openapi.json:调用 CLI generate 命令(packages/opencode/src/cli/cmd/generate.ts)→ Server.openapi() 输出 spec并给每个 operation 注入 x-codeSamples
  2. @hey-api/openapi-tscreateClient)生成:
    • src/v2/gen/types.gen.tsTS 类型)
    • src/v2/gen/sdk.gen.tsOpencodeClient 实例化 SDKparamsStructure: "flat"
    • src/v2/gen/client/*fetch clientbaseUrl 默认 http://localhost:4096
  3. 若干手工 patchsession.history 分页类型 string→number、SSE 泛型 bug
  4. bun prettier + bun tsc 校验。

SDK 对外暴露:packages/sdk/js/src/v2/client.tscreateOpencodeClient,支持自定义 fetch、x-opencode-directory 头路由到指定目录),packages/sdk/js/src/v2/server.tscreateOpencodeServer 用 cross-spawn 拉起 opencode serve 子进程)、process.ts

对 Astrion 的借鉴意义

  • 如果 Astrion 也要"HTTP API + 生成 SDK",可以用类似双轨:自己手写或代码生成 OpenAPI v3.1,再接入 openapi-typescript / openapi-generator / hey-api 生成 TS SDKPython 侧可用 openapi-python-client
  • "协议包protocol与实现server分离 + handler 注入"的分层(@opencode-ai/protocol 定义 group 与 error@opencode-ai/server 注入 middleware/handler值得借鉴便于多入口复用同一协议。
  • OpenAPI 元数据summary/description直接写在 schema 附近,文档与代码同源,避免过期。

2. 事件系统:/event、/global/event、事件总线、序号与重连

2.1 两条事件通路

A) 全局总线 /global/eventRootHttpApiGlobalApi

  • 总线本体:packages/opencode/src/bus/global.ts —— 单例 Node EventEmitterGlobalBus.emit("event", {...})),事件带 directory/project/workspace/payload
  • SSE 出口:packages/opencode/src/server/routes/instance/httpapi/handlers/global.tseventResponse()Stream.callbackGlobalBus.on("event") 转成 Effect Stream先发 server.connected10 秒心跳,Stream.pipeThroughChannel(Sse.encode())
  • 事件源:packages/opencode/src/event-v2-bridge.ts —— events.listen(...) 把 core EventV2 的每个事件转发到 GlobalBuspayload: {id, type, properties: data}durable 事件额外发一条 {type:"sync", syncEvent:{...}}

B) 实例流 /eventInstanceHttpApiEventApi

  • packages/opencode/src/server/routes/instance/httpapi/handlers/event.tsevents.listen 全量订阅 EventV2然后按 event.location.directory === instance.directory 在服务端过滤WorkspaceRoutingMiddlewaredirectory query/x-opencode-directory 头选中实例),发 server.connected + 10s 心跳,遇 server.instance.disposed 关闭流。

C) v2 精简流 /api/eventpackages/server

  • packages/server/src/handlers/event.tsEventV2.allBounded(events, 256)(有界 dropping 队列,容量 256溢出即断开报错server.connected(类型由 OpenCodeEvent union 合一15s 心跳。

2.2 事件类型

由 schema 中 Event.define 声明,按 manifest 汇总(packages/schema/src/event-manifest.ts

  • session 事件v2 增量式)packages/schema/src/session-event.ts —— session.next.prompted / prompt.admitted / context.updated / synthetic / shell.started|ended / step.started|ended|failed / text.started|delta|ended / reasoning.* / tool.input.*|called|progress|success|failed / retried / compaction.* / revert.* 等约 30 种。Delta 类事件text.delta、reasoning.delta、tool.input.deltalive-only、不入库Ended 类是 replayable 边界。
  • v1 事件(面向现有 TUIpackages/schema/src/v1/session.tsPartDelta/MessageUpdated 等 + permission.asked/repliedv1permission.v2.asked/repliedv2question.asked/repliedtui.*tui-event.ts)、server.connectedserver-event.ts)、插件/集成事件等。
  • 事件体统一形状:{ id: "evt_xx", type, data/properties, location?, durable?: {aggregateID, seq, version}, metadata? }packages/schema/src/event.ts)。

2.3 序号 / offset / 重放语义

核心机制在 packages/core/src/event.tsEventV2 service+ packages/core/src/event/sql.tsSQLite

  • event_sequence(aggregate_id PK, seq, owner_id)event(id PK, aggregate_id, seq, type(版本化), data JSON)uniqueIndex(aggregate_id, seq)
  • publish():若是 durable 事件 → 事务内 更新 seqlatest+1)、先跑 projectorsproject 注册的投影回调,和写库同事务)、再插 EventTable非 durable 事件只走内存 PubSub。
  • 事件有每聚合per-aggregate如 sessionID单调 seqversionschema 演进版本)type 落库时是版本化串(EventV2.versionedType(type, version))。
  • durable({aggregateID, after}) 流:先 readAfter(aggregateID, after) 从 SQLite 重放 seq > after 的历史事件,再 Stream.concat 内存 pubsub 的实时事件(subscribeDurable 用 per-aggregate 的 sliding PubSub 唤醒读库)。→ 断线重连 = 重放历史 + 续传实时,这是"重放事件"路线。
  • v1 老的那套 /global/event/eventlive-only、无 seq 无重放

对外的重放/追平接口v2

  • GET /api/session/:sessionID/event?after=NStreamSse(SessionEvent.Durable)"Replay durable events after an aggregate sequence, then continue with new durable events"packages/protocol/src/groups/session.ts)。
  • GET /api/session/:sessionID/history?limit&after → 分页读 durable 事件SessionHistory
  • 多 agent 协同:/sync/replay/sync/historypackages/opencode/src/server/routes/instance/httpapi/groups/sync.ts)返回 { aggregateID: lastKnownSeq } → seq 之后的事件,用于客户端从同步点追平。

2.4 客户端断线后怎么追数据

  • TUI 主路径v1 事件)packages/tui/src/context/sdk.tsxstartSSE()sdk.global.event(),断了之后指数退避重连1s→30s不重放 —— 因为 /global/event 无历史TUI 靠重新拉取状态补齐:重连后重新 GET session/messages/status实验性sync.start() 开启工作区同步。
  • v2 路径durable 事件):带 after seq 的订阅天然支持重放续传2.3)。
  • 结论:"轻事件总线(全量广播、可丢、无 seq+ 有 seq 的 durable 事件SQLite 持久化、可重放)+ 客户端主动拉状态" 三层组合,视客户端对可靠性的要求选层。

对 Astrion 的借鉴意义

  • Python 侧实现 SSE 事件总线很简单asyncio Queue + EventEmitter 等价物),关键是给事件加 durable 序号并落库,提供 after=N 重放订阅,才能让弱网/多客户端可靠追平。
  • 把"live-only delta"与"durable 终值"分离text.delta 不入库、text.ended 入库可重放)是很好的降本设计。
  • 服务端按 directory/workspace 过滤事件而非客户端过滤减少带宽Astrion 可按 workspace/agent 维度订阅。

3. Session 状态归属:存储、真状态、消息/part 模型

3.1 存哪

SQLitebun:sqlite + drizzle-orm + Effect 封装),单库文件:

  • packages/core/src/database/database.tsPRAGMA journal_mode=WAL; synchronous=NORMAL; busy_timeout=5000; foreign_keys=ON,库文件 Global.Path.data/opencode.dbopencode-${channel}.db)。
  • 表都在 packages/core/src/session/sql.tssessionmessagepartsession_messagev2 投影)、session_inputtodosession_context_epoch;事件表在 packages/core/src/event/sql.ts
  • 老 v1 的会话TUI 现行 API 用的 SessionService)读写 SQLite 的 session/message/part 表;新的 v2 由 SessionProjectorpackages/core/src/session/projector.ts)把 durable 事件投影进 session_message 表。

3.2 谁拥有"真状态"

Servercore 进程内)Sessionpackages/opencode/src/session/session.ts + packages/core/src/v1/session)和 SessionV2packages/core/src/session,事件溯源风格)都在 server 进程的 Effect 服务里;消息/part 以 SQLite 为持久真源事件总线只广播变化客户端TUI/SDK是只读投影。v2 的"真状态"本质是 durable 事件日志SQLite投影表/API 视图都是派生物 —— 事件即真相。所有权通过 EventSequenceTable.owner_id 表达(claim(aggregateID, ownerID)replay 支持 strictOwner),多进程共享 session 时只能有一个 owner 追加事件。

3.3 数据模型长什么样

  • v1 messagepackages/schema/src/v1/session.tsMessageID = "msg_..."message = { id, sessionID, time:{created}, role, ... }Part 是 tagged unionText / Subtask / Reasoning / File / Tool / StepStart / StepFinish / Snapshot / Patch / Agent / Retry / Compactionexport const Part = Schema.Union([...])discriminator "type"。SQLite message.data/part.data 以 JSON 存。
  • v2 session_messagepackages/schema/src/session-message.tsSessionMessage.ID = "msg_..."tagged unionagent-switched / model-switched / user / synthetic / system / shell / step-start / step-finish / reasoning / text / tool / ...,带 time:{created}metadata。投影表 session_message(id, session_id, type, seq, data JSON)uniqueIndex(session_id, seq) —— 消息有每会话 seq和事件 seq 对应
  • v2 session_inputpackages/core/src/session/input.ts + sql.ts):输入先 admitted_seq 落库durable admitpromoted_seq 被 agent 循环取走 —— 输入也是持久化的。

对 Astrion 的借鉴意义

  • "事件日志为真相 + 投影表 + 视图 API"的 CQRS/事件溯源结构在 Python 侧可用 sqlite3/SQLAlchemy + 事件表aggregate_id, seq, type, data实现成本可控。
  • 文件级 Session 与项目隔离(project_id/directory 列)对 Astrion 多 agent、多工作区是现成参考。
  • 输入 admit/promote 两段式(先持久化再消费)能天然解决"请求丢失/重复"问题。

4. 权限 / 审批流:POST /session/:id/permissions/:permissionID

4.1 路由在哪

当前主服务的定义在 packages/opencode/src/server/routes/instance/httpapi/groups/session.ts

SessionPaths = {
  ...
  permissions: `${root}/:sessionID/permissions/:permissionID`,   // root = "/session"
}

handler 在 packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts

const permissionRespond = Effect.fn("SessionHttpApi.permissionRespond")(function* (ctx) {
  yield* requireSession(ctx.params.sessionID)
  yield* permissionSvc.reply({ requestID: ctx.params.permissionID, reply: ctx.payload.response })
    .pipe(Effect.catchTag("Permission.NotFoundError", ...))
  return true
})

repo 中也存在 v2 等价路径 POST /api/session/:sessionID/permission/:requestID/reply,见 packages/protocol/src/groups/permission.ts。)

4.2 审批请求如何产生

审批服务于两代实现机制相同pending map + Deferred 阻塞 + 事件广播):

  • 老 v1packages/opencode/src/permission/index.ts —— ask() 先对 ruleset + approved 求值(evaluate(permission, pattern, ...rulesets)Wildcard.match 匹配 ruleallow/deny/ask 默认 ask需要问人时创建 PendingEntry{info, deferred} 放入内存 pending: Map<ID, PendingEntry>,然后 events.publish(Event.Asked, info) 广播,最后 Deferred.await(deferred) 阻塞住 agent 的工具调用直到有人回复。
  • 新 v2packages/core/src/permission.ts —— PermissionV2.ask()(返回 {id, effect})与 assert()(阻塞式,供 agent 内部用):问人时 create(request, agent){request, agent, deferred} 放进 pending: Map<ID, Pending>events.publish(Event.Asked, request)

4.3 如何推给客户端

通过事件总线广播,不是 HTTP 推送

  • events.publish(Event.Asked, info)EventV2Bridgepackages/opencode/src/event-v2-bridge.ts)→ GlobalBus.emit("event", ...)/global/event SSE 推给所有已连接客户端。
  • TUI 侧:packages/tui/src/context/permission.tsx / feature-plugins/system/notifications.ts 监听 permission.asked 事件弹审批 UI同时 context/sync.tsx 的 case permission.asked/permission.replied 更新本地状态。

4.4 多个客户端同时连接时审批路由给谁

不做"路由",做"共享待办 + 先到先得"

  • pending 表在 server 进程内存Map<ID, Pending/PendingEntry>),所有客户端共享;
  • permission.asked 广播给所有 SS E 连接TUI、web、其他 SDK 客户端都能看到并都能回复);
  • 回复接口按 sessionID + requestID 定位 pending 项(reply()pending.get(input.requestID)),任何客户端 POST 都算;Deferred 只能被 resolve/fail 一次后到的回复找不到请求404 PermissionNotFoundError
  • 不存在"审批钉死给某个 client"的机制 —— 谁先回复谁生效。当多个 TUI 同时开着,requestID 是共享主键service 层不区分连接。
  • 附加:回复 "always" 时保存规则到 permission_saved 表(packages/core/src/permission/saved.ts + sql.ts),并把其他 pending 的同类问题自动放行;"reject" 会级联拒绝同 session 所有 pending。

对 Astrion 的借鉴意义

  • 审批 = "异步请求对象(内存/DB+ 阻塞等待Deferred/Promise+ 事件广播(总线)+ 客户端主动回复HTTP POST",这是 HTTP 世界做 human-in-the-loop 的最小可靠模型。
  • 多客户端审批共享同一请求 ID、先到先得 意味着 Astrion 不需要做"连接路由",只需保证请求对象全局唯一、回复幂等。
  • 事件permission.asked驱动 UI、用 REST 回复的做法,比 RPC 回调更解耦,值得在 gateway 中复用。

5. TUI 与 server 的关系:连接方式与 /tui/* 反向控制

5.1 TUI 进程如何连接 server

两种模式packages/opencode/src/cli/cmd/tui.ts

  1. 内嵌 worker 模式(默认)CLI handler 用 new Worker(file, {...}) 拉起 packages/opencode/src/cli/tui/worker.ts 作为 Bun worker 子进程worker 内 Server.Default().app.fetch 直接处理请求不监听端口URL 伪装成 http://opencode.internal
    • HTTP 请求走 RPCcreateWorkerFetch(client)client.call("fetch", {...}) → worker rpc.fetchServer.Default().app.fetch(request) 返回 Response
    • 事件走 RPC 事件:createEventSource(client)client.on("global.event") 把 worker 里 GlobalBus.on("event") 转发出来的事件喂给 TUI 的 EventSource 接口。
  2. 外部模式(--port/--hostname/--mdns:先 client.call("server", network) 让 worker 真正 Server.listen() 起 HTTP 端口TUI 用真实 URL + Authorization 头(ServerAuth.headers()packages/opencode/src/server/auth.tsOPENCODE_SERVER_PASSWORD)直连。

TUI 内全部通过生成的 SDK@opencode-ai/sdk/v2createOpencodeClient)访问 APIpackages/tui/src/context/sdk.tsxstartSSE()sdk.global.event() 开 SSE见 §2.4 的重连逻辑)。

5.2 /tui/* 反向控制接口的设计意图

定义:packages/opencode/src/server/routes/instance/httpapi/groups/tui.tsTuiPaths/tui/append-promptopen-helpopen-sessionsopen-themesopen-modelssubmit-promptclear-promptexecute-commandshow-toastpublishselect-sessioncontrol/nextcontrol/response)。

意图:把 TUI 当作一个可被 server 及任何客户端web、插件、MCP、Agent控制的"界面设备",实现方式不是私有的进程内回调,而是两条标准通道

  1. 事件通道(多数 /tui/ 端点)*handlerhandlers/tui.ts)并不直接调 TUI 内部函数,而是 events.publish(TuiEvent.PromptAppend / CommandExecute / ToastShow / SessionSelect, ...) 把"UI 指令"作为普通事件发布到事件总线TUI 作为 SSE 消费者收到 tui.toast.showtui.command.executecommand 恒为 session.listhelp.showmodel.list 等字符串命令)后自己执行弹窗/切换。例如:
    • openHelppublishCommand("help.show")
    • openSessionspublishCommand("session.list")
    • MCP 认证失败时 server 自己 events.publish(TuiEvent.ToastShow, {title:"MCP Authentication Required", ...})packages/opencode/src/mcp/index.ts)—— 同一通道、任意调用方。
  2. 请求/响应队列通道(/tui/control/next + /tui/control/responsepackages/opencode/src/server/shared/tui-control.ts 用两个模块级 AsyncQueuepackages/opencode/src/util/queue.ts,阻塞式队列)实现 submitTuiRequest({path, body}) / nextTuiRequest() / submitTuiResponse(body) / nextTuiResponse()TUI 拉取 control/nextlong-poll执行、POST control/response 交回结果。适合"必须拿到返回值"的 UI 操作(如 TUI 弹一个选择框server 等它的结果)。

结论/tui/* = "通过标准 HTTP 接口把事件塞进总线、由 TUI 自主消费",使 TUI 与 server 彻底解耦 —— 同一台 server 可以同时被 TUI、桌面端、web 端、Agent 进程控制,且 UI 指令事件天然对所有客户端可见。

对 Astrion 的借鉴意义

  • "UI 即客户端设备、指令走事件总线、回执走请求队列"是 gateway 化后"远程控制本地交互界面"的标准答案Astrion 的"弹确认框/提示"可以由任意调用方发布指令事件,前端订阅执行。
  • 内嵌 worker + RPC 屏蔽 HTTP 与进程内调用的差异(rpc.fetch 模式),方便单元测试与本机零端口运行;对外则暴露真实端口 + 密码认证。Astrion 可在"进程内 gateway"与"独立 gateway 服务"之间无缝切换。

6. 实例模型:单实例单项目 vs 多项目多会话

是"单 server 进程 + 多 project 实例 + 每目录懒加载"

  • instance 概念packages/opencode/src/project/instance-store.tsInstanceStore.load({directory, ...}) —— 每个**目录directory**是一个 InstanceContext {directory, worktree, project}server 进程用 cache: Map<string, Entry> 按目录缓存懒加载的实例;实例内包含一套完整 core 服务Session/Permission/EventV2 等 Effect 层,AppNodeBuilder 组装)。
  • 路由如何选实例WorkspaceRoutingMiddlewaremiddleware/workspace-routing.ts)读 query 的 directory/workspace 或 header x-opencode-directoryInstanceContextMiddlewaremiddleware/instance-context.tsstore.load({directory}) 把请求路由到该目录的实例上下文(InstanceRef。SDK 客户端可在创建时传 directorypackages/sdk/js/src/v2/client.ts 自动加 x-opencode-directory 头)。
  • opencode serve 的注释直接说明:"Server loads instances per-request via x-opencode-directory header — no need for an ambient project InstanceContext at startup."packages/opencode/src/cli/cmd/serve.ts
  • workspace实验性WorkspaceV2control-plane 的 workspace.ts)在 directory 之上再加一层,WorkspaceRouteContext {directory, workspaceID};事件按 location.directory + workspaceID 过滤。
  • 会话session挂在 projectSessionTable.project_id)下,同一 server 可同时服务多个 project/session每个项目有自己的 SQLite 数据(同库分目录),隔离靠 directory/project 列与实例上下文。
  • 多实例并发时,单写者原则由 EventSequenceTable.owner_id + claim/strictOwner 保证§3.2)。

对 Astrion 的借鉴意义

  • Astrion gateway 可以是"单进程多 workspace 懒加载实例"而非"一项目一进程"进程常驻、按请求头路由实例上下文大幅简化部署实例级状态session、事件流天然隔离。
  • 用请求头/query 选实例的方案(x-opencode-directory)可作为 Astrion 多租户路由的样板。

opencode 设计要点速查表

# 要点 一句话
1 框架 Effect HttpApi/HttpRouter(非 HonoNode http 底层,OpenApi.fromApi 代码生成 OpenAPI 3.1
2 SDK bun dev generate → openapi.json → @hey-api/openapi-ts → TS SDKfetch client + SSE 类型)
3 Server 分层 @opencode-ai/protocol(协议/group + @opencode-ai/servermiddleware/handler 注入) + @opencode-ai/core(领域服务)
4 事件总线 GlobalBusEventEmitter/global/event SSE全量广播、live-only、10s 心跳);/event 按 directory 过滤
5 可靠事件 EventV2SQLite event/event_sequence 表 + per-aggregate seq + versiondurable(after) 先重放后续传
6 session 真状态 Server 进程 + SQLiteWAL消息/part 是 JSON 投影v1 message/part 表、v2 session_message 表),事件日志即真相
7 消息模型 Message/Part 都是 taggged uniontype 判别v2 session_message 带 per-session seq
8 审批流 pending Map + Deferred 阻塞等待 + permission.asked 事件广播;按 requestID 先到先得回复,无连接路由
9 TUI 关系 TUI=瘦客户端:内嵌 Bun worker + RPC fetch/事件,或 --port 真 HTTP + 密码认证;/tui/* 把 UI 指令发布为事件、TUI 自主消费
10 实例模型 单进程多项目:按 directory/x-opencode-directory 头懒加载 InstanceContext实例内一套 core 服务;单写者由事件 ownership 保证