fix(subagent-codex): verify 0.147 on native Windows

This commit is contained in:
Tianyi Cui
2026-08-08 23:56:39 +08:00
parent 3a518021f6
commit 176e4c6bbb
16 changed files with 70 additions and 62 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
README.md: c25ee90edf8972da66448fe84cb659b0aec79e6f
README.zh.md: 10c8fcc47a9ab04bca983857bd44ede265c23435
README.md: b0f0548ed7dd61c81111eb8a141b7a994066ed5b
README.zh.md: 8fe84e1428a77d9842a58c230185afb004345d58

View File

@@ -10,7 +10,7 @@ This package registers the fixed `codex` subagent provider. Each accepted run st
The published `run.result` starts exactly one turn. It accepts only notifications for that run's thread and turn, then waits for the authoritative `turn/completed` terminal notification. The latest `agentMessage` with `phase: "final_answer"` wins; when Codex emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback. Commentary never replaces either answer, and a successful turn with no nonblank answer settles as an error.
For command and file approvals, the unattended provider selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.146.0 request shape without an offered-decision list falls back to `decline`. It answers permission requests with an empty turn-scoped permission set, answers user-input requests with no answers, and declines MCP elicitation. A request with no legal unattended response, or any unknown server request, fails the run.
For command and file approvals, the unattended provider selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.147.0 request shape without an offered-decision list falls back to `decline`. It answers permission requests with an empty turn-scoped permission set, answers user-input requests with no answers, and declines MCP elicitation. A request with no legal unattended response, or any unknown server request, fails the run.
Local cancellation wins the result race and maps to `aborted`. A failed turn whose `codexErrorInfo` is `contextWindowExceeded` maps to `max-tokens`; every other remote interrupted or failed turn maps to `error`, and this version produces no `refusal`. `dispose()` is idempotent: it requests a best-effort `turn/interrupt` with both current ids when they are known, closes the JSON-RPC wire, ends stdin, invokes the shared process-tree termination escalation, and waits for whole-tree exit. Result failure and independent teardown failure remain separate.
@@ -47,7 +47,7 @@ Install this package and add the following rows to your own `cordis.yml`. Shippe
## Product compatibility and evidence
The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.146.0` / `codex-cli 0.146.0`: the keyless real-product spec drives the official binary against a loopback Responses service with a non-empty fake key and proves the task, authentication, exact answer, cancellation, approvals, and process-tree exit. A separate Loader composition e2e boots the README-shaped user configuration with no `codex` command available, verifies the fixed provider and foreground-only tool schema, and records zero child starts. A credentialed e2e starts the production provider and real Codex, then obtains a unique answer from the fixed official DeepSeek service through a loopback-only test bridge from Responses to Chat Completions; that bridge is not production functionality or native Codex support for DeepSeek's Chat Completions API. The npm package is a test-only dependency; deployments still supply `codex` on `PATH`.
The production wire intentionally implements only the app-server methods required by this one-shot contract. Development evidence is pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`: the keyless real-product spec drives the official binary against a loopback Responses service with a non-empty fake key and proves the task, authentication, exact answer, cancellation, approvals, and process-tree exit. A separate Loader composition e2e boots the README-shaped user configuration with no `codex` command available, verifies the fixed provider and foreground-only tool schema, and records zero child starts. A credentialed e2e starts the production provider and real Codex, then obtains a unique answer from the fixed official DeepSeek service through a loopback-only test bridge from Responses to Chat Completions; that bridge is not production functionality or native Codex support for DeepSeek's Chat Completions API. The npm package is a test-only dependency; deployments still supply `codex` on `PATH`.
## Model Experience
@@ -83,7 +83,7 @@ Append-only: the new tool result follows the reusable parent request prefix.
- **One fresh process, thread, and turn per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
- **Host-managed product installation and account state** — a missing or incompatible `codex`, configuration error, or authentication failure is surfaced as a startup or run error; the plugin provides no installer, login flow, or runtime version gate.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.146.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.147.0 protocol baseline requires regenerating upstream schema evidence and rerunning handshake, answer-selection, approval, cancellation, keyless real-product, and credentialed DeepSeek nonce tests.
- **No human approval path** — known unattended approval requests are denied and unknown server requests fail closed; deployments cannot configure an allow policy through this package.
- **Final text only** — reasoning, commentary, intermediate messages, tool traffic, usage, stderr, and workspace diffs remain product-local.
- **No optional shared capabilities** — output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.

