KC

kkarsyline/codex-app-server-gateway-guide

开发工具
34 stars 0 forks 质量 45 趋势 45

将 Codex App Server 接入现有聊天前端与网关的实践教程,涵盖流式事件、thread 生命周期、refill、多后端路由与 MCP 工具同步。

概览

文档状态:2026-08-22。本文聚焦 Codex;API 与 Claude Code -p 网络上有较多相关教程,这仅讲兼容边界。 本文在 gateway 的 mode 值中使用 codex,对应官方 Codex App Server 集成。claude-p 保留原名,因为它对应真实的 claude -p 执行模式。 这套方案适合已经拥有聊天前端、SSE 网关、会话数据库和自定义工具系统的人。最终效果是:同一前端可以按 conversation 在 API、Claude Code -p、Codex App Server 三条后端之间选择;三条后端共用消息存储、附件、流式 UI 和工具气泡;各 provider 的会话语义、提示词载体与原生事件留在各自 adapter 内。 1. gateway 负责产品语义:conversation、消息树、附件、持久化、统一 SSE、后台任务。 2. provider adapter 负责协议语义:启动、认证、session/thread、工具事件、错误和中断。 3. 工具注册拥有单一真源,再为 CC 与 Codex 渲染各自配置;不要维护两份人工列表。 4. persona、style、记忆检索和 refill 各有独立职责;混成一个巨大 system prompt 会让更新、缓存与会话恢复一起失控。 本文把 refill 定义为“在新建、恢复、分叉或 compact 后,为 Codex thread 回填必要的历史上下文”。gateway 从自己的消息库中按预算取出历史消息摘要、最近几组完整对话和必要状态,组成一个临时 context block,放进下一次 turn/start。refill 只填当前 thread 缺少的部分,不重复整份 transcript,也不替代 Codex 原生的 thread history。 OpenAI 将 App Server 定位为富客户端集成接口,覆盖认证、conversation history、approvals 和 streamed agent events。自动化或 CI 更适合 SDK。官方 App Server 文档 我们选择一个长期运行的 App Server 进程服务多个逻辑 conversation。

README

把 Codex 接进现有聊天前端:App Server 网关适配、三引擎切换与 MCP 工具同步

文档状态:2026-08-22。本文聚焦 Codex;API 与 Claude Code -p 网络上有较多相关教程,这仅讲兼容边界。

本文在 gateway 的 mode 值中使用 codex,对应官方 Codex App Server 集成。claude-p 保留原名,因为它对应真实的 claude -p 执行模式。

这套方案适合已经拥有聊天前端、SSE 网关、会话数据库和自定义工具系统的人。最终效果是:同一前端可以按 conversation 在 API、Claude Code -p、Codex App Server 三条后端之间选择;三条后端共用消息存储、附件、流式 UI 和工具气泡;各 provider 的会话语义、提示词载体与原生事件留在各自 adapter 内。

1. 最终架构

flowchart LR
    UI[Chat UI] -->|HTTP + SSE| GW[Chat Gateway]
    GW --> DB[(Conversation DB)]
    GW --> R{conversation.mode}
    R -->|api| API[API Provider]
    R -->|claude-p| CCP[Claude Code Adapter]
    R -->|codex| CXP[Codex App Server Adapter]
    CCP --> CC[Claude Code process/session]
    CXP --> AS[Persistent Codex App Server]
    AS --> T1[Codex thread A]
    AS --> T2[Codex thread B]
    REG[Canonical Tool Registry] --> RCC[CC renderer/consumer]
    REG --> RCX[Codex renderer/consumer]
    RCC --> CC
    RCX --> AS
    MEM[Optional memory service] -->|MCP or bounded injection| GW
    MEM -->|MCP| CC
    MEM -->|MCP| AS

最重要的边界有四条:

  1. gateway 负责产品语义:conversation、消息树、附件、持久化、统一 SSE、后台任务。
  2. provider adapter 负责协议语义:启动、认证、session/thread、工具事件、错误和中断。
  3. 工具注册拥有单一真源,再为 CC 与 Codex 渲染各自配置;不要维护两份人工列表。
  4. persona、style、记忆检索和 refill 各有独立职责;混成一个巨大 system prompt 会让更新、缓存与会话恢复一起失控。

