Codex 版 HappyClaw 运行时——把 HappyClaw 的 Agent 执行层从 Claude Agent SDK 迁移到 codex app-server(JSON-RPC over stdio)的运行时实现。
引擎底座(Stage 0-4):协议契约 + app-server 客户端 + 流式事件映射 + thread/turn 注入循环 + 17 个工具(dynamicTools)+ 多租户编排(per-folder 隔离 / resume / 单写者)+ 针对真实 codex 的 PoC 验证。基线 codex-cli 0.137.0。
完整应用(A 阶段已搬迁):在引擎底座之上,HappyClaw 的应用层(Web 前后端、五渠道 IM、 调度、计费、权限)已忠实搬迁为 codex-only 版,并叠加了 codex 特有的 per-user 登录与自定义 模型 provider。MVP 端到端(Web 登录→发消息→codex 执行→流式回复→重启 resume,host + Docker 双轨)已验证。
上游同步台账见
docs/UPSTREAM-SYNC.md;per-user 登录/provider 细节见docs/CODEX-PERUSER-AUTH.md。本 README 主体描述引擎底座(Stage 0-4) 的协议/架构;下方「运行完整应用」给出起 Web 服务的命令。
HappyClaw 用 agent-runner 在容器/宿主机进程里调用 Claude Agent SDK,把流式事件
经 OUTPUT_MARKER 包裹写 stdout,主进程解析后广播到 Web/IM。happycodex 把这条执行
管道的「引擎」换成 codex app-server:
- 协议层:codex app-server 是一个长驻子进程,走 newline-delimited JSON-RPC(省略
"jsonrpc":"2.0"头)。thread = 会话,turn = 一轮对话,token 增量通过item/agentMessage/delta等通知流式推送。 - 复用点:codex 的流式通知映射到 HappyClaw 既有的
StreamEvent形状(shared/stream-event.ts), 再经同一套OUTPUT_MARKER→ WebSocketstream_event管道吐给前端,前端 chat store 无需改动。
| HappyClaw | happycodex | 说明 |
|---|---|---|
container/agent-runner/ |
src/runtime/codex-runner.ts |
执行引擎:消费输入 + IPC 注入,驱动会话,吐 StreamEvent |
Claude Agent SDK query() |
src/runtime/session.ts(thread/turn) |
单会话生命周期 + 注入循环 |
| SDK 内部 spawn cli.js | src/appserver/client.ts |
spawn codex app-server,JSON-RPC 分帧/关联/背压重试 |
shared/stream-event.ts |
src/shared/stream-event.ts |
StreamEvent 类型(语义对齐,冻结) |
OUTPUT_START/END_MARKER 流式管道 |
同名 marker(shared/stream-event.ts) |
runner → stdout → 主进程解析,协议复用 |
| SDK 流式事件 → StreamEvent | src/runtime/stream-mapper.ts |
把 app-server 通知映射成 0..N 个 StreamEvent(纯函数) |
┌─────────────────────────────────────────────────────────────┐
│ codex app-server (子进程, JSON-RPC over stdio) │
└───────────────▲───────────────────────────┬─────────────────┘
请求/通知/审批 │ │ 增量通知(delta / item / turn)
┌───────────────┴───────────────────────────▼─────────────────┐
│ src/appserver/client.ts AppServerClient │
│ spawn + 行分帧 + id 关联 + -32001 背压重试 + 通知/请求分发 │
└───────────────▲───────────────────────────┬─────────────────┘
│ request/notify │ onNotification
┌───────────────┴───────────────────────────▼─────────────────┐
│ src/runtime/session.ts ThreadSession │
│ thread/start|resume · turn/start|steer · activeTurnId 维护 │
│ │ 通知 → StreamMapper → onStreamEvent │
│ ▼ │
│ src/runtime/stream-mapper.ts StreamMapper(纯映射) │
│ app-server 通知 → StreamEvent[] │
└───────────────▲───────────────────────────┬─────────────────┘
│ onStreamEvent │
┌───────────────┴───────────────────────────▼─────────────────┐
│ src/runtime/codex-runner.ts CodexRunner │
│ 消费 CodexRunnerInput + IPC 注入,StreamEvent 经 │
│ OUTPUT_MARKER 包裹写 sink(默认 stdout) │
└──────────────────────────────────────────────────────────────┘
| 模块 | 职责 |
|---|---|
src/contracts.ts |
冻结:跨模块公共契约(接口 + 构造签名),各实现针对它编程 |
src/appserver/protocol.ts |
冻结:codex app-server JSON-RPC 协议子集(手工策展,对齐 generate-ts 产物) |
src/appserver/client.ts |
AppServerClient:spawn 进程、JSON-RPC 分帧/关联、背压重试、握手、通知/请求分发 |
src/runtime/stream-mapper.ts |
StreamMapper:单条 app-server 通知 → 0..N 个 StreamEvent(无副作用,可单测) |
src/runtime/session.ts |
ThreadSession:thread/turn 生命周期;sendUserMessage 自动判定 turn/start vs turn/steer |
src/runtime/codex-runner.ts |
CodexRunner:agent-runner 等价物,OUTPUT_MARKER 包裹写 stdout;可挂工具层 |
src/runtime/tools/types.ts |
冻结:工具层契约(ToolBridge / ToolDefinition / IToolRegistry / IToolDispatcher) |
src/runtime/tools/registry.ts |
ToolRegistry:聚合工具、产出 dynamicTools schema、按名分发(never throws) |
src/runtime/tools/builtin.ts |
createBuiltinTools():HappyClaw 12 个工具的 dynamicTools 定义(schema + handler) |
src/runtime/tools/ipc-bridge.ts |
IpcToolBridge:工具副作用落成 HappyClaw 风格 IPC 文件 + 记忆文件操作 |
src/runtime/tools/dispatcher.ts |
ToolDispatcher:item/tool/call server 请求 → registry.dispatch → respond |
src/runtime/multitenant/types.ts |
冻结:多租户层契约(ISessionManager / ISessionStore / ICodexHomeProvisioner / ISerialQueue) |
src/runtime/multitenant/session-manager.ts |
SessionManager:编排 N 个 folder(per-folder app-server、resume、idle 回收、LRU 驱逐、可选工具) |
src/runtime/multitenant/session-store.ts |
SessionStore:folder→threadId JSON 持久化(映射 HappyClaw sessions 表,原子写) |
src/runtime/multitenant/codex-home.ts |
FsCodexHomeProvisioner:per-folder CODEX_HOME + 复制共享 auth(幂等、路径安全) |
src/runtime/multitenant/serial-queue.ts |
SerialQueue:per-key 单写者闸门(防 rollout 无锁并发损坏,对应 group-queue) |
src/shared/stream-event.ts |
冻结:StreamEvent 类型 + OUTPUT_MARKER 常量(对外流式协议单一真相源) |
src/poc/poc-stream.ts |
PoC R1:token 流式增量 |
src/poc/poc-steer.ts |
PoC R2:运行中注入(turn/steer) |
src/poc/poc-resume.ts |
PoC:跨进程 thread/resume 续接 |
src/poc/poc-tools.ts |
PoC R3:12 个工具走 dynamicTools,端到端往返 + 副作用落地 |
src/poc/poc-multitenant.ts |
PoC Stage 4:2 个 folder 并发,隔离 + 并发 + 跨进程 resume |
protocol/ts/、protocol/schema/ |
codex 0.137.0 generate-ts / generate-json-schema 全量参考产物(不入编译) |
- 安装 codex CLI,版本对齐基线
0.137.0(可用codex --version确认)。 - codex 认证:
- 共享/默认账号(首次部署的基线):服务端
codex login(或codex login --api-key)一次, 凭据落~/.codex/auth.json。它满足首次 setup 门控、并作为未自登用户的回退账号。 - per-user 自登(启动后,各用户在 Web 界面):进「设置 → Codex 账号」用自己的 codex 账号
登录——支持「浏览器登录」(本机推荐,自动开浏览器)/「设备码登录」(远程)/「API key」/
「access-token」。无需预先在终端
codex login。登录态按用户隔离(data/config/user-im/{userId}/codex/)。 - 也可在「Codex 配置」里配第三方模型 provider(兼容 OpenAI Responses API 的 GLM 等)。
- 注意:首次 setup 门控目前仍看共享账号,故全新机器至少需要一次服务端
codex login来过 setup;之后各用户即可在 UI 里登录自己的账号。
- 共享/默认账号(首次部署的基线):服务端
CODEX_HOME:指向有效的 codex 配置目录。PoC 默认用当前环境的CODEX_HOME;poc-resume的两次 client 必须共享同一CODEX_HOME,否则第二个进程读不到第一个 写入的 rollout,无法续接。(完整应用为每个 folder provision 独立的 per-folderCODEX_HOME。)- 安装依赖:
npm install。
# 类型检查(tsc --noEmit)
npm run typecheck # 或:make typecheck
# 单测(vitest,纯逻辑模块:stream-mapper / client 分帧等)
npm run test # 或:make test
# PoC(针对真实 codex app-server,需登录态 + CODEX_HOME)
npm run poc:stream # 或:make poc-stream —— R1 token 流式
npm run poc:steer # 或:make poc-steer —— R2 运行中注入 turn/steer
npm run poc:resume # 或:make poc-resume —— 跨进程 thread/resume 续接
npm run poc:tools # 或:make poc-tools —— R3 dynamicTools 工具端到端
npm run poc:multitenant # 或:make poc-multitenant —— Stage 4 多租户隔离/并发/resumePoC 会话统一用 { approvalPolicy: 'never', sandbox: 'read-only' }:不触发审批回环阻塞、
不写磁盘。每个脚本结尾打印 OK / UNCERTAIN / FAIL 结论并按结果 process.exit。
# 1) 构建前端(首次 / 改前端后):产物 web/dist 由后端静态托管
cd web && npm run build && cd ..
# 2) 起后端(host 模式 tsx 直跑;WEB_PORT 自定端口,避免与同机其它服务冲突)
WEB_PORT=3100 npm run dev # 生产可 npm run build → node dist/index.js浏览器打开 http://localhost:<WEB_PORT> → 首次按向导建管理员(setup)→ 进「设置 → Codex 账号」
登录你自己的 codex(浏览器/设备码/API key),即可发消息让 codex 执行。
同机若另跑了 happyclaw,给 happycodex 用不同端口即可(session cookie 已用专属名
happycodex_session,与 happyclaw 互不顶替)。
- 协议契约冻结(
contracts.ts/protocol.ts/stream-event.ts/tools/types.ts/multitenant/types.ts)。 AppServerClient:JSON-RPC over stdio、-32001背压指数退避重试、握手、通知/请求分发。StreamMapper:app-server 通知 → StreamEvent 纯映射。ThreadSession:thread/turn 生命周期 + 运行中注入(turn/start vs turn/steer 自动判定)。CodexRunner:OUTPUT_MARKER 流式管道(对齐 HappyClaw agent-runner)+ 可挂工具层。- Stage 3 工具层:12 个 HappyClaw 工具走
dynamicTools(注册 +item/tool/call客户端代理执行)。 - Stage 4 多租户(standalone):
SessionManager编排 N 个 folder——per-folderCODEX_HOME隔离(共享单账号,auth 复制进各 home,对齐 HappyClaw 全局凭据 + 配置目录隔离模型)、SessionStore持久化 folder→threadId(resume 续接)、SerialQueue单写者闸门、idle 回收 + LRU 驱逐。 - 五个 PoC 针对真实 codex app-server 验证 R1/R2/resume/tools/multitenant。
- 接 HappyClaw 主仓:改 HappyClaw
container-runner用codex-runner、IpcToolBridge对接真实 主进程 IPC/task-scheduler/memory(含listTasks真状态)、映射真实sessions表——是最终目标, 跨两个仓库,留待后续。 - 共享单账号 token 刷新集中化:standalone 下各 per-folder
CODEX_HOME独立 refresh 可能竞争; 生产应集中一个 refresh owner(随"接主仓"一起做)。 - provider failover(P0):HappyClaw 的多账号失效转移在 codex 架构下作废,不重做。
- provider failover 作废(P0):HappyClaw 的 provider 限额切换/上下文恢复机制在 codex 架构下作废——codex app-server 自管 provider,不在 happycodex 运行时重做。
- 容器化 / Docker 卷挂载、IM 通道、Web 前端:均属 HappyClaw 主仓既有能力,本仓库只替换执行引擎。
- ESM + NodeNext:所有相对 import 必须带
.js扩展名(即使源文件是.ts)。 - 严格 TS:
strict+noUncheckedIndexedAccess+noImplicitOverride,避免any滥用。 - 冻结契约:
src/contracts.ts、src/appserver/protocol.ts、src/shared/stream-event.ts是冻结文件,修改 = 改公共 API,需同步所有实现方。 - 升级 codex 后:
npm run protocol:regen重新生成protocol/,再对照protocol.tsdiff。