Changing editor state used to be pasted straight into the message text, spending the user message's native budget and polluting history replay. Each turn now freezes structured additionalContext fragments instead (ide_context, open_tabs, workspace_context, referenced_files_<index>, terminal_context_<index>), so every fragment enjoys its own native budget and the request text stays pristine. The service sniffs the selected CLI once per child process via app-server generate-json-schema --experimental to learn whether turn/start accepts additionalContext, and re-sniffs after a runtime replacement; discovery never creates a thread or turn and cancels leave it early. Older CLIs get the official desktop text envelope with UTF-8 text-element byte ranges shifted accordingly, and history replay strips that envelope so users only ever see their own request. Cover the flow with schema-probe, fallback, cancellation and daemon tests on the bridge side plus context-budget, payload-copy and display sanitizer tests on the Java side; document the contract in docs/codex/app-server.md.
38 KiB
Codex app-server 运行时合同
本文档是 Codex 常驻 app-server 运行时的协议与行为合同,由 migrate-codex-to-app-server 变更产出并随实现维护。变更过程、设计决策与各轮审查/验收记录见 openspec/changes/migrate-codex-to-app-server/;需求→验证追踪表见同目录 verification-tracking.md,实施基线见同目录 implementation-baseline.md。
2026-10-05 的第十一轮审查(D34/任务39)修复启动失败后等待未结束、连续无 stream-start 的失败丢失终态、并发 reset 覆盖新运行时,以及延后 Diff 决定的路径与会话归属。启动八秒只保护早到 cleanup,提交身份保留至明确开始或终态;完整与尾部 ERROR 使用本次捕获的 clientMessageId 收尾,旧请求错误不解除新发送。新的 busy/loading 边界重新准备一次终态;reset 调用共享一次真实 child 退役。
原生 Diff 打开前冻结绝对 target、rename source、sessionId/provider/ledgerKeys;接受与拒绝的成功回执只更新原会话已审阅检查点,保留后来编辑,明确 null 不借用当前会话。失败不记账,既有文件校验保持。离线历史只增加实际原生退出诊断的匹配;助手复制保留完整格式示例,用户附件包装仍会清理。审查范围、旧页面红绿、真实 stdio 与只读现场证据的区别,以及最终来源与交付见 review-round-11-results.md。
提示词增强与提交信息生成也使用 Codex CLI 的 app-server。每次创建独立 ephemeral thread,沿用已启用提供商或已授权 CLI Login 的原生配置;使用只读沙箱、never/user 策略,后台交互明确拒绝或取消。只有 completed 返回成功,最终答案覆盖预览;超时/失败也会关闭并等待 child 退出。此路径不加载 Codex SDK,不恢复聊天会话。
- 背景:将 Codex 从「每轮
codex exec+ JSONL 补读 + 执行后确认/回滚」迁移到常驻codex app-server --listen stdio://(Java → Node daemon → app-server stdio JSON-RPC)。 - 协议边界:官方 app-server v2 信封
{id, method, params}/{id, result|error}/{method, params}(无jsonrpc字段);thread/start.sandbox为 kebab-case 字符串,turn/start.sandboxPolicy.type为 camelCase,collaboration settings 使用 snake_case。
Daemon 命令与事件合同
persistent-codex-service.js 面向 daemon 暴露以下命令。长操作走 daemon commandQueue(占用 activeRequestId 直至原生终态);控制操作绕过 commandQueue(绝不触碰 activeRequestId,立即回执),否则批准回复会被排在自己等待的 send 之后造成死锁。
| 命令 | 类型 | 作用 |
|---|---|---|
codex.send |
长操作 | 登记 send operation(FIFO),携带 clientMessageId/冻结设置,等待原生终态 |
codex.compact / codex.review |
长操作 | 同一 FIFO;先过 settings revision 门 |
codex.preconnect |
长操作(短) | 启动/复用运行时并 resume thread,不发起回合 |
codex.updateSettings |
控制 | 记录 desired 设置(effectiveness 由通知确认) |
codex.respondInteraction / codex.respondInteractionError |
控制(绕过队列) | 对指定 rpcId 回 typed result/error |
codex.abortTurn |
控制(绕过队列) | 停止活动 operation(派发阶段语义见下文) |
codex.releaseThread / codex.resetRuntime |
生命周期 | 排空 relation set / 关闭 child 并确认退出 |
codex.listThreads / codex.countThreadMessages / codex.readThread / codex.readHistoryPage / codex.readSubagent |
独立只读通道 | 通过 app-server history projection 查询;child 先验证 parentThreadId 关系,不占发送 FIFO、不 resume writer |
codex.listModels / codex.listSkills / codex.getMcpStatus / codex.reloadMcp |
独立只读通道 | 目录与 MCP 状态查询;reload 仍受 native runtime access 门控 |
事件(进程级 NDJSON,经 _originalStdoutWrite 直写,不带 activeRequestId 包装):
{"type":"daemon","event":"codex_event","provider":"codex","runtimeGeneration":3,"channelId":"channel-id","sessionEpoch":"7","clientOperationId":"op-3-...","rootThreadId":"th-root","threadId":"th-child","turnId":"turn-1","itemId":"item-1","kind":"turnStarted","payload":{}}
kind枚举:runtimeStateChanged / threadStarted / threadResumed / operationQueued / operationAcked / turnStarted / turnAborted / itemStarted / itemCompleted / itemUpdated / interactionRequested / operationDone / orphanTurnTerminal / runtimeUnhealthy / runtimeReset / thread/settings/updated 等(未知原生通知以nativeNotification透传摘要)。threadRelationVerified只在当前 native client 验证 parent/root 后发出,Java 据此登记子线程的 writer;只读子历史查询不领取 writer。旧 client 查询结束后不发布到新代,释放根会等待 native child 退出再释放关系租约。- marker(
[MESSAGE_START]/[MESSAGE_END]等)走 console.log,被 daemon 包装进当前请求的 envelope——这是有意的 per-operation 捕获 emitter;后台 thread 事件永远不走该通道。 - heartbeat/status 的
runtimes.codex快照与 idle reaper:getCodexRuntimeSnapshot();sessionCount>0 || busy时 daemon 不 retire。
示例 NDJSON 可由 Node fixture(persistent-codex-service.test.js)与 Java fixture 共同解析。
IDE 上下文与用户正文
codex.send.message 只携带用户输入;活动文件与选区行号、最多 20 个其他打开文件路径、工作区信息分别通过 additionalContext 的 ide_context、open_tabs、workspace_context 片段传递,显式文件/终端引用分别使用带序号的 referenced_files_<index> / terminal_context_<index> 片段。每片段为 {kind:"application", value:"..."},随本次 operation 冻结;各片段分别使用原生长度预算,长标签列表不会截掉活动文件或挤占显式引用。片段描述编辑器状态,不断言活动文件就是问题主题,也不内联文件正文或所选代码。角色与项目模块仍使用原来的 developerInstructions,编辑器变化不参与运行时 fingerprint 或持久设置 revision。
仅在发送包含非空上下文时,当前 client 使用同一已解析 CLI 命令和环境执行一次 app-server generate-json-schema --out <temp> --experimental,读取 v2/TurnStartParams.json 的 additionalContext 字段;五秒超时、命令失败或 schema 不支持均回退。结果只缓存于当前 client,替换 CLI/重启 child 后重新探测,临时 schema 在探测结束时删除。探测不创建 thread 或 turn;绝不通过失败后重发 turn/start 来判断兼容性。
支持该实验字段时,原生 turn/start.input 保留原始用户正文与图片,另传 additionalContext。Codex 将 application 片段作为模型可见的 developer 上下文,独立于可见的 userMessage item;它仍可能写入原生历史,并受原生每片段约 1,000 token 的截断和相邻状态去重约束。片段更新追加到上下文,不能删除历史中已发送的旧内容。
不支持时采用官方 CLI/桌面端的文本约定:# Context from my IDE setup: 开头放上下文,再以 ## My request for Codex: 引出用户输入。图片相对顺序不变,text element 偏移按 UTF-8 字节调整。此回退仍把上下文存入原始用户正文,支持该约定的客户端在展示时隐藏;插件的 UserMessageSanitizer 同步剥离此包装,保留旧版追加上下文的历史清理能力。兼容性由目标客户端版本决定,不承诺任意旧客户端都隐藏包装。
codex-ide-context.test.js 覆盖 schema 探测/失败回退、临时文件清理、多片段回退与媒体/UTF-8 偏移;codex-appserver-service.test.js 和 persistent-codex-service.test.js 验证逐轮冻结、单次派发、原始用户正文及运行时复用。已取消的 operation 在能力探测前退出,探测期间取消也不会创建 thread/turn。Java 定向测试验证独立上下文预算、daemon 载荷拷贝和历史显示清理。
持久 service 运行时状态与恢复
CodexAppServerService(ai-bridge/services/codex/codex-appserver-service.js)是每个聊天宿主一实例的常驻编排层。状态与恢复语义:
运行时状态机
STOPPED → STARTING → INITIALIZING → READY → DRAINING → STOPPED
任意活动态 → FAILED(协议/进程故障;下一次用户操作可重新启动)
READY时才派发 FIFO 操作;#drainQueue在非 READY 时先ensureRuntime再继续。- 连接意外退出(EOF/崩溃)→
#failRuntime一次性收尾:所有未结算 operation 标记 failed、队列清空、thread 状态回到unloaded(thread id 保留)、child 关闭。恢复 = 重新 initialize +thread/resume,绝不自动重发turn/start。
操作与派发阶段
每个 operation 依次经过 notWritten → writtenUnconfirmed → nativeTurnKnown → terminal:
- 注册先于写入;
turn/started通知(先于或后于 ack)将 operation 绑定到 native turnId,同一 operation 不绑定两个回合。 - 终态只认
turn/completed的completed/failed/interrupted与已确认的 child 退出;ack、本地超时、单个 item、重试中的 error 都不是终态。 - Stop(10 秒总确认预算,从 cancelRequested 起算,迟到 ack 不重置):
- 仍在队列(未派发)→ 本地取消,零 RPC;
- 活动但未写入且自身 bootstrap 未确认 → 保留取消意图,dispatch 门在 bootstrap 确认后生效;
nativeTurnKnown→ 立即turn/interrupt,等待原生终态;- 已写入但身份未知 → 保留取消意图,身份到达即 interrupt;预算内仍无法确认 → 关闭 child、确认退出后一次性失败收尾(
runtimeUnhealthy事件)。
设置 revision 门
updateSettings只更新 desired(revision 递增);effectiveness 以thread/settings/updated通知为准(official source common.rs:685/1941,experimental)。- 每次 FIFO 派发前冻结最新 desired revision:
send内联携带设置;compact/review无设置参数 → 先thread/settings/update并等待匹配的 effective 通知,未确认不启动。 - 断线/child 退出后只恢复 thread;冷 resume(rebuild)应用新的 developerInstructions 并由上层清除临时 acceptForSession/session grants。
Launch-config 重建
computeCodexRuntimeFingerprint覆盖 authMode/凭证哈希/endpoint/provider/headers/CODEX_HOME/CLI 来源;model/effort 等普通设置不参与(走 RPC)。- fingerprint 变化 →
notifyLaunchConfigChange:空闲边界立即重建;活动回合中推迟到终态后。重建 = 排空 + resetRuntime + ensureRuntime + 冷 resume 根 thread(threadResumed事件供上层清理临时授权)。
测试映射
codex-appserver-service.test.js 覆盖:多轮复用同一连接、通知先于 ack 绑定、FIFO 顺序、重复终态只收尾一次、断线恢复不重发、三种 Stop 路径 + bootstrap 停止、settings 门、fingerprint 重建(空闲/活动推迟)、child 终态不释放父 FIFO。fixture 由 testing/codex-stdio-peer.js 提供(silent-start/never-respond/reverse-approval 等场景)。
协议信封、ID 与超时基准
CodexAppServerClient(ai-bridge/services/codex/codex-appserver-client.js)按结构分流,以下示例全部可通过 ai-bridge/services/codex/testing/codex-stdio-peer.js 的对应场景解析:
| 方向 | 形态 | 示例 |
|---|---|---|
| 客户端请求 | {id, method, params}(无 jsonrpc 字段) |
{"id":1,"method":"turn/start","params":{"threadId":"th","input":[{"type":"text","text":"hi"}]}} |
| 客户端请求响应 | {id, result} 或 {id, error} |
{"id":1,"result":{"thread":{…}}} |
| 通知 | {method, params}(无 id) |
{"method":"turn/started","params":{"threadId":"th","turn":{"id":"t","status":"inProgress"}}} |
| 服务端请求 | {id, method, params}(id 可为 number 或 string) |
{"id":3,"method":"item/commandExecution/requestApproval","params":{…}} / {"id":"server-fixture-77","method":…} |
| 服务端请求回复 | 原样回传 id 的 {id, result} / {id, error} |
{"id":3,"result":{"decision":"accept"}}、拒绝:{"id":9,"error":{"code":-32601,"message":"unsupported server request: …"}} |
规则:
- ID 类型保留:number/string 原样回传;双向各自维护 pending registry,同一数值 ID 在两个方向互不覆盖(fixture
numeric-id-collision)。 - 握手单飞:spawn →
initialize(clientInfocodemoss_intellij+capabilities.experimentalApi: true)→ 响应 →initialized通知 → READY;并发业务调用共享同一握手 promise,握手失败时没有任何业务 RPC 上线(fixtureinitialize-refusal)。 - 短 RPC 预算:默认 30 秒;用户交互(批准/询问)绝不借用短 RPC 超时。超时只是客户端等待失败(
err.code='RPC_TIMEOUT',err.phase为notWritten或writtenUnconfirmed),不代表原生操作终结;迟到的响应通过lateResponse事件保留给上层做 dispatchPhase 关联(fixturenever-respond)。 - 非法 JSON 行不中断运行时(
protocolError事件);EOF/进程退出以CHILD_EXITED一次性收尾,pending、timer、reader 全部清理,之后请求以真实退出错误失败。 - 未知服务端请求显式回
-32601(不自动批准、不降级 exec);未知通知向上层透传,不影响活动回合(fixtureunknown-server-request)。
运行方式与 CLI 识别
Codex 与 OpenCode 共用「提供商管理 → CLI」的识别列表,展示本机 CLI 的路径、版本和手动安装指引。SDK 页面及 SDK 安装、版本、更新、卸载 API 不再管理 Codex,聊天也不再依赖 codex-sdk 安装状态。优先使用官方独立安装器,npm / Homebrew 仍可选;安装与更新步骤见 Codex CLI 安装与识别,安装后刷新 CLI 页面。
显式路径或 CODEX_BIN / CODEX_PATH / CODEX_CLI_PATH 优先,其次查找 PATH、常用安装位置和登录 shell。Java 检测与 Node 发现同时覆盖 CODEX_INSTALL_DIR、Unix ~/.local/bin、Windows %LOCALAPPDATA%\Programs\OpenAI\Codex\bin,以及 ${CODEX_HOME:-~/.codex}/packages/standalone/current/bin 和旧式 current 平铺布局;不遍历未选中的 releases/。失效的显式覆盖会报错。只有没有外部 CLI 时才兼容查找旧 ~/.codemoss/dependencies/codex-sdk/ 中的既有 CLI binary/launcher;不创建安装标记、不修改或删除该目录。只有 SDK metadata 的旧目录不算 CLI 可用。Windows npm .cmd 使用现有 CLI spawn adapter 启动;不扫描 Codex 桌面应用 bundle。
多候选与启动回退:同一台机器常同时存在多份 codex(版本管理器、npm 全局前缀、手动下载)。发现阶段不再只取第一个命中项,而是按「显式/env 覆盖 → PATH 全部命中 → home 候选目录 → 常用 bin 目录 → 登录 shell」列出全部候选,并按真实路径去重(符号链接归一)。每个候选只做文件系统判定,不 spawn 任何进程(CLI 页面仍然独占版本探测):bin/codex.js npm 启动器会在其所在 @openai/codex 包(含嵌套与提升布局)内查找平台二进制;缺失时该安装被判定为「未确认」,降到最后一位作为保底,既不抢占后面可确认可用的安装,也不会因为启发式漏判而彻底拿掉用户本来可用的 CLI。完全不能作为 CLI 的路径(不存在、非文件)直接丢弃并给出原因。
解析结果同时返回 candidates(按优先级的可用候选,末尾为未确认保底项)与 rejected(完全不可用项及原因)。运行时的 startupAttempts 等于候选数:某个候选在 READY 之前退出(启动器 ENOENT、初始化失败、握手超时)时,服务在未派发任何 RPC 的前提下关闭该 generation、切到下一个候选重试,并发出 runtimeFallback 事件;只有全部候选失败才按失败收尾。READY 之后的退出绝不重试(不重放未知工作)。
失败诊断:客户端保留最近若干行已脱敏 stderr,并在 READY 之前退出时把「启动失败标记 + 启动命令 + stderr 尾部」挂在 err.details 上(err.message 仍是规范文本 codex app-server exited (code=…, signal=…),历史回退等既有匹配者不受影响)。runtime 失败文案在保留该规范前缀之外,追加每个候选 CLI 的尝试结果与一句可操作提示,前端按 codexCliUnavailable 诊断模式链接官方分平台安装/更新说明,并提供改 CODEX_BIN 的步骤,不要求独立安装用户先卸载 npm 包;CodexNativeHistoryReader.permitsOfflineFallback 对退出行采用前缀容忍匹配,带诊断后缀时仍允许只读历史回退。
CLI 页面仅运行 --version,成功退出且输出包含 codex-cli <版本> 才确认安装;前置警告不会覆盖版本,不再将探测失败的可执行文件标记为已安装。检测不读取 config/auth、不启动 app-server;原生聊天与目录访问仍遵守运行方式授权。Node system-status 保留 codex 消费合同并报告实际 CLI 命令路径;它不是 SDK 管理入口。
运行方式(getCodexRuntimeState())与 app-server child 的关系:
| access | 行为 |
|---|---|
managed |
使用已识别 CLI + 插件配置的 provider/endpoint/凭证(env_key 传递),可启动 app-server child |
cli_login |
使用已授权用户原生认证(CODEX_HOME),创建 child 前只补原生 provider env_key 指定且未继承的登录 shell 变量;已有值和明确空值保留 |
inactive |
不启动 app-server child:聊天、模型/skills/MCP 目录探测、历史读取都不得为解析或探测而 spawn 进程;已有本地 JSONL 历史只读能力继续可用 |
native auto 能力不再以 TypeScript SDK 版本判断:模式选择由原生运行时在发送时校验(原生 constraints 拒绝时显示错误,不回退 exec)。前端已移除 codexSdkMeetsMinimum 门控(useModelProviderState / modelProviderStateHelpers),Codex plan 通过原生 collaborationMode 发送并保持独立的 native approval/sandbox 设置。
GUI 的 env_key 查找使用实际 CODEX_HOME/config.toml,只读 TOML provider 字符串字段,支持普通/inline/dotted/quoted 配置并忽略指令正文。managed/inactive 不使用此发现路径;合法名字才可传入既有允许的登录 shell,不复制整份 shell 环境,不打印值或 shell stderr,解析/查找失败仍由原生鉴权报告。Windows 保留显式变量及大小写别名,不主动探测 POSIX shell。异步 child 准备仍受 generation/release 门控,旧查找结果不能启动已释放的 writer。
历史显示与目录来源
授权运行时优先通过 thread/read 元信息、thread/turns/list 和 thread/items/list 读取显示记录,保存 opaque cursor,并以 thread/turn/item/clientId 对账重叠页和本地未确认提交。仅原生明确不支持分页时固定选择 includeTurns 整读;读取过程不 resume、不抢执行 owner。鉴权、writer 或策略错误不触发离线 fallback。
历史页的 begin、batch/chunk、complete/error 都由窗口 FIFO 队列传输。同一页的连续 batch/chunk 共用一个待发送项,避免大页触及 512 项恢复溢出上限时丢失中间记录;发送脚本仍按参数大小分段,执行失败后保留批次顺序并按既有重试预算处理。
在 inactive、CLI/传输不可用,或原生投影遗漏工具、明文思考及 compact 前记录时,选择整个 legacy 只读 transcript;一次加载不会拼接两个来源。旧分页/整读共用 wrapper 回放器,静态 shell_command/exec_command 可显示 Bash 卡片,未知或结果无法归属的 wrapper 保留通用卡片和输出。明文 reasoning summary/content 显示为可展开思考;仅加密记录保留已知思考状态,界面隐藏空占位,不显示密文,也不拆开连续工具批次。response_item 保留记录中的轮次标识,并使用 session_meta 的线程标识,让正文、思考与原生工具按同一轮合并。内部 external_codex_apps_open_page 标签被去除,实际用户文字与图片保留。这些显示记录不会回注模型。
Promise.all / Promise.allSettled 的历史结果按原数组索引关联。除 {i, ...result} 等索引信封外,也识别 forEach 和按批次数组长度遍历的 for 循环打印的独立索引标签(模板字符串或 "Result " + i),并核对随后打印的结果来自同一数组位置。标签后的数组、基本值及普通文本也保留在对应槽位,不再要求结果必须是 JSON 对象;与标签同名的返回文字仍作为数据处理。混合的 write_stdin 与 exec_command 因此可分别回放;轮询只更新原进程的命令卡片,不生成额外命令。无法确认结果归属时保留原 wrapper 与输出。
批次中的非工具 Promise.resolve(...) 条目保留原表达式及该索引的实际结果,作为内部编排数据,不显示独立工具卡片,也不计入或打断命令分组;实时与历史展示使用相同过滤规则。回放不执行表达式,不根据本地值中的 exit_code 等字段推断命令状态;Promise.all 返回对象内的 status 仍属于数据,不作为 allSettled 状态封套解包。本地值直接展开到索引信封、缺少独立 result 时,保留整个 wrapper 与输出,避免把正文的 status / error 当成状态。allSettled 将 r.value 或条件分支结果放入 result 时,本地值也原样保留;只有源码明确按索引打印整个 settled receipt 时,才解包本地嵌套封套。内部 rejection 标记独立于结果正文,返回字段不能冒充失败状态;空数组、null 和明确 rejection 分别保留在原始消息数据中。缺少可关联的独立结果时,保留整个 wrapper 与输出。
原生子代理卡片使用实际 child thread ID;历史/status 先验证 parentThreadId 祖先链,active 子线程不因父终态或旧的 completed turn 被显示为完成。跨 Tab 的显示查询只读;切换根会话后旧请求不能更新新页面。节点状态未知时继续查询,不猜测完成。
模型目录使用原生 model slug、隐藏项和 reasoning effort 元数据。skills 目录按 cwd 读取原生 name/path,支持同名不同路径,刷新或配置变化会清除失效选择;选择项以原生 skill 输入发送。MCP 连接/认证状态来自原生目录,配置 CRUD 保留已有授权边界,用户刷新或写入成功后触发原生 reload。各目录按请求 ID 接受结果,skills 页面不同时加载模型/MCP;错误会在相应页面显示。
编辑、命令、图片和 compact 的显示
apply_patch(直接或旧 exec 的静态实参)与fileChange共用 Claude 的编辑标题、操作栏、diff 和默认展开设置,展示全部文件、增删行和真实状态,变化类型及移动路径保留在悬停说明。真实错误为红色状态,详情收进展开区域。混合/部分动态 wrapper 中可确认的 literal 补丁也可预览;缺少单独结果时为灰色 unknown,完整 wrapper 与输出保留。历史只用于显示,不执行补丁;hunk 只在聊天中预览,不冒充完整 old/new 文件内容。commandExecution的原生类型不保证提供“执行原因/summary”;已有 description/summary/title 被保留,原生批准reason与旧justification单列为批准理由。commandActions的读取、列目录和搜索可形成动作摘要;legacy 历史回执的parsed_cmd沿用相同显示规则(兼容list_files/listFiles)。缺少动作摘要时直接显示命令,历史回放不再生成Read last lines、Run node等通用英文标签,保证批量命令的单行展示与流式一致;旧字面量数组回放同样保留 Bash 工具类型,不再按命令文本改成 Read 或 Glob。不能把前一段思考或命令名编造成执行动机。dynamicToolCall使用原生 namespace/tool/arguments,文本和图片结果保留为标准结果块数组。inProgress 不显示已完成,终态和 success=false 按实际状态显示。通用卡片展开可查看已有结果正文/图片,混合 wrapper 的实际错误输出也可查看。- MCP 的 content 保持块数组,原生 image/data/mimeType 映射为共用图片 source,structuredContent 单独保留为可读块;实时与旧历史回执共用显示约定。网页 item/started 即可显示,在所属 operation 中断或失败后补配对终态,避免持续 pending。
- 桌面端完整附件包装在显示/复制时剥离,保留真实任务及普通文件引用。图片以正文气泡外的缩略图组显示,Codex 气泡复用 Claude 的主题色;仅图片不渲染空气泡,预览支持键盘关闭并恢复焦点。
contextCompaction和旧compacted即使没有摘要也在原消息位置显示压缩边界,禁止合入普通 assistant 或过滤空正文。优先显示原生 item 时间,只有 turn 时间则注明近似来源;实时收到事件的时间标明“记录时间”,历史缺失则未知。原生历史不能保证恢复手动/自动触发来源,缺字段不猜测。SessionIndexManager当前缓存版本为 v10,重建旧附件清单标题及内部自动审批过滤;临时分页 spool 随 reader 重建。
原生交互、秘密答案和工作区 diff
Java 为每个原生反向请求生成由 channel、session epoch、runtime generation 和原始 RPC ID 组成的不透明 interactionKey,并为当前页面签发 dialogToken。页面只回传这两个 opaque 值;Java 从 registry 取回真实 RPC ID、thread/turn/item 身份后发送 typed result,重复、过期或旧页面回答会被拒绝。JCEF 重建时 registry 保留 deadline 与 params 快照,frontend_ready 后按 deliverySequence 重放尚未 resolved 的请求并轮换 dialogToken;原生 serverRequest/resolved 事件携带相同 token,页面只关闭对应弹窗。
每次用户提交由 webview 生成 clientMessageId,乐观气泡、Java SessionState、Node codex.send 与原生 turn/start.clientUserMessageId 共用该值。原生 userMessage.clientId 到达后按身份更新原气泡,重复文本不会被当成同一条提交;不确定失败不会自动重发。
Codex 的 requestUserInput 和 MCP elicitation 在提交前写入插件私有 ~/.codemoss/codex-privacy/ 版本化索引。每次写入独立 <home 指纹>.record.<uuid>.json 原子记录,Node/Java 兼容读取旧 <home 指纹>.json,避免多个窗口的独立 Node 进程互相覆盖分类。索引只包含宿主/home scope、thread、turn、call、item、question 身份和 secret 标记,不写问题正文、答案或答案 hash;原生仍可能把已提交答案保存到自己的 transcript,插件不宣称可以控制该存储。页面草稿会跳过 isSecret 问题,索引原子写入失败时不会转发交互。
损坏 JSON/版本/条目与持久 conservativeMasking 继续保护答案及不可确认的工具结果;后续新记录不会覆盖丢失分类,冷读也不释放旧 opaque 结果或普通 answers。正常用户文字、思考、模型 JSON 回复、命令参数与历史时间/状态字段保持原值,不因索引损坏遮蔽整段会话。仅在结果体内解析答案 JSON;结果数据不能因字段名为 id/name/path/status 而被豁免,外层协议身份及已知内容类型保留。question 名称只在匹配交互内保护答案/值,题目正文和其他调用的同名数据保持可见。MCP elicitation 的 RPC ID 不当作工具 call/item ID,无 item 时按 turn 保护输出;turnId 为 null 时按 thread 保护输出,具体 call 的可靠普通分类仍优先。
MCP form elicitation 会把 requestedSchema.properties 映射成带稳定字段 ID 的问答页,提交时还原 string/number/integer/boolean/array 类型并发送 {action:"accept", content};可选字段由 schema 的 required 决定。URL elicitation 只在用户点击提交动作后打开目标 URL,取消或 unsupported schema 发送 {action:"cancel", content:null},不会自动接受或把 URL 当作普通模型文本。
原生 plan item 与 turn/plan/updated TODO 分开保存。item/plan/delta 先显示 preview,item/completed 以同一 native item ID 权威替换;权威正文在当前 thread 上显示“Execute plan”和“Continue adjusting”。执行按钮通过 execute_codex_plan 单飞校验 thread/plan 身份,然后以 default collaboration mode 发起一次新的 native turn,不改变原 approval preset 或 sandbox selection。
前端同步清除 runtimeReset、failed runtime 和同一计划回合失败/中断时的执行入口;成功完成的计划继续保留,其他 root 的状态不影响当前计划。文件批准预览读取原生 kind.type/movePath。单文件撤销保存各自的提交账本,不同文件可并行;回执只移除实际撤销的 keys。单文件或批量派发失败立即解除等待,保留记录,不自动重发。
执行计划按每次提交的 requestId 接受回执,成功/异常/提前失败均回传原 requestType/threadId/requestId;旧会话结果不结束当前等待,当前原生拒绝原因会显示。手动压缩保留 interrupted/cancelled 的原生 outcome,显示已中断。撤销的相对路径和 moveFrom 固定到提交时工作目录,EDT 等待期间切换目录不改变写入目标。
直接索引工具结果 {i,result:actualResult} 与原有 Promise 回执共用实际槽位关联,不将最终输出重新显示为 exec。原生生成图片显示开始、PNG/保存路径和失败终态;原生 error 通知显示完整原因,继续等待回合终态。子代理卡片与底部列表复用 Claude 表现并共享原生身份、状态和摘要,保留跨回合仍在运行的代理。
模型目录、技能和 MCP 状态使用独立的只读 native data 事件。Codex 选择器发送 codex_native_list_models,返回的 data 只描述当前有效运行时可见的模型,不代表账户 entitlement;技能和 MCP 设置分别使用 codex_native_list_skills、codex_native_mcp_status 与显式 codex_native_mcp_reload。inactive 或鉴权错误不会静默启动 legacy exec。
历史列表优先走 thread/list,页面保留 source=native、nextCursor 和 partial。cursor 原样传回,重叠 turn/item 页按 native id upsert;metadata-only 空页不会清除现有记录。只有 runtime access inactive 时才请求已有只读 legacy 列表,鉴权/writer 错误保持可见。
thread/list 不提供 messageCount,返回的空 turns 也不代表会话为空。列表先显示元信息,再通过独立只读 codex.countThreadMessages 请求后台统计,前端最多同时派发 4 个计数请求,相同元信息的重叠页复用待完成的请求。计数遍历原生历史页,沿用显示消息的 item 投影(含思考、工具调用及结果),按消息身份消除重叠页,不 resume 线程或启动模型回合。不完整的 turn/item 页缺少续页游标时保留未知计数。每次计数总时限为 110 秒,超时后停止请求后续页并允许重试。仅保存计数的宿主级缓存最多 128 条,有效期 60 秒;前端同样限制已知计数的复用时间,元信息变更及活跃线程重新读取。旧 CLI 不支持原生分页时采用完整原生读取,失败不记作 0,也不切换 JSONL 来源。
未知计数显示“消息数未统计”,已确认空会话才显示 0。顶部只在当前已加载列表的计数全部已知时显示消息总和,会话数量单独计算;增量页和异步计数回执不会重复累加。回执按列表请求及元信息版本校验,页面切换、行更新或删除之后的旧计数不能覆盖当前记录。分页及改名、收藏都合并到最新状态,保留本地删除和编辑;进入 legacy 回退后忽略尚未返回的 native 页。较新 native 记录的未知或归零计数不会从较旧记录补齐。
设置页的 Codex 访问范围统一为 read-only、workspace-write、danger-full-access,旧 set_codex_sandbox_mode 仍兼容但同时发出带 source=user 的 set_codex_sandbox_selection。计划/批准选择不会改写 sandbox,页面显示迁移来源。
旧 JSONL history adapter 只在 inactive、离线或原生投影明确缺失时提供只读回退;它不会重新执行任务,也不自动与 native history 拼接。旧 adapter 在未加载新 privacy index 的版本中不提供本版本的 secret 遮蔽保证,回退文案必须明确这一边界。
/diff 走 codex_read_workspace_diff 独立 Java handler,基于当前 effective cwd 读取 Git staged、unstaged 和 untracked 快照,带大小/超时上限;它不创建模型回合,也不复用旧 turn 的 diff。非 Git 目录通过独立结果事件返回明确状态。
边界行为补充
会话展示行为:编辑账本独立读取全部原生 fileChange,checkpoint 以操作身份保存,删除/移动/EOF 与冲突撤销依据实际 patch;ImageView 的空终态可完成。Codex 文本使用完整 item 快照,agentMessage 的 text、content 数组或 content 字符串共用投影,结束时提交整个待处理列表;Guardian 目录按来源过滤并重建旧缓存。用户气泡共用 Claude 主题,单条/批次命令共用外层 shell 显示清理,纯轮询不创建卡片或分隔线。
新会话在首轮原生 threadStarted 事件到达时,遵守现有开关和授权异步生成标题,不等待回合结束。辅助请求使用独立 app-server child,标题通过原生 thread/name/set 保存,手动名称优先。只有创建了线程才占用一次标题机会;bootstrap 失败后的立即重试或排队发送仍可生成标题。每次元信息读取与写入都校验主运行时仍为 ready 且 child 存活,辅助任务不会重启已崩溃或退役的 writer。释放、重置、回合中断或启动配置变化会取消辅助请求;释放与重置等待所有待命名任务收尾。进程管理面板将 Codex daemon 登记为 DAEMON,孤儿清理保留其原生子进程;面板重启仅停止 daemon,Java 会话路由在下一次请求时继续使用。
输出片段不表示命令完成;明确终态及空的 MCP 结果仍会收尾。失败/中断只更新当前操作尚未结束的工具,已结束回合的运行时输出缓存会释放。异步批准预览以请求实例校验租约,原生 resolved 使旧回调失效,同一 RPC ID 后续使用不受旧回调影响。
流式尾部传输按完整 raw 内容比较快照差异,结构签名仅用于调度节流;思考正文或原生 item 元信息变化也会扩大待发送后缀。前端应用 updateMessageTail 前先提交定时器中等待的完整快照,避免尾部更新的 sequence 淘汰其依赖的基线,丢失批次之间的说明、思考或编辑卡片。
历史 wrapper 中相同命令在不同显式绝对工作目录执行时,可按唯一匹配的命令及目录关联原生回执;原生 file: URI 与对应本地路径按目录比较,已确认结果归属的进程身份仍优先。无索引并行输出中的 session_id 不按到达顺序分配给源码调用;这类批次仅用独立原生回执确认各命令结果。Windows 路径统一分隔符,POSIX 绝对路径中的反斜杠保留为文件名字符。相同目录、相对目录或缺少唯一目录证据时继续保留原有保守回退,不能靠完成顺序交换命令的输出。纯命令批次与包含编辑的混合批次沿用逐个工具投影及分组。
旧 wrapper 仅在操作完整可确认且结果可关联时被替换。具有独立 FileChange/CommandExecution/ImageView 或结构化结果的已知混合调用恢复为共用卡片;Promise 数组接受 i 或 index 结果封套并按实际槽位配对,两者冲突时保留通用卡片。运行中命令的延迟原生完成记录和空输入轮询均更新原命令,不生成第二张卡片。write_stdin 的字面量输入(包括 Ctrl+C)关联已知进程,或通过原生 process_id 恢复对应命令;中断回执只结束原命令,后续命令保留各自结果。原生回执带进程标识时优先按该标识匹配,避免相同命令文本误关联;同批次的同名命令以结果中的 session_id 和回执的 process_id 配对,即使完成顺序相反也不交换目录、输出或状态,缺少唯一关联时保留 wrapper;旧回执缺少标识时,仅接受命令文本与线程/回合归属唯一的目标,或由显式绝对工作目录唯一确认的目标;仍有同名进程或同名后续命令无法区分时保留 wrapper。输入的目标或结果无法确认时仍保留完整 wrapper。历史来源选择按已恢复的具体调用身份判断,额外 exec 数量不能覆盖完整投影。缺少独立结果的混合类型、多补丁、动态输入或不确定对象保留完整通用卡片和输出,其中静态补丁可另行灰色预览,不把共同结果归给各补丁。CRLF、顶层 Script failed 错误封套、结构化失败及未配对输入缓存预算均纳入处理。不同明确时间的 compact 记录继续独立显示。实时/历史图片共用原生 URL、legacy image_url、base64/source 和 localImage 转换,清理正文时保留附件;不完整文件变化整体回退通用卡片并保留状态。
原生控制指令不依赖共享 SDK 安装查询;/compact、/review、/diff 桥接无法派发时显示错误,不提示“已开始”或生成模型消息。目录重复 cursor/页数限制/桥接派发失败会结束 loading 并显示错误。/diff 同时排空 Git stdout/stderr,输出各自受限,结果按会话对象、sessionId 和 cwd 校验。dynamic/collab started 和失败/中断收尾进入同一工具显示路径;receiver 存在不能替代启动调用的 completed 状态,父回合的收尾也不改变仍活动的 child。