本文把 refill 定义为“在新建、恢复、分叉或 compact 后,为 Codex thread 回填必要的历史上下文”。gateway 从自己的消息库中按预算取出历史消息摘要、最近几组完整对话和必要状态,组成一个临时 context block,放进下一次 turn/start。refill 只填当前 thread 缺少的部分,不重复整份 transcript,也不替代 Codex 原生的 thread history。

2. 为什么选 App Server

Codex 有三类常见接入面:

接入面 适合场景 长聊天前端中的局限
codex exec --json CI、脚本、一次性自动化 每轮进程与历史管理成本高,深度交互能力有限
Codex SDK 应用内编程调用、希望使用类型封装 很合适;仍需自行定义 gateway 事件与持久化契约
codex app-server 自定义富客户端、完整 thread/turn/event/approval 集成 协议面较宽,需要严谨 adapter 与版本测试

OpenAI 将 App Server 定位为富客户端集成接口,覆盖认证、conversation history、approvals 和 streamed agent events。自动化或 CI 更适合 SDK。官方 App Server 文档

我们选择一个长期运行的 App Server 进程服务多个逻辑 conversation。这样做可以减少反复启动成本,也便于集中处理认证、模型目录、MCP 初始化与全局通知。每个前端 conversation 仍绑定独立 Codex thread,adapter 必须维护 ownership,严禁把一个 thread id 同时交给两个窗口。

3. 先冻结你自己的网关契约

先让前端只认识一套 provider-neutral contract。一个够用的最小集合如下:

{"event":"start","conversation_id":"conv_demo","session_action":"resume"}
{"event":"session_bound","provider":"codex","session_id":"opaque-thread-id"}
{"event":"thinking_delta","text":"正在检查……","reasoning_source":"summary"}
{"event":"delta","text":"结论是……","phase":"final_answer"}
{"event":"tool_call_start","tool_call_id":"item_1","tool_name":"search","arguments":null,"tool_round":1}
{"event":"tool_call_result","tool_call_id":"item_1","tool_name":"search","arguments":{"q":"demo"},"result":"...","tool_round":1}
{"event":"status","kind":"rate_limit","detail":{}}
{"event":"server_request","request_id":"req_1","request_type":"user_input","payload":{}}
{"event":"done","text":"完整正文","usage":{}}

重要的细节:

  • session_bound 在 native thread 创建或恢复成功后立即发出,gateway 收到后再落库。
  • tool start 时参数可能尚未完整,UI 应允许 arguments: null,完成事件再回填。
  • done.text 是整轮最终文本,方便断线恢复和一致性校验;前端实时展示仍消费 delta。
  • reasoning 只展示官方提供的 summary 或明确可见的 reasoning text。不要把隐藏 chain-of-thought 写进产品承诺。
  • provider 原生 id 只在 adapter 与数据库绑定层流转;公开日志可使用哈希或业务侧 id。

4. Codex runtime 与认证

4.1 固定版本,保存校验信息

App Server 仍在快速演化。部署时应固定 Codex 版本,并记录:

  • CLI 版本;
  • 安装包来源;
  • artifact SHA-256;
  • 该版本生成的 App Server schema;
  • adapter 测试所用 fixture 版本。

升级流程应先生成/比较 schema,再跑 adapter parity tests,最后做真实 E2E。生产验证过某一版,不代表 main 分支或半年后的 CLI 会保持所有字段形状。

本文实际工程曾锁定并验证 Codex 0.148.0。当前官方文档已经展示更丰富的 thread 字段和新能力,因此示例使用 opaque thread id,不使用 UUID 正则。若你的固定版本确实只返回 UUID,可以在版本专属 adapter 内收紧校验。当前官方 Python SDK 仍公开 base_instructions / developer_instructions,并在 wire 上映射到 camelCase;这两个字段可在 SDK API referenceSDK FAQ 核对。App Server 网页的简短 start 示例并非完整 schema。

4.2 认证目录必须持久化

远程或无头环境可以使用:

codex login --device-auth
codex login status