View File

@@ -10,7 +10,7 @@
已发布的 `run.result` 恰好启动一个轮次。它只接受与此次运行的线程和轮次匹配的通知,随后等待权威的终止通知 `turn/completed`。以最后一条 `phase: "final_answer"``agentMessage` 为准;若 Codex 没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退。过程说明绝不会取代上述任一答案;成功完成的轮次若没有非空白答案,结果也会判为错误。
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.146.0 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应或是未知服务器请求此次运行就会失败。
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应或是未知服务器请求此次运行就会失败。
本地取消会在结果竞态中胜出并映射为 `aborted`。失败轮次的 `codexErrorInfo` 若为 `contextWindowExceeded`,则映射为 `max-tokens`;其他任何远端中断或失败轮次都映射为 `error`,且本版本不会产生 `refusal``dispose()` 具有幂等性:如果当前的两个标识符均已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,并等待整棵进程树退出。结果失败与独立的清理失败仍彼此分离。
@@ -47,7 +47,7 @@
## 产品兼容性与证据
生产环境的协议层有意只实现这一单次执行契约所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.146.0` / `codex-cli 0.146.0`:无密钥真实产品测试使用非空的伪密钥,驱动官方二进制程序连接回环 Responses 服务,并证明任务、身份验证、精确回答、取消、审批与进程树退出。独立的 Loader 装配 e2e 会在没有可用 `codex` 命令时启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的工具 schema并记录零次子级启动。带密钥 e2e 会启动生产提供方和真实 Codex再通过一个仅限回环、将 Responses 转为 Chat Completions 的测试桥接层,从固定的 DeepSeek 官方服务获得唯一答案;该桥接层既不属于生产功能,也不代表 Codex 原生支持 DeepSeek 的 Chat Completions API。该 NPM 包仅作为测试依赖;部署环境仍需通过 `PATH` 提供 `codex`
生产环境的协议层有意只实现这一单次执行契约所需的 app-server 方法。开发证据锁定在 `@openai/codex@0.147.0` / `codex-cli 0.147.0`:无密钥真实产品测试使用非空的伪密钥,驱动官方二进制程序连接回环 Responses 服务,并证明任务、身份验证、精确回答、取消、审批与进程树退出。独立的 Loader 装配 e2e 会在没有可用 `codex` 命令时启动与 README 同形的用户配置,验证固定提供方与只支持前台执行的工具 schema并记录零次子级启动。带密钥 e2e 会启动生产提供方和真实 Codex再通过一个仅限回环、将 Responses 转为 Chat Completions 的测试桥接层,从固定的 DeepSeek 官方服务获得唯一答案;该桥接层既不属于生产功能,也不代表 Codex 原生支持 DeepSeek 的 Chat Completions API。该 NPM 包仅作为测试依赖;部署环境仍需通过 `PATH` 提供 `codex`
## 模型体验
@@ -83,7 +83,7 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这
- **每次运行均新建一个进程、一个线程和一个轮次**:不支持续接、恢复、池化、进度流或产品会话持久化。
- **产品安装和账户状态由宿主管理**`codex` 缺失或不兼容、配置错误或身份验证失败,都会呈现为启动错误或运行错误;本插件不提供安装程序、登录流程或运行时版本门禁。
- **兼容性由开发证据锁定**:若要从已验证的 0.146.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;部署方无法通过本包配置允许策略。
- **仅返回最终文本**推理、过程说明、中间消息、工具通信、用量信息、stderr 和工作区差异仍只保留在产品内部。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。

View File

@@ -49,7 +49,7 @@
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"@openai/codex": "0.146.0",
"@openai/codex": "0.147.0",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -1,5 +1,5 @@
/**
* Minimal Codex app-server 0.146.0 protocol adapter. The shared JSON-RPC
* Minimal Codex app-server 0.147.0 protocol adapter. The shared JSON-RPC
* transport owns framing and request correlation; this module owns only the
* product methods, current thread/turn association, unattended approval
* responses, and terminal-answer selection.

View File

@@ -109,8 +109,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
const version = await execFileAsync(join(codexBinDir, 'codex'), ['--version'], {
env: { ...process.env, ...env },
})
expect(codexPackage.version).toBe('0.146.0')
expect(version.stdout.trim()).toBe('codex-cli 0.146.0')
expect(codexPackage.version).toBe('0.147.0')
expect(version.stdout.trim()).toBe('codex-cli 0.147.0')
const parent = {
id: 'deepseek-e2e-parent',

View File

@@ -140,18 +140,18 @@ function responseInputTexts(body: Record<string, unknown>): string[] {
})
}
describe('real @openai/codex 0.146.0 product', () => {
describe('real @openai/codex 0.147.0 product', () => {
it('passes the exact task and fake authentication to local Responses and returns exact text', async () => {
const sentinel = 'REAL_CODEX_SENTINEL_0_146_0'
const sentinel = 'REAL_CODEX_SENTINEL_0_147_0'
const task = 'Return the fixture sentinel exactly.'
const { harness, fixture } = await realHarness([
{ kind: 'complete', text: sentinel },
])
expect(codexPackage.version).toBe('0.146.0')
expect(codexPackage.version).toBe('0.147.0')
const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
env: { ...process.env, ...harness.env },
})
expect(version.stdout.trim()).toBe('codex-cli 0.146.0')
expect(version.stdout.trim()).toBe('codex-cli 0.147.0')
const run = await harness.ctx.subagents.start('codex', {
prompt: [{ type: 'text', text: task }],

View File

@@ -86,7 +86,7 @@ function responseObject(text: string): Record<string, unknown> {
}
/**
* Build the minimal Responses SSE event sequence consumed by Codex 0.146.0.
* Build the minimal Responses SSE event sequence consumed by Codex 0.147.0.
* @param text - exact assistant answer.
* @returns ordered response lifecycle events.
*/

View File

@@ -199,7 +199,7 @@ async function initializeWire(): Promise<{
wire.start()
const initializing = wire.initialize(new AbortController().signal)
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.146.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
await initializing
expect(await child.peer.nextMethod('initialized')).toEqual({
jsonrpc: '2.0',
@@ -219,7 +219,7 @@ async function publishRun(
) {
const starting = startCodexRun(request(undefined, signal), runSpec(child, specOverrides))
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.146.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })
@@ -380,7 +380,7 @@ describe('CodexAppServerWire', () => {
requestAttestation: false,
},
})
child.peer.respond(initialize, { userAgent: 'codex-cli 0.146.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
await initializing
await child.peer.nextMethod('initialized')
@@ -855,7 +855,7 @@ describe('run lifecycle and quiescence', () => {
void starting.then(() => { published = true })
const initialize = await child.peer.nextMethod('initialize')
expect(published).toBe(false)
child.peer.respond(initialize, { userAgent: 'codex-cli 0.146.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
expect(published).toBe(false)
@@ -962,7 +962,7 @@ describe('run lifecycle and quiescence', () => {
runSpec(child),
)
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.146.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })
@@ -1032,7 +1032,7 @@ describe('run lifecycle and quiescence', () => {
signal: new AbortController().signal,
})
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.146.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })