KC

kkarsyline/codex-app-server-gateway-guide

开发工具
40 stars 0 forks 质量 68 趋势 20MIT

将 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 reference 与 SDK 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 done 或 error 核对 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。若机器本身拥有合适且稳定的网络出口,可以直接以本地进程、systemd 或 launchd 服务运行 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_PROXY、HTTPS_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/completed 和 turn/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。

在 GitHub 查看完整 README

安装

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

Open the repository installation guide 新手?先看 mcpskill.org 教程 →

配置

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