官方也支持先在有浏览器的机器登录,再复制 ~/.codex/auth.json。该文件包含访问令牌,权限应设为 0600,放进持久卷,绝不能进入 Git、日志、issue 或教程截图。官方认证文档

使用 ChatGPT 登录身份时,启动 App Server 前清掉会偷偷改变 provider 或计费身份的环境变量,例如:

for key in (
    "OPENAI_API_KEY",
    "CODEX_API_KEY",
    "CODEX_ACCESS_TOKEN",
    "OPENAI_BASE_URL",
    "OPENAI_API_BASE",
):
    child_env.pop(key, None)

若你明确选择 API key 模式,则反过来:把凭据放进 secret manager 或受限 env 文件,并在 health/log 中只输出“存在/缺失”,永远不输出值。

5. 启动 App Server 与 initialize 握手

最容易调试的 transport 是 stdio JSONL:

codex app-server --listen stdio://

父进程需要同时读取 stdout 和 stderr。stdout 每行是一条省略 jsonrpc: "2.0" 的 JSON-RPC 消息;stderr 只能当诊断日志,别混进协议 parser。

连接建立后顺序固定:

{"method":"initialize","id":1,"params":{"clientInfo":{"name":"my_gateway","title":"My Gateway","version":"0.1.0"}}}
{"method":"initialized","params":{}}

任何 thread 请求都应等 initialize 成功。父进程需要维护:

  • 自增 request id;
  • request_id -> Future 的 pending map;
  • thread_id -> Queue 的通知路由;
  • reader task 与 stderr task;
  • App Server epoch;
  • 进程退出时对全部 pending future/queue 的失败广播。

精简实现见 examples/codex_app_server_bridge.py

6. conversation 与 Codex thread 生命周期

数据库至少保存这些字段:

CREATE TABLE codex_window_sessions (
  conversation_id TEXT PRIMARY KEY,
  codex_thread_id TEXT UNIQUE,
  codex_session_id TEXT,
  runtime_hash TEXT,
  resume_safe INTEGER NOT NULL DEFAULT 1,
  generation INTEGER NOT NULL DEFAULT 0,
  last_completed_message_id TEXT,
  compact_refill_pending INTEGER NOT NULL DEFAULT 0,
  last_error TEXT,
  updated_at TEXT NOT NULL
);

当前 App Server 还可能返回 thread.sessionId:root thread 通常以自己的 thread id 作为 session tree root,forked thread 会保留 root session id。codex_thread_id 负责具体 conversation ownership;codex_session_id 用于 fork 树审计和跨 thread 关联。固定版本没有该字段时保留 NULL,不要自行推导。

resume_safe 是关键状态位:

  • turn 已被 App Server 接受后置为 0
  • 收到同一 turn 的 turn/completed 并完成持久化后置为 1
  • 连接在“请求可能已送达、响应未知”的窗口断掉时保持不安全;
  • 不安全状态禁止盲目重试,否则可能产生双回复。

6.1 fresh

{"method":"thread/start","id":10,"params":{
  "cwd":"/workspace",
  "approvalPolicy":"never",
  "sandbox":"workspaceWrite",
  "personality":"none",
  "model":"YOUR_PINNED_MODEL",
  "baseInstructions":"",
  "developerInstructions":""
}}

字段名和 sandbox 枚举要以你的固定版本 schema 为准。当前 SDK 使用 snake_case Python 参数并在 wire 上转换为 camelCase;旧版与新版的 sandbox 结构也可能不同。

6.2 warm

同一 App Server epoch 内,conversation 已绑定 thread,且 runtime fingerprint 未变化时,直接在现有 thread 上 turn/start。这一条不重复执行完整 refill。

6.3 cold resume

App Server 重启、逻辑 session 被 idle reaper 回收,或 adapter 重新加载时:

{"method":"thread/resume","id":11,"params":{
  "threadId":"opaque-thread-id",
  "baseInstructions":"",
  "developerInstructions":""
}}

恢复成功后返回的 thread id 必须与目标一致。恢复时配置覆盖的首轮生效语义曾在部分版本出现过问题;生产系统应有“改变 style 后立即 cold resume”的回归用例,不能只测试 fresh。

6.4 fork

{"method":"thread/fork","id":12,"params":{
  "threadId":"source-thread-id",
  "baseInstructions":"",
  "developerInstructions":""
}}

新 thread id 必须与 source 不同。fork 期间锁住 source ownership 与 target slot,避免 source 正在前进时从错误 tip 分叉。若产品允许“从任意历史消息分支”,数据库的 source tip 和 native transcript 必须精确对齐;对齐证明不足时退回 fresh + frozen hydration。

6.5 runtime fingerprint

将这些值稳定序列化并哈希:

model
+ reasoning effort
+ identity prompt hash
+ output style hash
+ MCP registry digest
+ provider-specific config version

fingerprint 变化时轮换逻辑 session,随后 resume/fork/fresh。继续把旧 warm thread 当成同一 runtime,会出现“前端已经改了 persona,模型还在读旧内容”的幽灵状态。

7. persona、style 与上下文窗口

推荐把提示词分为四层:

内容 Codex 载体
Identity persona、用户关系背景、长期身份事实 baseInstructions
Style 语气、格式、输出偏好 developerInstructions
Turn context 当前时间、附件说明、动态状态 当前 turn/start.input
Refill 历史消息摘要、最近完整 exchanges、分支上下文 fresh/resume/fork/compact 时的受限输入块

personality 建议设为 none,由你自己的 style 完成输出控制。若希望保留 Codex 内置 personality,就把它纳入 runtime fingerprint,并做冲突测试。baseInstructions / developerInstructions 属于官方协议/SDK 字段;“identity 放前者、style 放后者”是本文 adapter 的映射策略,仍需对目标版本生成的 schema 与 fresh/resume/fork 首轮行为做测试。

7.1 为什么要把 style 单独拿出来

  • 修改文风无需重建 identity 文本;
  • 日志能分别显示 persona hash 和 style hash;
  • API、CC、Codex 可以共用同一个前端 prompt_blocks.style,adapter 再映射到各自载体;
  • provider 迁移时,关系身份和输出格式不会互相污染。

7.2 “start hook/启动提示”怎么处理

若你的项目在 thread 启动时加入自定义提示模板,把它当成一段可替换的 prompt resource:

  • 不把模板硬编码进 adapter 状态机;
  • 为有模板/无模板各跑一套功能回归;
  • 保留工具调用、MCP、compact、resume、approval 的 canary;
  • 模板失效时只替换资源层,不改协议层。

8. turn/start 与输入格式

{"method":"turn/start","id":30,"params":{
  "threadId":"opaque-thread-id",
  "input":[
    {"type":"text","text":"用户消息"},
    {"type":"localImage","path":"/workspace/uploads/image.png"}
  ],
  "effort":"high",
  "summary":"detailed"
}}

输入 item 的类型随协议版本变化。把附件转换集中在一个函数里,并对未知类型 fail closed。远程前端上传文件后,seat 只能访问受控共享卷内的副本;不要接受用户直接传宿主绝对路径。

refill context 可以垫在当前用户消息前:


  ...
  ...

full refill 只用于 fresh 或无法安全恢复的情况。cold resume、fork 和 compact 后使用更小的 light refill;warm turn 不执行 refill。

9. App Server 事件翻译

App Server 的 item/completed 应作为该 item 的权威完成态;delta 用于实时 UI。当前官方事件说明见 App Server events

App Server 事件/Item Gateway 事件 处理要点
item/agentMessage/delta delta 同 item 按顺序拼接
item/reasoning/summaryTextDelta thinking_delta 稳定的可读 reasoning summary 通路
item/reasoning/textDelta thinking_delta 只在模型支持时出现,不作产品保证
item/started + mcpToolCall tool_call_start 记录 item id、tool、server、round
item/completed + tool item tool_call_result 最终参数与结果从 completed item 回填
item/commandExecution/outputDelta status/tool_progress itemId 顺序拼接;适合可折叠进度,不必写入正文
contextCompaction item completed status/compact_boundary 给下一轮设置 refill pending
thread/tokenUsage/updated 内部 usage snapshot done 时附上最终快照
account/rateLimits/updated status/rate_limit 只持久化安全字段
turn/completed doneerror 核对 turn id、status、error

9.1 别把 reasoning summary 写成“完整 CoT”

官方说明 summaryTextDelta 提供 readable reasoning summaries;textDelta 只在模型支持时出现。教程、UI 和 marketing 都应使用“reasoning summary/思考摘要”这一表述。隐藏推理链没有稳定提取契约。

9.2 server request 是双向协议

App Server 可能向 client 发起 approval、user input 或 MCP elicitation 请求。adapter 要区分:

  • 可由固定策略自动回答的请求;
  • 必须转发前端、等待用户输入的请求;
  • 该产品明确不支持、应返回 JSON-RPC error 的请求。

转发前端时记录 request 所属 thread/conversation。用户回填时再次校验 ownership,防止 A 窗口替 B 窗口批准操作。

10. 客户端断线之后继续排空

移动端切后台、网络抖动和用户离开页面都可能切断 SSE。若 gateway 的 adapter HTTP client 随前端断线一起关闭,native agent 仍可能继续工作,后半段正文和工具结果却无人读取。

稳定做法:

  1. gateway 创建独立 producer task 读取 adapter;
  2. producer 在收到 start 后插入一条空 assistant placeholder;
  3. 每隔几秒原地覆写已累计正文、thinking 与 tool log;
  4. 前端 SSE 只是 producer 的订阅者;
  5. 订阅者断开时 producer 继续读到 turn/completed
  6. 最终一次原子写回并标记 session resume-safe。

adapter 自己也要有 detached drain:若 gateway 到 adapter 的 HTTP 连接中断,而 turn/start 已送达,adapter 继续消费该 thread queue 到 terminal event,再释放 lifecycle lock。这样下一条消息不会与仍在运行的 turn 叠到同一 thread。

11. 三引擎自由切换

conversation 创建时固定 mode

api | claude-p | codex

已有 conversation 的 mode 不允许被普通 chat 请求偷偷覆盖。若用户明确切换 provider,建议创建新 conversation 或执行显式 migration 流程;两种 native transcript 不能假装拥有同一条隐形历史。

路由骨架见 examples/provider_router.py。三条路径共享:

  • conversation/message/attachment 表;
  • parent/active-leaf 规则;
  • 前端 SSE event contract;
  • placeholder 与 detached persistence;
  • memory preflight 的接口;
  • 历史消息摘要生成器;
  • tool UI 与 tool audit schema。

各自保留:

能力 API Claude Code -p Codex App Server
上游形态 HTTP request/stream CLI process + stream-json 长期 App Server + JSON-RPC
会话原语 gateway 重放 messages session resume/fork thread start/resume/fork
Identity system/developer message system prompt file baseInstructions
Style system block output style/append prompt developerInstructions
工具 API tool schema + middleware executor 内建工具 + MCP App Server items + MCP
Context compact gateway 历史消息摘要/裁剪 CC 原生 compact + refill contextCompaction + refill
中断 取消 HTTP/background request control request/process signal turn/interrupt
分支 DB 截断后重放 依版本与 session 能力 thread/fork 或 fresh hydration
计费/额度 token/cost 订阅侧 usage 订阅或 API 身份对应 usage

前端只显示 provider-neutral 名称与能力开关。某 provider 暂不支持的能力应在 capability response 中明确关闭,别靠失败后猜。

11.1 CC 与 Codex 的原语对照

产品层语义 Claude Code -p 常见原语 Codex App Server 原语
新会话 启动 claude -p / 新 session thread/start
冷恢复 --resume thread/resume {threadId}
原生分叉 --resume ... --fork-session thread/fork {threadId}
发一轮消息 写 stream-json stdin turn/start
中断 control request / process control turn/interrupt {threadId, turnId}
正文增量 content block text delta item/agentMessage/delta
思考显示 provider thinking/summary event item/reasoning/summaryTextDelta;可选 textDelta
工具开始 tool_use content block start tool item 的 item/started
工具完成 replayed tool result/result event tool item 的 item/completed
Turn 完成 result event turn/completed
Context 压缩 CC compact/system event completed contextCompaction item
Identity --system-prompt-file baseInstructions
Output style Output Style / append prompt developerInstructions
MCP 配置 JSON/CLI MCP config config.toml [mcp_servers.*]

这里对齐的是产品行为,字段与事件原样照搬会出错。adapter 负责把两套 native 原语翻译成同一 gateway contract。

12. MCP 工具注册如何同步给 CC 与 Codex

Codex 将 MCP 配置放在 config.toml[mcp_servers.] 下;CLI、桌面端和 IDE 扩展共享该配置。官方 MCP 文档

要让 CC 与 Codex 的工具注册完全同步,核心是 canonical registry:

flowchart TD
    A[App manifest / admin action] --> B[Canonical registry transaction]
    B --> C[Validate names, URL, transport, health]
    C --> D1[Render CC fragment]
    C --> D2[Render Codex config block]
    D1 --> E1[CC consumer verify]
    D2 --> E2[Codex consumer verify]
    E1 --> F[ACK: room + digest + heartbeat + ready]
    E2 --> F
    F --> G[Commit registry revision]

每个 revision 建议包含:

{
  "revision": 42,
  "digest": "sha256-of-canonical-json",
  "servers": {
    "demo": {
      "transport": "http",
      "url_by_room": {
        "cc": "http://cc-tools.example:8000/mcp",
        "codex": "http://codex-tools.example:8000/mcp"
      }
    }
  }
}

“同一工具”指 manifest、schema 与 revision 相同;两个 room 仍各用自己的 hostname/容器实例,避免跨房串流量或身份。

同一 canonical server 可渲染成两种 provider 配置。CC 的最小脱敏 fragment 例如:

{
  "mcpServers": {
    "memory": {
      "type": "http",
      "url": "http://memory.example:8000/mcp"
    }
  }
}

启动时以 claude -p --mcp-config /path/to/generated-mcp.json ... 加载;registry digest 改变后轮换受影响的 CC session。Codex renderer 则把同一条 canonical record 写成:

[mcp_servers.memory]
url = "http://memory.example:8000/mcp"
enabled = true
Canonical 字段 CC renderer Codex renderer
stable server name mcpServers. [mcp_servers.]
room-local URL url url
stdio command/args command / args command / args
secret header/token 版本支持的 env/header 配置 bearer_token_env_var / env_http_headers
enabled/required/timeout 按 CC 目标版本渲染或由 wrapper 强制 Codex config 对应键
registry digest session runtime fingerprint App Server transition/consumer ACK

两边原生配置格式会变化,canonical registry 只存 provider-neutral 意图;renderer 和验证器随目标版本更新。

12.1 consumer 应完成的验证

  1. fragment 名称与 app id 合法;
  2. fragment 声明的 room 与当前 consumer 一致;
  3. URL hostname 只能属于当前 room allowlist;
  4. TCP/HTTP 可达;
  5. 原生 CLI 能列出预期 MCP server;
  6. 生成配置使用原子替换;
  7. 写入带 heartbeat 的 ACK,禁止伪造静态 ready=true

12.2 Codex 动态变更为何常要轮换

修改 config.toml 并不保证运行中的 App Server、已有 thread、代理 NO_PROXY 和 MCP client 都立刻刷新。保守流程如下:

关闭新 turn 入口
→ 等待/排空 active turns
→ 写入候选配置
→ 用 codex mcp list 或 schema-aware client 验证
→ 轮换逻辑 sessions
→ 重启 App Server
→ 等待 initialize + MCP ready
→ 写 ACK
→ 重新开放 turn 入口

任一步失败都保留 ready=false 与明确错误,并恢复上一 committed revision。别让聊天继续进入一个工具清单半新半旧的进程。

12.3 legacy SSE MCP

一些旧 MCP server 只实现 legacy SSE,Codex 侧常用 streamable HTTP。可放一个窄 bridge:

  • 对 Codex 暴露 loopback streamable-HTTP endpoint;
  • bridge 连接旧 SSE endpoint;
  • session id、消息方向、超时和关闭语义逐项转译;
  • 限制目标 allowlist,禁止把它做成任意 URL proxy。

若上游已支持当前 MCP transport,直接升级上游更省心。

13. 可选:路由中转与进程隔离

Codex App Server 不要求 Docker。若机器本身拥有合适且稳定的网络出口,可以直接以本地进程、systemdlaunchd 服务运行 adapter 与 App Server,只需持久化 CODEX_HOME、限制工作目录权限并做好进程重启。

我们的部署位于远程 VPS,需要把 Codex 的登录、模型请求和 MCP 流量经过指定路由中转,同时禁止意外回落到 VPS 默认出口,所以增加了独立 gateway、隔离网络和 fail-closed probe。Docker 只是实现这一边界的一种方式;VM、network namespace 或 Kubernetes 也能完成同样目标。没有路由中转需求时,可以跳过本节。

13.1 Docker 路由中转示例

以下是可解析结构的脱敏片段。外部 bridge 名、代理端口和内部 service hostname 需按你的环境替换;不使用代理时删除 HTTP_PROXYHTTPS_PROXY 和相关 gateway 配置:

services:
  codex-seat:
    image: your-codex-seat:PINNED
    command: ["python3", "/opt/example-adapter/codex_app_server_adapter.py"]
    restart: unless-stopped
    working_dir: /srv/example-codex/workspace
    environment:
      HOME: /srv/example-codex/home
      CODEX_HOME: /srv/example-codex/config
      CODEX_BIN: /opt/example-codex/bin/codex
      HTTP_PROXY: http://egress.example:7890
      HTTPS_PROXY: http://egress.example:7890
      NO_PROXY: 127.0.0.1,localhost,egress.example,tools.example,memory.example
    volumes:
      - example_codex_home:/srv/example-codex/home
      - example_codex_config:/srv/example-codex/config
      - example_workspace:/srv/example-codex/workspace
    cap_drop: ["ALL"]
    security_opt: ["no-new-privileges:true"]
    networks: [agent_internal, gateway_bridge, memory_bridge]

networks:
  agent_internal:
    internal: true
  gateway_bridge:
    external: true
    name: REPLACE_WITH_GATEWAY_BRIDGE
  memory_bridge:
    external: true
    name: REPLACE_WITH_MEMORY_BRIDGE

volumes:
  example_codex_home: {}
  example_codex_config: {}
  example_workspace: {}

13.2 使用路由中转时的检查项

  • Codex 进程没有绕过中转的默认外网回退;
  • DNS、IPv4、IPv6 从进程所在的隔离边界归因;
  • gateway health 只证明代理可用,额外 probe 还要证明 direct bypass 失败;
  • 内部 MCP、gateway 和记忆服务地址加入 NO_PROXY 或等价 bypass;
  • Docker 方案避免使用 host network,并将 auth/config/workspace 分卷;
  • health 同时检查 App Server、认证状态和 MCP consumer freshness;
  • 网络/代理变化先做只读检查和回滚设计。

14. 上下文与记忆系统只需要一个通用接口

每个人的记忆库、历史消息摘要、检索和归档设计都不同,Codex adapter 无需理解内部 bucket、embedding、画像或评分算法。它只消费一段带预算的 context,或把记忆服务作为 MCP 挂给 Codex。

Codex 侧的处理顺序可以固定为:

fresh / 无法安全恢复  → full refill:带硬预算的 recovery context
cold resume / fork     → light refill:历史消息摘要和最近缺口
warm                   → 不重复补历史
contextCompaction 完成 → 下一轮加入一次 post-compact light refill
普通记忆检索           → gateway preflight 注入,或由 Codex 主动调用 memory MCP

记忆接口只需提供 recall(query, budget)、可选的异步 remember(turn)health()。返回内容应有字符预算、来源标记、超时和失败降级。

15. 流式 Markdown 前端

SSE adapter 只负责提供增量文本;SwiftUI 端可以用 Microsoft SwiftStreamingMarkdown 渲染逐步增长的完整 Markdown snapshot。它采用 MIT License。

一个常见实现是:

delta 到达
→ append 到 message buffer
→ 合并为“目前为止的完整 Markdown”
→ 推送给 StreamedMarkdownSource
→ tool/thinking 使用独立 UI block,避免塞进 Markdown 正文

不要每个字符都触发数据库写入。UI 可以高频刷新,gateway 持久化按时间窗口节流,并在 terminal event 强制 final flush。

16. 测试矩阵

fake App Server 可以覆盖绝大多数 adapter 状态机,真实 Codex E2E 负责证明协议 fixture 没有空转。

16.1 单元/协议测试

  • initialize 之前拒绝 thread 请求;
  • fresh 返回新 thread 并绑定;
  • resume 必须返回目标 thread;
  • fork 必须返回新 thread;
  • thread ownership 唯一;
  • agent/reasoning/tool delta 与 completed fallback;
  • usage、rate limit、auth required;
  • server request resolve 校验 conversation ownership;
  • interrupt;
  • 客户端断线后 detached drain;
  • ghost turn 与迟到 terminal event 不得串进新 turn;
  • App Server epoch 改变后 warm session 自动转 cold resume;
  • compact pending 在消息被接受后清除,新 compact boundary 仍能留下;
  • dynamic MCP config 写入失败时维持旧 committed revision。

16.2 真实 E2E

  1. fresh 对话,验证 persona/style 与普通 streaming;
  2. 调一个只读 MCP 工具,核对 start/result/UI/落库;
  3. 重启 App Server 后 cold resume;
  4. 相同 fingerprint 的 warm turn 不执行 refill;
  5. 修改 effort/style 后轮换且首轮生效;
  6. fork 后 child id 与 source 不同;
  7. 触发或模拟 compact,下一轮只吃 light refill;
  8. 前端中途断开,后台仍完整落库;
  9. registry apply → 新工具可用 → down → 工具消失;
  10. 若使用路由中转,从 Codex 进程所在隔离边界验证出口、DNS 与旁路策略。

17. 踩坑

1:每轮启动一个 App Server

能回字,长期会话、MCP 初始化、认证诊断和并发管理会变得很难看。优先维持一个 App Server runtime,再在其上管理逻辑 thread。

2:按 conversation id 猜 thread id

thread id 由 Codex 返回。数据库显式绑定,adapter 验证唯一 ownership。

3:请求发送后发生网络错误就自动重试

这段状态可能已经被模型接受。先标记 ambiguous/unsafe,恢复并查询 terminal state,确认安全后再决定。

4:把 delta 当最终事实

delta 可缺失、重复或在断线时丢一截。item/completedturn/completed 才是完成态权威来源。

5:MCP 配置文件改了,运行态仍旧

同时考虑 App Server 进程、thread、环境变量、代理 bypass 与 consumer ACK。把变更做成带锁、可回滚的 transition。

6:把 API context assembly 原封不动塞进 native agent 每一轮

native thread 已持有历史。每轮重复整段 summary/persona 会浪费上下文并强化旧信息。使用 fresh/full、resume/light、warm/none、post-compact/light 的状态机。

7:升级 Codex 时不核对 schema

App Server 协议仍在演进。升级前先 diff 新旧 schema,再用保存的 JSONL fixture 重放 start、resume、fork、tool、compact 和 error 路径;字段变化应先在 adapter 兼容层消化。

18. 推荐的实现顺序

  1. 冻结 gateway SSE 与 persistence contract。
  2. 写 fake App Server,先完成 initialize/thread/turn 最小闭环。
  3. 接 agent text delta 与 terminal completion。
  4. 加 thread ownership、resume-safe 与 lifecycle lock。
  5. 加 reasoning、tool、usage、rate limit、interrupt。
  6. 加 detached drain 与 gateway background producer。
  7. 加 persona/style/runtime fingerprint。
  8. 加 fresh/resume/fork/compact refill。
  9. 接静态 MCP,再做 canonical registry 和动态 transition。
  10. 最后处理网络隔离、真实 E2E、可重复验收与回滚。

19. 官方资料

License

本仓库的原创文档、图表与示例代码采用 MIT License

文中涉及的第三方项目及依赖保留各自许可证,详见 ATTRIBUTION.md

View this README on GitHub

安装

This server does not publish a one-line install command.

Open the repository installation guide

配置

{ "mcpServers": { "memory": { "type": "http", "url": "http://memory.example:8000/mcp" } } }