Merge pull request #2873 from deepseek-harness/codex/product-subagent-runtime-refresh-codex

feat(subagent): configure Codex provider models
This commit is contained in:
pku-xht
2026-08-25 20:43:31 +08:00
committed by GitHub
49 changed files with 410 additions and 438 deletions
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 .agents/notes/archived/feature/2026-08-18-product-subagent-failure-facts.md
2026-08-18-product-subagent-failure-facts.md: b1d80cf66172ac67d38dbad873fa4cbd970a775c
2026-08-18-product-subagent-failure-facts.zh.md: df4b14b4a243f7768240678b8d434c7aef7d48a7
@@ -1,6 +1,7 @@
# Agent Note: Product subagents expose bounded structured failure facts
Status: implemented
Archived: 2026-08-21
English | [中文](2026-08-18-product-subagent-failure-facts.zh.md)
@@ -1,6 +1,7 @@
# Agent Note: 产品 subagent 公开有界结构化失败事实
Status: implemented
Archived: 2026-08-21
[English](2026-08-18-product-subagent-failure-facts.md) | 中文
+3
View File
@@ -295,6 +295,9 @@
"feature/2026-08-11-web-export-command-and-dialog.i18n.yaml": "sha256:db7d523a2a1f82a86f532661bd2953ee8538d971d91f886e4bd4e0d88f7226b2",
"feature/2026-08-11-web-export-command-and-dialog.md": "sha256:ec44b47589ca7924018dc24f7fa73379a97b8f053d9e8ccce2aebb600230e47b",
"feature/2026-08-11-web-export-command-and-dialog.zh.md": "sha256:ad28e67d397c87300cfe1705ba3d206cc4d054e07f5647c095c718ac8cf4ec98",
"feature/2026-08-18-product-subagent-failure-facts.i18n.yaml": "sha256:0aa7a873fdd878ee7f4b0a850ecf16d7b652b4f85de979acb7efcdf90883b6c1",
"feature/2026-08-18-product-subagent-failure-facts.md": "sha256:f7e05703c44106359798e6e4b76e442a4107b62ff0363554382d4767e4806788",
"feature/2026-08-18-product-subagent-failure-facts.zh.md": "sha256:19d2619fb5b5c6e40305dd82432d837357afa433ab735504ec204a2c25582ce6",
"feature/2026-08-18-web-home-path-tilde.i18n.yaml": "sha256:f151e3e3514f59784fc646c2feb3075dc954c65110d48c2cc482ad486fc0b86f",
"feature/2026-08-18-web-home-path-tilde.md": "sha256:8c7ecf120ff8c81826160acab5fc906a2a0a14213bcd2958343cfea47328d68e",
"feature/2026-08-18-web-home-path-tilde.zh.md": "sha256:3486c5b42aed5bcadf12c62c5e1e6cf7c1b493fc1085ad7d154cdf2ec34076cc",
@@ -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 .agents/notes/implemented/architecture/2026-08-10-product-subagent-providers-in-shared-host.md
2026-08-10-product-subagent-providers-in-shared-host.md: eca1d5b6b9b0e46b39c6c2ef382bfc261d014e44
2026-08-10-product-subagent-providers-in-shared-host.zh.md: 374a3ef72e298bb621fd71d306a4fca16c8000b4
2026-08-10-product-subagent-providers-in-shared-host.md: a998408c4dd01ba8f7ab09eab453a963beb855a4
2026-08-10-product-subagent-providers-in-shared-host.zh.md: cce746399b7ec553d145441cc3d51df3e051ca9b
@@ -14,9 +14,9 @@ The placement decision must preserve two independent facts. Loading a provider m
Product providers remain process-scoped host-plane registrations. The [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) supersedes only this note's former base-bundle installation choice: production `dsh-base` neither depends on nor mounts them. A Profile that opts in installs the selected provider Bundle; its patch mounts the default instance, and the Profile may mount additional named instances on the host plane. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns each row's registry identity: both products accept multiple unique `providerName` values while preserving `codex` and `claude-code` as their defaults. Loading either plugin only registers a dormant backend; the corresponding Codex or Claude process starts on the first actual delegation call. Agent Presets independently contribute ordinary `dsh-tool-subagent` rows whose `provider` and `toolName` values expose exactly the configured instances needed by one agent without changing the Host registry.
Each provider package owns its directly installable Bundle patch and private product runtime. This note continues to own process-wide Host placement whenever either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns the optional Claude Code model and other per-instance configuration. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
Each provider package owns its directly installable Bundle patch and private product runtime. This note continues to own process-wide Host placement whenever either provider is installed. The provider-contract note continues to own each product protocol, result mapping, cancellation, process-tree lifecycle, and evidence tiers. The [named-instance decision](../feature/2026-08-18-product-subagent-named-instances.md) owns both optional product models and other per-instance configuration. The [Agent Preset architecture](2026-08-03-per-session-agent-presets.md) continues to own the Host/Agent split, preset authoring, and the rule that edits affect only newly composed sessions.
Each Bundle delegates executable selection to its package-owned product runtime: the Codex package runs its declared wrapper, while the Claude Code package lets its pinned Agent SDK select the private native executable. Neither provider consults or falls back to a host product command. Profile loading creates no product state, probes no version or authentication, and may supply each mounted Provider instance's deployment configuration, including an optional opaque model where supported and the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing platform payloads and product failures remain local to the attempted delegation.
Each Bundle delegates executable selection to its package-owned product runtime: the Codex package runs its declared wrapper, while the Claude Code package lets its pinned Agent SDK select the private native executable. Neither provider consults or falls back to a host product command. Profile loading creates no product state, probes no version or authentication, and may supply each mounted Provider instance's deployment configuration, including an optional opaque model and the product-specific `permissionMode` values owned by the [non-interactive permissions decision](../feature/2026-08-15-product-subagent-noninteractive-permissions.md), without moving those choices into an Agent Preset or model-facing tool. Missing platform payloads and product failures remain local to the attempted delegation.
## Verification
@@ -14,9 +14,9 @@ Status: implemented
产品提供方仍是进程级的 host plane(宿主平面)注册。[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)只取代本说明原先由 base bundle 安装提供方的选择:生产 `dsh-base` 既不依赖也不挂载它们。选择产品集成的 Profile 会安装目标提供方 Bundle;其 patch 挂载默认实例,而 Profile 可以在 host plane 挂载更多命名实例。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.zh.md)负责每个配置项的注册身份:两个产品都接受多个唯一的 `providerName`,同时保留 `codex``claude-code` 作为默认值。加载任一插件只会注册一个休眠后端;对应的 Codex 或 Claude 进程直到第一次实际委派调用时才启动。Agent Preset 通过普通 `dsh-tool-subagent` 配置项的 `provider``toolName` 准确公开单个 agent 所需的已配置实例,而无需更改 Host 注册表。
每个提供方包都拥有可直接安装的 Bundle patch 与私有产品运行时。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.zh.md)负责可选 Claude Code 模型及其他逐实例配置。[Agent Preset 架构](2026-08-03-per-session-agent-presets.zh.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
每个提供方包都拥有可直接安装的 Bundle patch 与私有产品运行时。本说明继续负责每个已安装提供方的进程级 Host 放置。提供方约定说明继续负责每个产品的协议、结果映射、取消、进程树生命周期与证据层级。[命名实例决策](../feature/2026-08-18-product-subagent-named-instances.zh.md)负责两个可选产品模型及其他逐实例配置。[Agent Preset 架构](2026-08-03-per-session-agent-presets.zh.md)继续负责宿主与 agent 的划分、preset 创作,以及改动只影响新组装会话的规则。
每个 Bundle 都把可执行文件选择交给包自有的产品运行时:Codex 包运行自身声明的 wrapperClaude Code 包则让锁定的 Agent SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令。加载 Profile 不会创建产品状态、探测版本或测试身份验证;它可以提供每个已挂载 Provider 实例的部署配置,包括产品支持时可选的不透明模型,以及由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。平台载荷缺失和产品故障仍局限于发生问题的那次委派。
每个 Bundle 都把可执行文件选择交给包自有的产品运行时:Codex 包运行自身声明的 wrapperClaude Code 包则让锁定的 Agent SDK 选择私有原生可执行文件。两个提供方都不会查询或回退宿主产品命令。加载 Profile 不会创建产品状态、探测版本或测试身份验证;它可以提供每个已挂载 Provider 实例的部署配置,包括可选的不透明模型,以及由[非交互权限决策](../feature/2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责的产品专属 `permissionMode` 值,但不会把这些选择移入 Agent Preset 或面向模型的工具。平台载荷缺失和产品故障仍局限于发生问题的那次委派。
## 验证
@@ -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 .agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md
2026-08-04-claude-code-and-codex-subagent-backends.md: 8af99c9171e073e5901390fed29047b350fe4924
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: 5abc0b16077bcf0be7835b2cf687a1709b53fc81
2026-08-04-claude-code-and-codex-subagent-backends.md: 04b8fcb9c3fd899a485a12ce9c002b8ab5d6bbb3
2026-08-04-claude-code-and-codex-subagent-backends.zh.md: de9248ed7c82266bd6083020ab32190a3dfd8d1f
@@ -12,7 +12,7 @@ The product integrations must not become second owners for task text, cwd, cance
## Decision
The harness publishes two sibling one-shot provider packages whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity, optional instance model where supported, and static tool binding; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their independent optional Bundles and host-plane placement; the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice; the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and safe permission decisions; and the [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns coarse product action categories while the [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) continues to describe Codex's current detailed categories. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.
The harness publishes two sibling one-shot provider packages whose default registry names are `codex` and `claude-code`. This note owns their product protocols, result mapping, and process lifecycle; the [named-instance decision](2026-08-18-product-subagent-named-instances.md) owns Profile-selected provider identity, optional instance model, and static tool binding; the [production-install exclusion decision](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md) owns their independent optional Bundles and host-plane placement; the [product one-shot background decision](2026-08-12-product-subagent-one-shot-background-tasks.md) owns the model-visible scheduling choice; the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) owns each product Provider's Profile-selected mode and safe permission decisions; and the [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns coarse product action categories. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.
Both providers report `inheritsParentContext: false`, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use `backgroundMode: 'one-shot'` and `maxDepth: 'provider-managed'`: the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. `ctx.subagents` owns named-request resolution and paired lifecycle events; `dsh-tool-subagent` owns model-visible scheduling and foreground-versus-Job adaptation; `ctx.jobs` and `dsh-tool-jobs` own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while `dsh-subprocess` owns credential scrubbing, process-tree termination, and whole-tree exit observation.
@@ -34,17 +34,17 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
## Codex provider
`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. Installation, login, `CODEX_HOME`, model selection, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
`@deepseek-ai/dsh-subagent-codex` registers a Profile-selected provider name that defaults to `codex`, resolves the `codex` bin declared by its pinned `@openai/codex@0.149.1` package, and starts that wrapper through the current Node executable with `app-server --stdio`. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host `codex`. Its public configuration contains a non-empty `providerName`, an optional non-empty `model`, an explicit `env` overlay, a positive finite `disposeGraceMs` no greater than the repository's shared `MAX_TIMER_DELAY_MS`, and a three-value native `permissionMode` that defaults to `never`. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral `thread/start`; omission leaves native Codex settings authoritative. Installation, login, `CODEX_HOME`, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize``initialized`, maps the resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no mode or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes `initialize``initialized`, maps the optional model and resolved mode into official `thread/start` fields, and creates an `ephemeral: true` thread. The fixed app-server argv contains no model, mode, or task text. The published run owns exactly one `turn/start`; its thread and turn ids remain private and are never persisted in the parent Session.
`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns Codex error-info categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains `aborted` without a failure diagnostic.
`turn/completed` is the authoritative remote terminal fact. The latest `agentMessage` with `phase: "final_answer"` wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with `phase: null` is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns Codex action categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains `aborted` without a failure diagnostic.
For command and file approvals, the unattended wire 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 grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and `sandboxError`. Codex emits some early `never` rejections and sandbox violations only on structured stderr, so the Provider pipes and forwards stderr unchanged while matching two fixed signatures in a bounded per-run tail; raw stderr never enters the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.149.1 request shape without an offered-decision list falls back to `decline`. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.
An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, detaches the stderr observer, and then rejects `start()` with its fixed operation stage. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, waits for whole-tree exit, and detaches the observer. Independent cleanup failure reports `teardown`; when startup and rollback both fail, the aggregate's top message retains both safe stage lines while the underlying causes remain internal.
Codex 0.147.0 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
Codex 0.149.1 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.
## Claude Code provider
@@ -62,7 +62,7 @@ The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract
Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Codex Loader fixture exposes two named Codex instances and tools; the Claude Code Loader fixture exposes the default Codex tool plus two named Claude Code instances and tools. Both fixtures include generic Job controls and start neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.
The Codex evidence pins `@openai/codex@0.147.0`, `codex-cli 0.147.0`, and all six optional platform aliases. Its real-product spec observes the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, two named instances retain separate environments and modes, and production never resolves a host `codex` from `PATH`. The [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) owns schema, failure, process-outcome, and final presentation evidence.
The Codex evidence pins `@openai/codex@0.149.1`, `codex-cli 0.149.1`, and all six optional platform aliases. Its generated schema proves optional `ThreadStartParams.model`; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host `codex` from `PATH`. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns failure, process-outcome, and final presentation evidence.
The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.
@@ -12,7 +12,7 @@ Status: implemented
## 决策
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex``claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.zh.md)负责 Profile 选择的提供方身份、支持时的可选实例模型与静态工具绑定;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)负责各自独立的可选 Bundle 与 host plane(宿主平面)放置;[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.zh.md)负责模型可见的调度选择;[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责各产品提供方的 Profile 模式选择与安全权限决定;[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责粗粒度产品行动类别,而[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)继续描述 Codex 当前的详细类别。两个包都接受多个命名实例。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品与实例选择仍属于部署配置。
harness 交付两个同级的一次性提供方包,其默认注册名称分别为 `codex``claude-code`。本说明负责它们的产品协议、结果映射和进程生命周期;[命名实例决策](2026-08-18-product-subagent-named-instances.zh.md)负责 Profile 选择的提供方身份、可选实例模型与静态工具绑定;[生产安装排除决策](../simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md)负责各自独立的可选 Bundle 与 host plane(宿主平面)放置;[产品一次性后台任务决策](2026-08-12-product-subagent-one-shot-background-tasks.zh.md)负责模型可见的调度选择;[非交互权限决策](2026-08-15-product-subagent-noninteractive-permissions.zh.md)负责各产品提供方的 Profile 模式选择与安全权限决定;[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责粗粒度产品行动类别。两个包都接受多个命名实例。加载任一提供方都不会启动产品进程,而且每个工具只接受独立文本任务;产品与实例选择仍属于部署配置。
这两个提供方都报告 `inheritsParentContext: false`,不声明任何可选的启动能力,并传递父会话 cwd,但不会复制父级对话。文档所示的工具使用 `backgroundMode: 'one-shot'``maxDepth: 'provider-managed'`:消费方默认在前台收集结果,也可把同一次运行放入通用 Job 运行时,而递归策略仍由进程外产品负责。每次调用都会创建一个全新的产品进程和一次不可续接的产品对话。`ctx.subagents` 负责具名请求解析与成对生命周期事件;`dsh-tool-subagent` 负责模型可见的调度以及前台与 Job 适配;`ctx.jobs``dsh-tool-jobs` 负责 Job id、状态、输出、控制、通知与父级 owner 取消;各产品提供方负责原生结果映射,`dsh-subprocess` 则负责凭证清洗、进程树终止以及整棵进程树的退出观测。
@@ -34,17 +34,17 @@ configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> pro
## Codex 提供方
`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.147.0` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`、显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS``disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。安装、登录、`CODEX_HOME`、模型选择、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approvalreviewersandbox 字段。
`@deepseek-ai/dsh-subagent-codex` 注册由 Profile 选择、默认值为 `codex` 的提供方名称,解析锁定的 `@openai/codex@0.149.1` 包所声明的 `codex` bin,并使用当前 Node 可执行文件加 `app-server --stdio` 启动该 wrapper。Wrapper 会选择私有原生平台载荷;提供方既不解析也不回退宿主 `codex`。其公开配置包含非空的 `providerName`可选的非空 `model`显式的 `env` 覆盖项、须为正有限值且不得大于仓库共享 `MAX_TIMER_DELAY_MS``disposeGraceMs`,以及默认使用 `never` 的三值原生 `permissionMode`。每个命名实例会为自己的运行保留这些已解析值。显式模型会原样传给每个临时 `thread/start`;省略时仍以 Codex 原生设置为权威。安装、登录、`CODEX_HOME`、模型发现或 fallback、基础 URL 和产品会话设置仍由 Codex 原生机制或部署环境负责;所选模式只拥有非交互权限决策中描述的线程 approvalreviewersandbox 字段。
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize``initialized` 握手,把已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
发布前,提供方会验证非空的纯文本任务,在父级工作区中启动受管的 app-server,完成 `initialize``initialized` 握手,把可选模型与已解析模式映射为官方 `thread/start` 字段,并创建一个 `ephemeral: true` 线程。固定 app-server argv 不包含模型、模式或任务文本。已发布的运行只拥有一次 `turn/start`;其线程 ID 与轮次 ID 保持私有,绝不会持久化到父会话。
`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"``agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责 Codex error-info 类别、HTTP status、生命周期阶段、进程结果与终止原因保持。本地取消仍是 `aborted` 且不附带失败诊断。
`turn/completed` 是权威的远端终止事实。以最后一条带有 `phase: "final_answer"``agentMessage` 为准,且选中的消息必须包含非空白文本。若产品没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退,该消息也必须包含非空白文本;过程说明绝不会取代上述任一答案。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责 Codex 行动类别、HTTP status、生命周期阶段、进程结果与终止原因保持。本地取消仍是 `aborted` 且不附带失败诊断。
对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与 `sandboxError` 的安全类别。Codex 的部分早期 `never` 拒绝和 sandbox violation 只写入结构化 stderr,因此提供方会 pipe 并原样转发 stderr,同时在每次运行的有界尾部中匹配两个固定签名;原始 stderr 绝不会进入诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
对于命令与文件审批,无人值守的协议连接会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.149.1 请求形态没有决策选项列表,因此回退到 `decline`。它不授予该轮次请求的任何权限,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。它会记录这些请求、被拒绝的命令/文件 item 与结构化 `sandboxError` 终态的安全类别。产品 stderr 会原样转发给 Host,但既不会被分类,也不会复制进诊断。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败,而不会等待本提供方没有提供的用户界面。
若启动在发布前失败,提供方会关闭协议连接、终止已获取的进程树、等待其退出、移除 stderr observer,然后用固定操作阶段拒绝 `start()`。对已发布的运行执行资源释放时,提供方会尽力中断已知轮次、关闭协议连接、结束标准输入、调用共享的逐级终止机制,等待整棵进程树退出,并移除 observer。独立清理失败会报告 `teardown`;启动与回滚同时失败时,聚合的顶层消息会保留两条安全阶段说明,而底层 cause 仍只在内部可见。
Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSEServer-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
Codex 0.149.1 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端点使用 Chat Completions。因此,带密钥 Codex e2e 会采用一个仅限回环、仅供测试内部使用的桥接层来处理一次不使用工具的随机数请求:真实 Codex 将 Responses 发送到桥接层,桥接层把收到的 Bearer 凭据与提取出的任务转发到固定的 DeepSeek 官方端点,再将真实文本包装进最小化的 Responses SSEServer-Sent Events)生命周期。该桥接层既不是生产代理,也不能作为 Codex 原生连接 DeepSeek Chat Completions 的证据。
## Claude Code 提供方
@@ -62,7 +62,7 @@ Codex 0.147.0 使用 Responses 协议,而 DeepSeek 的公开 OpenAI 兼容端
每个产品都负责覆盖所有分支的包测试、一项必跑的无密钥真实产品测试、一项 Loader 组合 e2e 和一项带密钥 DeepSeek e2e。无密钥产品层级使用被测的确切官方发行版、非空的伪产品密钥、隔离的临时工作区与产品主目录,以及能返回固定答案的回环模型。产品请求缺失、身份验证错误、任务文本被改动、答案不完全一致、真实产品被跳过或受管句柄仍存活,都会使这项必跑测试失败。Codex Loader fixture 会公开两个命名 Codex 实例与工具;Claude Code Loader fixture 会公开默认 Codex 工具以及两个命名 Claude Code 实例与工具。两个 fixture 都包含通用 Job 控制工具,而且不会启动任何产品进程。带密钥层级会使用仅在运行时提供的密钥启动同一生产提供方与真实产品,要求从固定的 DeepSeek 官方服务取得唯一随机数,并再次证明完全停稳;仅当本地操作者未提供密钥时才会自行跳过,而受信任的 CI 会预检该 secret。
Codex 证据会锁定 `@openai/codex@0.147.0``codex-cli 0.147.0` 与六个平台 alias。其真实产品测试会观测包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,两个命名实例会保留彼此独立的环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)负责 schema、失败、进程结果与最终呈现证据。
Codex 证据会锁定 `@openai/codex@0.149.1``codex-cli 0.149.1` 与六个平台 alias。生成 schema 会证明可选的 `ThreadStartParams.model`;真实产品测试会观测省略模型继承、两个显式实例模型、包内 wrapper argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、原生权限模式、测试拥有临时存储中的显式危险绕过写入,以及 wrapper/原生整棵进程树退出。独立 wrapper fixture 会证明载荷缺失时不回退宿主命令,命名实例会保留彼此独立的模型、环境与模式,生产环境也不会从 `PATH` 解析宿主 `codex`。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责失败、进程结果与最终呈现证据。
带密钥 Codex e2e 会注册生产提供方,启动同样的真实 app-server,并通过上述测试专用桥接层请求一个随机数。该测试固定外部端点与模型,不存储任何凭据或请求载荷,要求上游恰好完成一次响应,将去除首尾空白后的产品答案与该随机数逐字节比较,并等待所有受管句柄退出。
@@ -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 .agents/notes/implemented/feature/2026-08-15-product-subagent-noninteractive-permissions.md
2026-08-15-product-subagent-noninteractive-permissions.md: f9fe1e0af4f65b83a53127a64f6475861a13f0a1
2026-08-15-product-subagent-noninteractive-permissions.zh.md: 2acb1bf095a23022fc761d254f0e515e1acf1588
2026-08-15-product-subagent-noninteractive-permissions.md: 8ce71cbf156d3e22a22fa8ad526b838ec1a5effe
2026-08-15-product-subagent-noninteractive-permissions.zh.md: e6471b2ce1c8ebbb767254df2426969a3da36f78
@@ -32,7 +32,7 @@ Every query disables `AskUserQuestion`. Non-bypass permission callbacks deny ins
### Codex
Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.147.0. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
Codex defaults to `never` and accepts the three native non-interactive modes exposed by Codex 0.149.1. The Provider starts the fixed app-server command, then maps the selected mode into official `thread/start` fields because CLI-global permission flags do not configure threads created later by an app-server client:
| Value | `thread/start` fields | Native behavior |
| --- | --- | --- |
@@ -40,13 +40,13 @@ Codex defaults to `never` and accepts the three native non-interactive modes exp
| `approve-for-me` | `approvalPolicy: on-request`, `approvalsReviewer: auto_review`, `sandbox: workspace-write` | Route permission requests through Codex automatic review. |
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`, `sandbox: danger-full-access` | Skip approval and sandbox enforcement. |
The Provider overrides only those thread fields. `CODEX_HOME`, project configuration, model/provider selection, MCP, hooks, skills, authentication, and sandbox facts not selected by the mode remain native Codex state. The wire still denies any unexpected approval, permission, user-input, or MCP request rather than opening a dynamic allow path.
The Provider overrides only those permission and sandbox fields. An optional instance-level model is a separate direct `thread/start` override; `CODEX_HOME`, project configuration, model-provider selection, MCP, hooks, skills, authentication, and sandbox facts not selected by the mode remain native Codex state. The wire still denies any unexpected approval, permission, user-input, or MCP request rather than opening a dynamic allow path.
### Failure diagnostic
`SubagentResult` carries an optional `diagnostic` for provider-authored, non-assistant failure detail. A Provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads before producing it. The shared out-of-process result boundary limits the complete text to 4096 UTF-8 bytes and marks truncation without splitting a character. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns Claude Code's non-permission action categories, while the [structured failure-facts decision](2026-08-18-product-subagent-failure-facts.md) continues to own Codex's current categories; both retain lifecycle stages and process outcomes in the same field.
`SubagentResult` carries an optional `diagnostic` for provider-authored, non-assistant failure detail. A Provider removes tool inputs, file contents, environment values, credentials, and raw protocol payloads before producing it. The shared out-of-process result boundary limits the complete text to 4096 UTF-8 bytes and marks truncation without splitting a character. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns both products' non-permission action categories, lifecycle stages, HTTP facts, and process outcomes carried by the same field.
Each product's permission fact contains only the effective mode, request category, unattended decision, and a fixed safe reason. Claude Code derives those facts from SDK callbacks and `permission_denied` messages. Codex derives them from app-server requests, declined items, `sandboxError`, and two fixed permission signatures in a bounded stderr tail; raw stderr is still forwarded to the Host but never copied into the diagnostic. Both Providers place their structured failure line before the latest contributing permission fact. A successful result returns only the strict final answer; local cancellation remains `aborted` without permission detail; an unpublished startup failure still rejects `start()`. The Provider never adds either diagnostic fact to assistant output, structured output, or `subagent/end.lastAssistantMessage`.
Each product's permission fact contains only the effective mode, request category, unattended decision, and a fixed safe reason. Claude Code derives those facts from SDK callbacks and `permission_denied` messages. Codex derives them from app-server requests, declined items, and structured `sandboxError` terminals. Raw stderr is forwarded to the Host but is neither classified nor copied into the diagnostic. Both Providers place their failure line before the latest contributing permission fact. A successful result returns only the strict final answer; local cancellation remains `aborted` without permission detail; an unpublished startup failure still rejects `start()`. The Provider never adds either diagnostic fact to assistant output, structured output, or `subagent/end.lastAssistantMessage`.
The foreground consumer presents the stop-reason headline, then the optional diagnostic, then any partial assistant output. The one-shot background adapter stores the same diagnostic beside the stop reason in the failed Job detail. Providers that omit the field retain their previous behavior.
@@ -63,7 +63,7 @@ The foreground consumer presents the stop-reason headline, then the optional dia
## Verification
Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.241 and Claude Code 2.1.241 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, fixed stderr signatures produce safe diagnostics, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter.
Package tests pin every allowed and rejected Config value, the exact SDK and app-server field mappings, dangerous confirmations, unattended terminal responses, diagnostic sanitization and UTF-8 bound, successful-result omission, concurrent-run isolation, foreground ordering, Job detail, stderr observer disposal, and process cleanup. The real Claude Agent SDK 0.3.241 and Claude Code 2.1.241 fixture proves its safe default, restricted denial, explicit bypass, and whole-tree quiescence. The real Codex 0.149.1 app-server fixture proves that thread-level `never` overrides ambient `on-request`, automatic review starts, dangerous bypass writes only inside suite-owned temporary storage, a rejected escalation leaves no side effect or raw command or path in the diagnostic, stderr remains Host-only, and the wrapper/native tree exits. Loader composition proves non-default modes can be published without starting either product, and the keyless ACP snapshot records each product's failure diagnostic through foreground and Job presentation while the model-facing product tool schemas contain no permission parameter.
## Alternatives considered
@@ -32,7 +32,7 @@ Claude Code 默认使用 `dontAsk`,而且只接受锁定版本 Agent SDK 支
### Codex
Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
Codex 默认使用 `never`,并接受 Codex 0.149.1 公开的三种原生非交互模式。提供方启动固定的 app-server 命令,再把所选模式映射为官方 `thread/start` 字段,因为 CLI 全局权限 flag 不会配置之后由 app-server 客户端创建的线程:
| 值 | `thread/start` 字段 | 原生行为 |
| --- | --- | --- |
@@ -40,13 +40,13 @@ Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交
| `approve-for-me` | `approvalPolicy: on-request``approvalsReviewer: auto_review``sandbox: workspace-write` | 由 Codex 自动评审权限请求。 |
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never``sandbox: danger-full-access` | 跳过审批与 sandbox。 |
提供方只覆盖这些线程字段。`CODEX_HOME`、项目配置、模型provider 选择、MCP、hook、skill、身份验证,以及模式未选择的 sandbox 事实仍属于 Codex 原生状态。wire 仍会拒绝任何意外到达的审批、权限、用户输入或 MCP 请求,而不会开放动态 allow 通道。
提供方只覆盖这些权限与 sandbox 字段。可选的实例级模型是独立的直接 `thread/start` 覆盖;`CODEX_HOME`、项目配置、模型 provider 选择、MCP、hook、skill、身份验证,以及模式未选择的 sandbox 事实仍属于 Codex 原生状态。wire 仍会拒绝任何意外到达的审批、权限、用户输入或 MCP 请求,而不会开放动态 allow 通道。
### 失败诊断
`SubagentResult` 携带可选的 `diagnostic`,用于提供方产生且不属于 assistant 内容的失败说明。提供方在生成它之前会排除工具输入、文件内容、环境值、凭证与原始协议载荷。共享的进程外结果边界会把完整文本限制在 4096 个 UTF-8 字节以内,并在不切断字符的前提下标记截断。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责 Claude Code 的非权限行动类别,[结构化失败事实决策](2026-08-18-product-subagent-failure-facts.zh.md)继续负责 Codex 的当前类别;二者都在同一字段中保留生命周期阶段与进程结果。
`SubagentResult` 携带可选的 `diagnostic`,用于提供方产生且不属于 assistant 内容的失败说明。提供方在生成它之前会排除工具输入、文件内容、环境值、凭证与原始协议载荷。共享的进程外结果边界会把完整文本限制在 4096 个 UTF-8 字节以内,并在不切断字符的前提下标记截断。[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)负责两个产品通过同一字段承载的非权限行动类别、生命周期阶段、HTTP 事实与进程结果。
每个产品的权限事实都只包含有效模式、请求类别、无人值守决定与固定的安全原因。Claude Code 从 SDK 回调和 `permission_denied` 消息取得这些事实。Codex 从 app-server 请求、被拒绝的 item`sandboxError` 与每次运行有界 stderr 尾部中的两个固定权限签名取得事实原始 stderr 仍会转发给 Host,但绝不会复制进诊断。两个提供方都会把结构化失败行放在最新参与失败的权限事实之前。成功结果只返回严格的最终答案;本地取消仍以 `aborted` 结算且不附带权限说明;未发布的启动失败仍会拒绝 `start()`。提供方绝不会把任一诊断事实写入 assistant 输出、结构化输出或 `subagent/end.lastAssistantMessage`
每个产品的权限事实都只包含有效模式、请求类别、无人值守决定与固定的安全原因。Claude Code 从 SDK 回调和 `permission_denied` 消息取得这些事实。Codex 从 app-server 请求、被拒绝的 item 与结构化 `sandboxError` 终态取得事实原始 stderr 仍会转发给 Host,但既不会被分类,也绝不会复制进诊断。两个提供方都会把失败行放在最新参与失败的权限事实之前。成功结果只返回严格的最终答案;本地取消仍以 `aborted` 结算且不附带权限说明;未发布的启动失败仍会拒绝 `start()`。提供方绝不会把任一诊断事实写入 assistant 输出、结构化输出或 `subagent/end.lastAssistantMessage`
前台消费方依次呈现终止原因标题、可选诊断和任何部分 assistant 输出。一次性后台适配器会在失败 Job 的 detail 中,把同一诊断与终止原因一起保存。没有填写该字段的提供方保持原有行为。
@@ -63,7 +63,7 @@ Codex 默认使用 `never`,并接受 Codex 0.147.0 公开的三种原生非交
## Verification
包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、固定 stderr 签名产生安全诊断,而且 wrappernative 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。
包测试固定所有允许与拒绝的 Config 值、准确的 SDK 与 app-server 字段映射、危险确认、无人值守终态、诊断脱敏与 UTF-8 上限、成功结果不携带诊断、并发运行隔离、前台顺序、Job detail、stderr observer 释放和进程清理。真实 Claude Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 证明其安全默认、受限拒绝、显式 bypass 与整棵进程树完全停稳。真实 Codex 0.149.1 app-server fixture 证明线程级 `never` 覆盖环境中的 `on-request`、自动评审可以启动、危险绕过只在测试拥有的临时存储中写入、被拒绝的提权不会留下副作用且诊断不含原始命令或路径、stderr 只供 Host 观测,而且 wrappernative 进程树会退出。Loader 组装证明非默认模式可以在不启动任一产品的情况下发布;无密钥 ACP snapshot 则记录每个产品的失败诊断如何经过前台与 Job 呈现,同时面向模型的产品工具 schema 不包含权限参数。
## Alternatives considered
@@ -1,6 +0,0 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# 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 .agents/notes/implemented/feature/2026-08-18-product-subagent-failure-facts.md
2026-08-18-product-subagent-failure-facts.md: e47fc3246ce20e988cd693b2eb04f785d37bcb27
2026-08-18-product-subagent-failure-facts.zh.md: 5efd15e0e22d38b0100a96fc020ba3671fd257ed
@@ -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 .agents/notes/implemented/feature/2026-08-18-product-subagent-named-instances.md
2026-08-18-product-subagent-named-instances.md: fd4f95dc9de0cc03a95f8f1dcbd6dcded258dc5f
2026-08-18-product-subagent-named-instances.zh.md: 1d8395a21824501c5f26bdbec9c9d7f25d3bf20f
2026-08-18-product-subagent-named-instances.md: 749267e311cbcdebc7a6b8ad84e046fe21acf6fd
2026-08-18-product-subagent-named-instances.zh.md: d39fdd46a30aa665715d782162ddaaaac3a31f3e
@@ -6,17 +6,17 @@ English | [中文](2026-08-18-product-subagent-named-instances.zh.md)
## Problem
A Profile can mount one Cordis plugin package in multiple rows, but the Codex and Claude Code product providers previously registered every row under one fixed product name. A second row therefore failed as a duplicate before its distinct model where supported, permission mode, environment, or process-release settings could become usable. Deriving an implicit name from those settings would create a second identity rule, while choosing a provider during a tool call would let model input select deployment authority.
A Profile can mount one Cordis plugin package in multiple rows, but the Codex and Claude Code product providers previously registered every row under one fixed product name. A second row therefore failed as a duplicate before its distinct model, permission mode, environment, or process-release settings could become usable. Deriving an implicit name from those settings would create a second identity rule, while choosing a provider during a tool call would let model input select deployment authority.
The existing subagent registry already owns unique provider names, reversible registration, lifecycle events, and holder-owned published runs. The existing `dsh-tool-subagent` configuration already binds one provider name to one model-visible tool name. Product providers need to expose the missing Profile-owned identity without adding another registry or selection protocol.
## Decision
Each product provider Config owns a non-empty `providerName`; the defaults remain `codex` and `claude-code`. The resolved name is fixed when the plugin row loads and becomes the Provider object's `name`; registration, lookup, lifecycle events, run logs, and HMR removal therefore use the same value. Each mounted row retains its own `permissionMode`, `env`, `disposeGraceMs`, and run resources. Claude Code rows also retain an optional non-empty `model`; an explicit value is fixed for every query from that instance, while omission leaves native settings authoritative.
Each product provider Config owns a non-empty `providerName`; the defaults remain `codex` and `claude-code`. The resolved name is fixed when the plugin row loads and becomes the Provider object's `name`; registration, lookup, lifecycle events, run logs, and HMR removal therefore use the same value. Each mounted row retains its own optional non-empty `model`, `permissionMode`, `env`, `disposeGraceMs`, and run resources. An explicit model is fixed for every query or thread from that instance, while omission leaves the native product settings authoritative.
Profiles may mount multiple Codex or Claude Code rows when every row uses a distinct `providerName`. Each `dsh-tool-subagent` row continues to bind its existing `provider` field to that exact name and exposes an independently configured `toolName`. Tool calls carry no provider selector, alias, or permission input. A duplicate provider name fails through the existing `DUPLICATE_PROVIDER` path and leaves the first registration intact.
Removing one provider row blocks new starts and removes only tools bound to that name. Runs already published by the removed instance remain owned by their holders and settle or dispose independently. Sibling instances remain registered and keep their own configured model where supported, environment, native permission mode, cancellation controller, product process, and cleanup grace.
Removing one provider row blocks new starts and removes only tools bound to that name. Runs already published by the removed instance remain owned by their holders and settle or dispose independently. Sibling instances remain registered and keep their own configured model, environment, native permission mode, cancellation controller, product process, and cleanup grace.
### Ownership and lifecycle
@@ -25,11 +25,11 @@ Removing one provider row blocks new starts and removes only tools bound to that
| Provider instance name | Product Provider Config | One immutable registry name per mounted row, with the existing default when omitted |
| Name uniqueness and lifecycle events | `ctx.subagents` | Duplicate registration fails; disposal removes only the matching name |
| Model-visible tool name and binding | `dsh-tool-subagent` Config | One static tool resolves one configured provider name |
| Model where supported, permission, environment, and process cleanup | One Provider instance | Concurrent runs and sibling instances do not share deployment configuration or run resources |
| Model, permission, environment, and process cleanup | One Provider instance | Concurrent runs and sibling instances do not share deployment configuration or run resources |
## Verification
Both product packages pin their default and custom names, empty-name rejection, duplicate rollback, actual-name diagnostics, two concurrent instances with different permission modes, environments, and cleanup grace, cancellation isolation, and removal of one instance while its published run remains valid. Claude Code additionally pins empty-model rejection, omitted-model inheritance, two explicit models, and exact per-query isolation through the real SDK/CLI fixture. Public Loader compositions mount two rows and two distinct tools for each product without starting either product, while keyless ACP snapshots pin the four-tool combined roster and the absence of dynamic provider or model parameters.
Both product packages pin their default and custom names, empty-name and empty-model rejection, duplicate rollback, actual-name diagnostics, two concurrent instances with different models, permission modes, environments, and cleanup grace, cancellation isolation, and removal of one instance while its published run remains valid. Official product fixtures prove omitted-model inheritance and exact per-query or per-thread model isolation. Public Loader compositions mount two rows and two distinct tools for each product without starting either product, while keyless ACP snapshots pin the four-tool combined roster and the absence of dynamic provider or model parameters.
## Alternatives considered
@@ -43,6 +43,6 @@ Both product packages pin their default and custom names, empty-name rejection,
## Consequences
A Profile can expose several Codex and Claude Code tools backed by separate native permission modes and environments, and Claude Code instances may additionally fix separate models, while existing configurations continue to resolve `codex` and `claude-code`. Provider and tool names remain independent configuration facts, so changing one requires updating the binding that refers to it.
A Profile can expose several Codex and Claude Code tools backed by separate models, native permission modes, and environments while existing configurations continue to resolve `codex` and `claude-code`. Provider and tool names remain independent configuration facts, so changing one requires updating the binding that refers to it.
The design adds no runtime renaming, model-visible provider or model selector, generated tool name, persistent instance directory, shared process pool, model discovery, fallback, or compatibility alias. Correct multi-instance configurations require unique provider names and unique tool names; duplicate tool-name waiting remains a separate limitation.
@@ -6,17 +6,17 @@ Status: implemented
## 问题
Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与 Claude Code 产品提供方此前会把每个配置项都注册到一个固定产品名称下。因此,第二个配置项会在其支持时使用的独立模型、权限模式、环境或进程释放设置可用前因名称重复而失败。根据这些设置隐式派生名称会建立第二套身份规则,而在工具调用期间选择提供方会让模型输入决定部署权限。
Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与 Claude Code 产品提供方此前会把每个配置项都注册到一个固定产品名称下。因此,第二个配置项会在其独立模型、权限模式、环境或进程释放设置可用前因名称重复而失败。根据这些设置隐式派生名称会建立第二套身份规则,而在工具调用期间选择提供方会让模型输入决定部署权限。
现有 subagent 注册表已经拥有提供方名称唯一性、可逆注册、生命周期事件和由持有方拥有的已发布运行。现有 `dsh-tool-subagent` 配置也已经把一个提供方名称绑定到一个模型可见工具名称。产品提供方只需公开缺失的 Profile 所有身份,无需增加另一套注册表或选择协议。
## 决策
每个产品提供方 Config 都拥有非空的 `providerName`;默认值仍分别为 `codex``claude-code`。插件配置项加载时会固定解析后的名称,并把它作为 Provider 对象的 `name`;注册、查找、生命周期事件、运行日志和 HMR(热模块替换)移除因此使用同一个值。每个已挂载配置项保留自己的 `permissionMode``env``disposeGraceMs` 和运行资源。Claude Code 配置项还会保留可选的非空 `model`;显式值为该实例的每次 query 固定,省略时仍以原生设置为权威。
每个产品提供方 Config 都拥有非空的 `providerName`;默认值仍分别为 `codex``claude-code`。插件配置项加载时会固定解析后的名称,并把它作为 Provider 对象的 `name`;注册、查找、生命周期事件、运行日志和 HMR(热模块替换)移除因此使用同一个值。每个已挂载配置项保留自己的可选非空 `model``permissionMode``env``disposeGraceMs` 和运行资源。显式模型会为该实例的每次 query 或线程固定,省略时仍以原生产品设置为权威。
当每个配置项使用不同的 `providerName` 时,Profile 可以挂载多个 Codex 或 Claude Code 配置项。每个 `dsh-tool-subagent` 配置项继续用已有的 `provider` 字段绑定这个准确名称,并公开独立配置的 `toolName`。工具调用不携带提供方选择器、别名或权限输入。重复提供方名称沿用现有 `DUPLICATE_PROVIDER` 路径失败,而且不会替换第一个注册项。
移除一个提供方配置项会阻止新的启动,并且只移除绑定到该名称的工具。该实例已经发布的运行仍由其持有方拥有,并会独立结算或 dispose(资源释放)。兄弟实例继续保持注册,并保留各自支持时配置的模型、环境、原生权限模式、取消控制器、产品进程和清理宽限期。
移除一个提供方配置项会阻止新的启动,并且只移除绑定到该名称的工具。该实例已经发布的运行仍由其持有方拥有,并会独立结算或 dispose(资源释放)。兄弟实例继续保持注册,并保留各自配置的模型、环境、原生权限模式、取消控制器、产品进程和清理宽限期。
### 所有权与生命周期
@@ -25,11 +25,11 @@ Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与
| 提供方实例名称 | 产品提供方 Config | 每个已挂载配置项拥有一个不可变注册名称;省略时使用现有默认值 |
| 名称唯一性与生命周期事件 | `ctx.subagents` | 重复注册失败;资源释放只移除匹配名称 |
| 模型可见工具名称与绑定 | `dsh-tool-subagent` Config | 一个静态工具解析一个已配置的提供方名称 |
| 支持时的模型、权限、环境与进程清理 | 一个提供方实例 | 并发运行与兄弟实例不共享部署配置或运行资源 |
| 模型、权限、环境与进程清理 | 一个提供方实例 | 并发运行与兄弟实例不共享部署配置或运行资源 |
## 验证
两个产品包测试都会固定默认与自定义名称、空名称拒绝、重复注册回滚、实际名称诊断、使用不同权限模式、环境与清理宽限期的两个并发实例、取消隔离,以及移除一个实例后其已发布运行仍然有效。Claude Code 还会固定空模型拒绝、省略模型继承、两个显式模型,以及通过真实 SDK/CLI fixture 证明逐 query 精确隔离。公共 Loader 组合会为每个产品挂载两个配置项与两个不同工具,而且不启动任一产品;无密钥 ACP 快照固定最终四工具组合,并证明没有动态提供方或模型参数。
两个产品包测试都会固定默认与自定义名称、空名称和空模型拒绝、重复注册回滚、实际名称诊断、使用不同模型、权限模式、环境与清理宽限期的两个并发实例、取消隔离,以及移除一个实例后其已发布运行仍然有效。官方产品 fixture 会证明省略模型继承,以及逐 query 或逐线程的准确模型隔离。公共 Loader 组合会为每个产品挂载两个配置项与两个不同工具,而且不启动任一产品;无密钥 ACP 快照固定最终四工具组合,并证明没有动态提供方或模型参数。
## 考虑过的替代方案
@@ -43,6 +43,6 @@ Profile 可以用多个配置项挂载同一个 Cordis 插件包,但 Codex 与
## 结果
Profile 可以公开多个由不同原生权限模式与环境支持的 Codex 与 Claude Code 工具,Claude Code 实例还可分别固定模型,而现有配置仍会解析为 `codex``claude-code`。提供方名称与工具名称继续是彼此独立的配置事实,因此修改其中一项时必须同时更新引用它的绑定。
Profile 可以公开多个由不同模型、原生权限模式与环境支持的 Codex 与 Claude Code 工具,而现有配置仍会解析为 `codex``claude-code`。提供方名称与工具名称继续是彼此独立的配置事实,因此修改其中一项时必须同时更新引用它的绑定。
本设计不增加运行时改名、模型可见的提供方或模型选择器、自动生成的工具名称、持久实例目录、共享进程池、模型发现、fallback 或兼容别名。正确的多实例配置要求提供方名称与工具名称都保持唯一;重复工具名称的等待问题仍是独立限制。
@@ -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 .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md
2026-08-08-native-windows-pull-request-ci.md: 24f1a5fa0c72255f678b837c910eb779445dee27
2026-08-08-native-windows-pull-request-ci.zh.md: 805974192577b835b565bd42688c574473ac63bf
2026-08-08-native-windows-pull-request-ci.md: dad23f1a49393fbfc3c0798407ed9abe21ff3df2
2026-08-08-native-windows-pull-request-ci.zh.md: 0d81b160fd40ae0a26569351bca6e631aab9a8de
@@ -32,7 +32,7 @@ Windows durable JSONL paths keep drive roots in native spelling and apply the ex
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.md) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write.
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.147.0 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.149.1 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
## Alternatives considered
@@ -32,7 +32,7 @@ Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../bug-fix/2026-08-03-cli-signal-shutdown-escalation.zh.md)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.147.0 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.149.1 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
## 曾考虑的替代方案
@@ -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 .agents/notes/implemented/simplification/2026-08-12-production-dsh-excludes-product-subagent-providers.md
2026-08-12-production-dsh-excludes-product-subagent-providers.md: aaa6ddc70a0320c85d0d784441dfc82d74831a90
2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 37b861ad879995ac7dbd91903cf92332a83f9d92
2026-08-12-production-dsh-excludes-product-subagent-providers.md: 551cf86bd0d1901e5ad8e33588b7bf274cb2c8fe
2026-08-12-production-dsh-excludes-product-subagent-providers.zh.md: 611c6192c501f6289911c18c36ca21a31c57e2cd
@@ -16,7 +16,7 @@ The two Bundles remain independent. The Codex Bundle owns the pinned official wr
## Verification
Package tests pin both Bundle manifests, published patches, exact self-provider rows, and product runtime dependencies. Claude coverage pins Agent SDK 0.3.241, Claude Code 2.1.241, all eight platform packages, SDK-selected execution, and missing-payload failure without host fallback. Codex coverage pins wrapper 0.147.0, all six platform aliases, package-declared execution, native descendant quiescence, and the same missing-payload behavior. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Package/base assertions plus actual pnpm production evidence prove the default and selected-product dependency boundaries, while real Bundle-patch and Agent-Preset composition covers none, either product, both, the tool-grant intersection, later-Session adoption, and zero startup processes.
Package tests pin both Bundle manifests, published patches, exact self-provider rows, and product runtime dependencies. Claude coverage pins Agent SDK 0.3.241, Claude Code 2.1.241, all eight platform packages, SDK-selected execution, and missing-payload failure without host fallback. Codex coverage pins wrapper 0.149.1, all six platform aliases, package-declared execution, native descendant quiescence, and the same missing-payload behavior. Workspace validation derives each published patch from its Bundle declaration rather than a package catalog. Package/base assertions plus actual pnpm production evidence prove the default and selected-product dependency boundaries, while real Bundle-patch and Agent-Preset composition covers none, either product, both, the tool-grant intersection, later-Session adoption, and zero startup processes.
## Alternatives considered
@@ -16,7 +16,7 @@ Status: implemented
## 验证
包测试会固定两个 Bundle 的 manifest、发布 patch、准确的自身提供方行与产品运行时依赖。Claude 覆盖会固定 Agent SDK 0.3.241、Claude Code 2.1.241、八个平台包、SDK 所选执行路径,以及载荷缺失时不回退宿主命令的失败。Codex 覆盖会固定 wrapper 0.147.0、六个平台 alias、包声明的执行路径、原生后代进程停稳,以及同样的载荷缺失行为。工作区验证会从 Bundle 声明派生每份发布 patch,而非维护包目录。包与 base 断言加上实际 pnpm 生产证据会证明默认与所选产品的依赖边界;真实 Bundle patch 与 Agent Preset 组装则覆盖未安装、任一单包、双包、工具授权交集、后续 Session 采纳以及零启动进程。
包测试会固定两个 Bundle 的 manifest、发布 patch、准确的自身提供方行与产品运行时依赖。Claude 覆盖会固定 Agent SDK 0.3.241、Claude Code 2.1.241、八个平台包、SDK 所选执行路径,以及载荷缺失时不回退宿主命令的失败。Codex 覆盖会固定 wrapper 0.149.1、六个平台 alias、包声明的执行路径、原生后代进程停稳,以及同样的载荷缺失行为。工作区验证会从 Bundle 声明派生每份发布 patch,而非维护包目录。包与 base 断言加上实际 pnpm 生产证据会证明默认与所选产品的依赖边界;真实 Bundle patch 与 Agent Preset 组装则覆盖未安装、任一单包、双包、工具授权交集、后续 Session 采纳以及零启动进程。
## 考虑过的替代方案
@@ -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 .agents/notes/implemented/simplification/2026-08-21-product-subagent-minimal-diagnostics.md
2026-08-21-product-subagent-minimal-diagnostics.md: a4d51c325738e6e3e6c74561deb877c9bca3c016
2026-08-21-product-subagent-minimal-diagnostics.zh.md: 409b548e0e6169479cec892d349dc8c969b24637
2026-08-21-product-subagent-minimal-diagnostics.md: b99ed487911a7ac9bf7c0751ea02290ff92cad6a
2026-08-21-product-subagent-minimal-diagnostics.zh.md: 46f948334df0268a095f8cb34ab1a7707eee811b
@@ -26,7 +26,20 @@ Claude Code maps Agent SDK results into five categories:
The diagnostic also retains the derived `query-start`, `query-run`, `process`, or `teardown` stage and independently observed exit code and signal. A contributing permission decision follows the failure line. Successful completion and local cancellation expose no failure diagnostic, and original SDK text remains only on the internal cause chain and in Host observation.
Codex currently retains the version-pinned categories documented by the [structured failure-facts decision](../feature/2026-08-18-product-subagent-failure-facts.md). The two providers may adopt the minimal mapping independently because neither shares a public error enum or asks a consumer to branch on category text.
Codex maps app-server failures into eight categories:
| Category | Safe input |
| --- | --- |
| `limit` | Context, session-budget, or usage limits |
| `access-policy` | Authentication, cyber-policy, product-policy, or sandbox failures |
| `service` | Overload or internal service failures |
| `transport` | HTTP and response-stream connection failures or exhausted attempts |
| `product-error` | Invalid requests, rollback, active-turn, or other product failures |
| `invalid-result` | A completed turn without a nonblank final answer |
| `process` | The managed app-server exits before another terminal result |
| `unknown` | Startup, teardown, malformed protocol values, or failures without a more specific safe fact |
The Codex diagnostic retains `initialize`, `thread-start`, `turn-start`, `turn`, `process`, or `teardown`, plus applicable numeric HTTP status and independently observed exit code and signal. `contextWindowExceeded` still maps the shared stop reason to `max-tokens`; every other category remains `error`. Only structured protocol facts contribute permission detail. Product stderr is Host-only observation and is neither classified nor copied into the result.
### Ownership and lifecycle
@@ -40,7 +53,7 @@ Codex currently retains the version-pinned categories documented by the [structu
## Verification
Claude Code package tests cover every coarse category, all four stages, unknown structured values, permission ordering, raw-text exclusion, success and cancellation omission, concurrent-run isolation, and independent exit code and signal fields. The real Agent SDK 0.3.241 and Claude Code 2.1.241 fixture produces an actual max-turns limit, process failure, permission denial, strict final answer, cancellation, and whole-tree quiescence. Loader and keyless product compositions continue to expose static tools without a diagnostic parser or model-visible category input.
Claude Code package tests cover every coarse category, all four stages, unknown structured values, permission ordering, raw-text exclusion, success and cancellation omission, concurrent-run isolation, and independent exit code and signal fields. The real Agent SDK 0.3.241 and Claude Code 2.1.241 fixture produces an actual max-turns limit, process failure, permission denial, strict final answer, cancellation, and whole-tree quiescence. Codex package tests cover every coarse category, all six stages, applicable HTTP status, structured permission ordering, stderr exclusion, success and cancellation omission, concurrency, and cleanup aggregation. The real 0.149.1 app-server fixture produces service, product-error, process, final-answer, model-isolation, cancellation, and quiescence evidence. Loader and keyless product compositions continue to expose static tools without a diagnostic parser or model-visible category input.
## Alternatives considered
@@ -54,6 +67,6 @@ Claude Code package tests cover every coarse category, all four stages, unknown
## Consequences
Claude Code upgrades no longer require a model-visible promise for every SDK error subtype. Parents still distinguish a limit, general product failure, invalid result, managed process exit, and unknown failure while retaining stage, permission, and process facts.
Product runtime upgrades no longer require a model-visible promise for every SDK or app-server error member. Parents still distinguish limits, access and policy restrictions, service and transport failures, general product failures, invalid results, managed process exits, and unknown failures while retaining applicable stage, permission, HTTP, and process facts.
The diagnostic remains safe display text rather than a recovery protocol. This change adds no raw error forwarding, fallback model, automatic retry, product session persistence, public structured result field, or dynamic provider and model selection.
@@ -26,7 +26,20 @@ Claude Code 会把 Agent SDK 结果映射为五种类别:
诊断还会保留派生的 `query-start``query-run``process``teardown` 阶段,以及分别观测到的退出码与信号。参与失败的权限决定会跟在失败行之后。成功完成与本地取消不公开失败诊断,原始 SDK 文本只留在内部 cause 链和 Host 观测中。
Codex 当前仍保留[结构化失败事实决策](../feature/2026-08-18-product-subagent-failure-facts.zh.md)记录的锁定版本类别。两个提供方可以分别采用最小映射,因为它们既不共享公共错误枚举,也不要求消费方根据类别文本分支。
Codex 会把 app-server 失败映射为八种类别:
| 类别 | 安全输入 |
| --- | --- |
| `limit` | 上下文、会话预算或用量限制 |
| `access-policy` | 身份验证、网络安全策略、产品策略或 sandbox 失败 |
| `service` | 过载或内部服务失败 |
| `transport` | HTTP 与响应 stream 连接失败或尝试耗尽 |
| `product-error` | 无效请求、回滚、活动轮次或其他产品失败 |
| `invalid-result` | 已完成轮次没有非空白最终答案 |
| `process` | 受管 app-server 在其他终态结果前退出 |
| `unknown` | 启动、清理、格式错误的协议值,或没有更具体安全事实的失败 |
Codex 诊断会保留 `initialize``thread-start``turn-start``turn``process``teardown`,以及适用的数值 HTTP status 和分别观测到的退出码与信号。`contextWindowExceeded` 仍把共享终止原因映射为 `max-tokens`;其他所有类别仍使用 `error`。只有结构化协议事实会贡献权限说明。产品 stderr 只供 Host 观测,既不会被分类,也不会复制进结果。
### 所有权与生命周期
@@ -40,7 +53,7 @@ Codex 当前仍保留[结构化失败事实决策](../feature/2026-08-18-product
## Verification
Claude Code 包测试覆盖全部粗粒度类别、四个阶段、未知结构化值、权限顺序、原始文本排除、成功与取消时省略、并发运行隔离,以及彼此独立的退出码与信号字段。真实 Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 会产生实际 max-turns 限制、进程失败、权限拒绝、严格最终答案、取消与整棵进程树完全停稳。Loader 与无密钥产品组合继续公开静态工具,不增加诊断解析器或模型可见类别输入。
Claude Code 包测试覆盖全部粗粒度类别、四个阶段、未知结构化值、权限顺序、原始文本排除、成功与取消时省略、并发运行隔离,以及彼此独立的退出码与信号字段。真实 Agent SDK 0.3.241 与 Claude Code 2.1.241 fixture 会产生实际 max-turns 限制、进程失败、权限拒绝、严格最终答案、取消与整棵进程树完全停稳。Codex 包测试覆盖全部粗粒度类别、六个阶段、适用 HTTP status、结构化权限顺序、stderr 排除、成功与取消时省略、并发及清理聚合。真实 0.149.1 app-server fixture 会产生服务、产品错误、进程、最终答案、模型隔离、取消与完全停稳证据。Loader 与无密钥产品组合继续公开静态工具,不增加诊断解析器或模型可见类别输入。
## Alternatives considered
@@ -54,6 +67,6 @@ Claude Code 包测试覆盖全部粗粒度类别、四个阶段、未知结构
## Consequences
Claude Code 升级不再要求为每个 SDK 错误 subtype 提供模型可见承诺。父级仍可区分限制、一般产品失败、无效结果、受管进程退出与未知失败,并保留阶段、权限与进程事实。
产品运行时升级不再要求为每个 SDK 或 app-server 错误成员提供模型可见承诺。父级仍可区分限制、访问与策略限制、服务与传输失败、一般产品失败、无效结果、受管进程退出与未知失败,并保留适用的阶段、权限、HTTP 与进程事实。
诊断仍是安全展示文本,而不是恢复协议。本改动不增加原始错误转发、fallback 模型、自动重试、产品会话持久化、公共结构化结果字段或动态提供方与模型选择。
+2 -2
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 docs/config-catalog.md
config-catalog.md: 0f8a5cf6a9eb356b585b6c308b3ac02a0bac8e9a
config-catalog.zh.md: 6feb92ca28a421f7c2131441aff4634434fb92b7
config-catalog.md: 581c8c89dafea93e34968535eacfb0bb8f8ed34a
config-catalog.zh.md: 8a64b5789ee02f724cd15b303910c5baa5c381d6
+3 -1
View File
@@ -2317,10 +2317,12 @@ Source: [`packages/subagent/subagent-claude-code/src/index.ts:38`](../packages/s
Requires: `subagents` · `subprocess`
```ts config-catalog
/** Deployment-owned permission, environment, and process-release settings. */
/** Deployment-owned model, permission, environment, and process-release settings. */
export interface Config {
/** Provider name on `ctx.subagents` (default `codex`). */
providerName?: string
/** Native Codex model fixed for this instance; omitted to inherit Codex settings. */
model?: string
/**
* Explicit environment entries layered over the subprocess seam's
* credential-scrubbed parent environment.
+3 -1
View File
@@ -2319,10 +2319,12 @@ export type ClaudeCodePermissionMode = typeof CLAUDE_CODE_PERMISSION_MODES[numbe
需要:`subagents` · `subprocess`
```ts config-catalog
/** Deployment-owned permission, environment, and process-release settings. */
/** Deployment-owned model, permission, environment, and process-release settings. */
export interface Config {
/** Provider name on `ctx.subagents` (default `codex`). */
providerName?: string
/** Native Codex model fixed for this instance; omitted to inherit Codex settings. */
model?: string
/**
* Explicit environment entries layered over the subprocess seam's
* credential-scrubbed parent environment.
@@ -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: 5016d6b9aa4b57c9d1e83701a0ccfc4b04616f6d
README.zh.md: adabd942c362a28a4eeabe7a4d1b27b138aee4c6
README.md: 7ff5e1dd06f18cc5e313cca1a84576e976e96454
README.zh.md: 8532f6cfb5ece282aa916771eb971d0f3edfab53
+14 -11
View File
@@ -6,13 +6,13 @@ This package registers a Profile-named Codex subagent provider whose default nam
## Start and ownership
`start(request)` accepts only a non-empty sequence of text blocks and derives the child cwd from the parent Session. It then spawns the fixed command through [`dsh-subprocess`](../../subprocess/subprocess/README.md), performs `initialize``initialized`, maps the Profile-selected mode into official `thread/start` approval/reviewer/sandbox fields beside `{ cwd, ephemeral: true }`, and publishes the run only after Codex returns a valid ephemeral thread. A failure or cancellation before publication closes the wire, terminates the managed process tree, waits for it to exit, and rejects `start()`. Non-cancellation rejections expose only the fixed `initialize` or `thread-start` stage plus an already observed process outcome; raw product and Host errors remain on internal cause chains.
`start(request)` accepts only a non-empty sequence of text blocks and derives the child cwd from the parent Session. It then spawns the fixed command through [`dsh-subprocess`](../../subprocess/subprocess/README.md), performs `initialize``initialized`, maps the Profile-selected mode and optional model into official `thread/start` fields beside `{ cwd, ephemeral: true }`, and publishes the run only after Codex returns a valid ephemeral thread. A failure or cancellation before publication closes the wire, terminates the managed process tree, waits for it to exit, and rejects `start()`. Non-cancellation rejections expose only the fixed `initialize` or `thread-start` stage plus an already observed process outcome; raw product and Host errors remain on internal cause chains.
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.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. The wire records only the effective mode, request category, decision, and fixed safe reason. It also recognizes declined command/file items and `sandboxError` terminals. Codex 0.147.0 writes some early `never` rejections and sandbox violations only to structured stderr, so the Provider pipes stderr, forwards it unchanged to the host, and matches two fixed signatures in a bounded per-run tail; raw stderr never enters the diagnostic.
For command and file approvals, the unattended provider selects a non-approval decision offered by the request, preferring `cancel`; the stable 0.149.1 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. The wire records only the effective mode, request category, decision, and fixed safe reason. It also recognizes declined command/file items and structured `sandboxError` terminals. Product stderr is forwarded unchanged to the Host but is never classified or copied into the diagnostic.
Local cancellation wins the result race and maps to `aborted`. For failed turns, the diagnostic preserves all eleven string and five object variants in the Codex 0.147.0 `codexErrorInfo` union; the four connection/stream variants retain a numeric `httpStatusCode` when supplied, while `activeTurnNotSteerable` does not expose `turnKind`. The diagnostic also names `turn-start`, `turn`, or `process`, independently includes available exit code and signal, and uses `unknown` for unrecognized or malformed values without copying raw fields. `contextWindowExceeded` remains `max-tokens`; every other remote interruption or failure remains `error`, and the provider produces no `refusal`. A contributing permission decision follows the structured failure line. Successful and locally cancelled runs omit both facts.
Local cancellation wins the result race and maps to `aborted`. Failed turns use the coarse categories `limit`, `access-policy`, `service`, `transport`, `product-error`, `invalid-result`, or `unknown`; an early app-server exit uses `process`. Applicable connection and stream failures retain a numeric `httpStatusCode`. The diagnostic also names `turn-start`, `turn`, or `process` and independently includes available exit code and signal without copying raw fields. `contextWindowExceeded` remains `max-tokens`; every other remote interruption or failure remains `error`, and the provider produces no `refusal`. A contributing structured permission decision follows the failure line. Successful and locally cancelled runs omit both facts.
`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, waits for whole-tree exit, and detaches the stderr observer. Independent cleanup rejection uses the fixed `teardown` stage and any available process outcome. When startup and rollback both fail, the top-level aggregate message preserves both safe stage lines while the raw failures remain internal.
@@ -25,6 +25,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
| Key | Default | Meaning |
|---|---|---|
| `providerName` | `codex` | Non-empty registry name on `ctx.subagents`; each mounted instance needs a unique value. |
| `model` | native Codex settings | Optional non-empty native model name fixed for every thread from this Provider instance; omission sends no app-server override. |
| `env` | `{}` | Explicit child environment layered over the subprocess seam's credential-scrubbed parent environment. |
| `permissionMode` | `never` | Native non-interactive approval and sandbox mode fixed for every thread from this Provider instance. |
| `disposeGraceMs` | `3000` | Positive finite grace in milliseconds, no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), between the shared process-tree owner's termination tiers; disposal then waits for whole-tree exit. |
@@ -35,7 +36,7 @@ The provider advertises no optional start-time capabilities and reports `inherit
| `approve-for-me` | `approvalPolicy: on-request`, `approvalsReviewer: auto_review`, `sandbox: workspace-write` | Route permission requests through Codex automatic review without a human. |
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never`, `sandbox: danger-full-access` | Skip approval and sandbox enforcement; this value must be selected explicitly. |
Production resolves the `codex` bin declared by its pinned `@openai/codex@0.147.0` dependency and launches that JavaScript wrapper with the current Node executable. The wrapper selects the matching native platform payload; the provider neither inspects nor falls back to a host `codex` on `PATH`. Native Codex configuration and authentication remain authoritative through the parent cwd, `HOME`, and `CODEX_HOME`, while the Provider overrides only the selected thread approval/reviewer/sandbox fields. All other project, model, provider, MCP, hook, skill, and account settings remain native. The plugin does not select a model, create `CODEX_HOME`, log in, or probe an account. Credential-shaped ambient variables are removed by the subprocess seam before the explicit `env` overlay is applied.
Production resolves the `codex` bin declared by its pinned `@openai/codex@0.149.1` dependency and launches that JavaScript wrapper with the current Node executable. The wrapper selects the matching native platform payload; the provider neither inspects nor falls back to a host `codex` on `PATH`. Native Codex configuration and authentication remain authoritative through the parent cwd, `HOME`, and `CODEX_HOME`. A configured `model` is passed unchanged on each ephemeral `thread/start`; omission leaves the field absent, and the Provider does not discover models, rewrite aliases, select `modelProvider` or `serviceTier`, or set a fallback. The selected permission mode remains the only other thread-level override. The plugin does not create `CODEX_HOME`, log in, or probe an account. Credential-shaped ambient variables are removed by the subprocess seam before the explicit `env` overlay is applied.
This package is an optional Profile Bundle. Install it into the target Profile, then restart that Profile; installation brings the official wrapper and one compatible native platform payload into that Profile, while the declared `cordis.patch.yml` layer registers only the dormant `codex` Host provider and starts no Codex process. Removing the package withdraws that provider and its private runtime closure on the next Profile start.
@@ -45,7 +46,7 @@ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
dsh --profile <name>
```
Installation controls Host availability, not model permission. The Bundle supplies the dormant default `codex` row; the Profile may replace that row's complete config or mount additional rows with distinct `providerName`, `permissionMode`, and `env` values. Loading an instance starts no Codex process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
Installation controls Host availability, not model permission. The Bundle supplies the dormant default `codex` row; the Profile may replace that row's complete config or mount additional rows with distinct `providerName`, `model`, `permissionMode`, and `env` values. Loading an instance starts no Codex process until a bound tool calls it. Each `dsh-tool-subagent` row names one provider and needs its own `toolName`, so the model sees static tools rather than a dynamic provider or model selector. Full Agent Presets carry a matching default product tool row with `disabled: true`; copy a preset and remove that field to expose `subagent_codex` only to agents composed from the copy. Its `one-shot` policy keeps omitted or `false` `run_in_background` calls in the foreground, while explicit `true` returns a parent-owned Job id for `job_output` or `job_kill`. The base host and full presets already provide the generic Job registry and controls.
The standalone composition below shows the complete explicit capability. A Profile based on `@deepseek-ai/dsh-base` keeps its existing Job rows, adds the product provider and tool rows, and does not mount duplicate Job services.
@@ -54,6 +55,7 @@ The standalone composition below shows the complete explicit capability. A Profi
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-safe
model: approved-review-model
permissionMode: never
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
@@ -62,6 +64,7 @@ The standalone composition below shows the complete explicit capability. A Profi
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-bypass
model: approved-edit-model
permissionMode: dangerously-bypass-approvals-and-sandbox
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
@@ -94,9 +97,9 @@ The standalone composition below shows the complete explicit capability. A Profi
## Product compatibility and evidence
The production wire intentionally implements only the app-server methods required by this one-shot contract. The runtime dependency and all six optional-dependency aliases are pinned to `@openai/codex@0.147.0` / `codex-cli 0.147.0`. A normal install selects one payload for the current OS and CPU. For the current darwin-arm64 payload, `npm pack --dry-run --json @openai/codex@0.147.0-darwin-arm64` reports 111,199,052 packed bytes and 274,777,843 unpacked bytes. That package contains native `codex`, `codex-code-mode-host`, `rg`, and `zsh` resources; other platforms may differ, and these values are disclosure rather than an installation threshold.
The production wire intentionally implements only the app-server methods required by this one-shot contract. The runtime dependency and all six optional-dependency aliases are pinned to `@openai/codex@0.149.1` / `codex-cli 0.149.1`. A normal install selects one payload for the current OS and CPU. For the current darwin-arm64 payload, `npm pack --dry-run --json @openai/codex@0.149.1-darwin-arm64` reports 114,152,335 packed bytes and 282,491,555 unpacked bytes. That package contains native `codex`, `codex-code-mode-host`, `rg`, and `zsh` resources; other platforms may differ, and these values are disclosure rather than an installation threshold.
Generated schema evidence and package tests pin all sixteen error-info variants, HTTP-status locations, six lifecycle stages, process outcomes, stop-reason mapping, unknown fallback, sanitization, permission ordering, cancellation, concurrency, and cleanup aggregation. The keyless real-product test drives the package wrapper against a loopback Responses fixture and observes the package-local argv, exact Bearer key, original task, byte-exact final answer, thread-level `never` overriding ambient `on-request`, automatic-review startup, unattended rejection without file side effects, a real `internalServerError`, explicit dangerous-bypass writing in suite-owned temporary storage, process/protocol failure with safe exit facts, and wrapper/native quiescence. The same tier proves two named instances retain separate environments and native modes.
Generated schema evidence pins the optional `ThreadStartParams.model` field, while package tests cover representative error groups, applicable HTTP status, six lifecycle stages, process outcomes, stop-reason mapping, unknown fallback, sanitization, structured permission ordering, cancellation, concurrency, and cleanup aggregation. The keyless real-product test drives the package wrapper against a loopback Responses fixture and observes the package-local argv, omitted-model inheritance, two explicit instance models, exact Bearer key, original task, byte-exact final answer, thread-level `never` overriding ambient `on-request`, automatic-review startup, unattended rejection without file side effects, a real service failure, explicit dangerous-bypass writing in suite-owned temporary storage, process/protocol failure with safe exit facts, and wrapper/native quiescence.
Installing with optional dependencies omitted, using an unsupported platform, or losing the selected payload makes the first delegation fail at `initialize` with the safe `unknown` category and any observed process outcome. Raw wrapper text remains on Host stderr; the provider neither probes a host CLI nor retries with one. An isolated wrapper fixture separately proves the native payload failure and absence of host fallback.
@@ -106,7 +109,7 @@ Installing with optional dependencies omitted, using an unsupported platform, or
#### What the model sees
The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd; its model, system instructions, tools, and authentication come from native Codex configuration, the selected Provider instance's Profile configuration fixes the thread's environment, non-interactive approval policy, and sandbox mode, and the executable version comes from the Bundle's pinned platform payload.
The Codex child receives the standalone text blocks as one turn in a fresh ephemeral thread. Its workspace is the parent Session cwd; the selected Provider instance fixes any configured model, environment, non-interactive approval policy, and sandbox mode, while an omitted model and every other product setting come from native Codex configuration. The executable version comes from the Bundle's pinned platform payload.
#### Token effect
@@ -120,7 +123,7 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi
#### What the model sees
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or an error containing the stop reason and optional safe diagnostic for a non-completed result. The diagnostic can distinguish the fixed error-info category, protocol stage, numeric HTTP status, and observed process outcome without copying product prose. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the same final answer or failed status detail through `job_output`, and let `job_kill` request cancellation. Codex commentary, reasoning, tool activity, raw stderr, workspace diffs, usage, product ids, commands, paths, and protocol payloads are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or an error containing the stop reason and optional safe diagnostic for a non-completed result. The diagnostic can distinguish a coarse action category, protocol stage, applicable numeric HTTP status, and observed process outcome without copying product prose or stderr. A background call first returns a Job id; the generic job controls later deliver a completion notice, expose the same final answer or failed status detail through `job_output`, and let `job_kill` request cancellation. Codex commentary, reasoning, tool activity, raw stderr, workspace diffs, usage, product ids, commands, paths, and protocol payloads are not copied into the parent Session.
#### Token effect
@@ -133,10 +136,10 @@ Append-only: foreground adds one result after the reusable parent prefix, while
## Known Limitations and Deferred Work
- **One fresh process, thread, and turn per run** — there is no continuation, resume, pooling, progress stream, or product-session persistence.
- **Static instance selection** — Profile rows fix provider names and tool bindings; calls cannot choose a provider dynamically, and every exposed tool needs a unique `toolName`.
- **Static instance selection** — Profile rows fix provider names, optional models, and tool bindings; calls cannot choose or change either a provider or model dynamically, and every exposed tool needs a unique `toolName`.
- **Authentication and account state remain native** — the Bundle supplies the CLI but does not create an account, log in, trust a project, or rewrite Codex settings; configuration and authentication failures surface with their lifecycle stage and the safe `unknown` fallback rather than a separate public taxonomy.
- **The native platform payload is required at delegation time** — installs that omit optional dependencies, unsupported platforms, and missing or damaged payloads fail at the first run; there is no host-CLI fallback.
- **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.
- **Compatibility is pinned by development evidence** — upgrading from the verified 0.149.1 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; the three Profile modes never create a DSH interaction channel or per-call allow policy.
- **Assistant payload is final text only** — a failed run may additionally expose the separate safe diagnostic; reasoning, commentary, intermediate messages, tool traffic, usage, raw stderr, and workspace diffs remain outside the parent Session, while generic Job ids, notices, and status come from the shared job runtime.
- **No optional shared capabilities**`agentOptions`, output schemas, child personas, tool filtering, and harness depth enforcement are rejected by the shared service for this provider.
+14 -11
View File
@@ -6,13 +6,13 @@
## 启动与所有权
`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。随后,它通过 [`dsh-subprocess`](../../subprocess/subprocess/README.zh.md) spawn 固定命令,依次执行 `initialize``initialized`,把 Profile 选择的模式映射为官方 `thread/start` approvalreviewersandbox 字段并与 `{ cwd, ephemeral: true }` 一起发送,且仅在 Codex 返回有效的临时线程后才发布此次运行。若在发布前发生失败或取消,它会关闭通信链路、终止受管进程树并等待其退出,然后拒绝 `start()` 调用。非取消拒绝只公开固定的 `initialize``thread-start` 阶段及已经观测到的进程结果;原始产品与 Host 错误只保留在内部 cause 链中。
`start(request)` 只接受非空的文本块序列,并根据父会话确定子级 cwd。随后,它通过 [`dsh-subprocess`](../../subprocess/subprocess/README.zh.md) spawn 固定命令,依次执行 `initialize``initialized`,把 Profile 选择的模式与可选模型映射为官方 `thread/start` 字段并与 `{ cwd, ephemeral: true }` 一起发送,且仅在 Codex 返回有效的临时线程后才发布此次运行。若在发布前发生失败或取消,它会关闭通信链路、终止受管进程树并等待其退出,然后拒绝 `start()` 调用。非取消拒绝只公开固定的 `initialize``thread-start` 阶段及已经观测到的进程结果;原始产品与 Host 错误只保留在内部 cause 链中。
已发布的 `run.result` 恰好启动一个轮次。它只接受与此次运行的线程和轮次匹配的通知,随后等待权威的终止通知 `turn/completed`。以最后一条 `phase: "final_answer"``agentMessage` 为准;若 Codex 没有发出明确的最终阶段,则以最后一条 `phase: null` 的消息作为兼容性回退。过程说明绝不会取代上述任一答案;成功完成的轮次若没有非空白答案,结果也会判为错误。
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.147.0 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败。wire 只记录有效模式、请求类别、决定与固定的安全原因,也会识别被拒绝的命令/文件 item 和 `sandboxError` 终态。Codex 0.147.0 的部分早期 `never` 拒绝和 sandbox violation 只写入结构化 stderr,因此提供方会 pipe stderr、原样转发给 Host,并在每次运行的有界尾缓冲中匹配两个固定签名;原始 stderr 不会进入诊断。
对于命令与文件审批,无人值守的提供方会从请求给出的决策选项中选择一项不予批准的决策,并优先选择 `cancel`;稳定的 0.149.1 请求形态没有决策选项列表,因此回退到 `decline`。它对权限请求返回作用域限于当前轮次的空权限集,不向用户输入请求提供任何答案,并拒绝 MCP elicitation。若请求在无人值守模式下没有合法响应,或是未知服务器请求,此次运行就会失败。wire 只记录有效模式、请求类别、决定与固定的安全原因,也会识别被拒绝的命令/文件 item 和结构化 `sandboxError` 终态。产品 stderr 会原样转发给 Host,但绝不会被分类或复制进诊断。
本地取消会在结果竞态中胜出并映射为 `aborted`对于失败轮次,诊断会保留 Codex 0.147.0 `codexErrorInfo` 联合中的全部十一种字符串与五种对象 variant;四种连接/stream variant 会在上游提供时保留数值 `httpStatusCode`,而 `activeTurnNotSteerable` 不公开 `turnKind`。诊断还会注明 `turn-start``turn``process`,分别包含可用的退出码与信号,并对无法识别或格式错误的值使用 `unknown`且不复制原始字段。`contextWindowExceeded` 仍映射为 `max-tokens`;其他任何远端中断或失败仍映射为 `error`,且该提供方不会产生 `refusal`。参与失败的权限决定会跟在结构化失败行之后。成功与本地取消都不附带这两类事实。
本地取消会在结果竞态中胜出并映射为 `aborted`。失败轮次使用粗粒度类别 `limit``access-policy``service``transport``product-error``invalid-result``unknown`app-server 提前退出使用 `process`。适用的连接与 stream 失败会保留数值 `httpStatusCode`。诊断还会注明 `turn-start``turn``process`,分别包含可用的退出码与信号,且不复制原始字段。`contextWindowExceeded` 仍映射为 `max-tokens`;其他任何远端中断或失败仍映射为 `error`,且该提供方不会产生 `refusal`。参与失败的结构化权限决定会跟在失败行之后。成功与本地取消都不附带这两类事实。
`dispose()`(资源释放)具有幂等性:如果当前的两个标识符均已知,它会尽力请求 `turn/interrupt`,关闭 JSON-RPC 通信链路,结束标准输入,调用共享的进程树逐级终止机制,等待整棵进程树退出,并移除 stderr observer。独立清理拒绝使用固定的 `teardown` 阶段与可用进程结果。当启动与回滚同时失败时,顶层聚合消息会保留两条安全阶段说明,而原始失败仍只在内部可见。
@@ -25,6 +25,7 @@
| 配置键 | 默认值 | 含义 |
|---|---|---|
| `providerName` | `codex` | `ctx.subagents` 中的非空注册名称;每个已挂载实例都需要唯一值。 |
| `model` | Codex 原生设置 | 可选的非空原生模型名称,为该实例的每个线程固定;省略时不发送 app-server 覆盖。 |
| `env` | `{}` | 显式指定的子进程环境,叠加在由子进程 seam 清除凭证后的父环境之上。 |
| `permissionMode` | `never` | 为该提供方实例的每个线程固定原生非交互审批与沙箱模式。 |
| `disposeGraceMs` | `3000` | 共享进程树责任方各终止层级之间的宽限期,单位为毫秒且须为正有限值,并不得大于仓库共享的 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.zh.md);随后资源释放会等待整棵进程树退出。 |
@@ -35,7 +36,7 @@
| `approve-for-me` | `approvalPolicy: on-request``approvalsReviewer: auto_review``sandbox: workspace-write` | 由 Codex 自动评审权限请求,不等待人工。 |
| `dangerously-bypass-approvals-and-sandbox` | `approvalPolicy: never``sandbox: danger-full-access` | 跳过审批与 sandbox;必须显式选择该值。 |
生产环境会解析锁定的 `@openai/codex@0.147.0` 依赖所声明的 `codex` bin,并使用当前 Node 可执行文件启动该 JavaScript wrapper。Wrapper 会选择匹配的原生平台载荷;提供方既不检查也不回退 `PATH` 中的宿主 `codex`。父会话 cwd、`HOME``CODEX_HOME` 继续让原生 Codex 配置和身份验证保持权威,而提供方只覆盖选定线程的 approvalreviewersandbox 字段。其他项目、模型、provider、MCP、hook、skill 与账户设置仍由原生机制负责。本插件不选择模型、不创建 `CODEX_HOME`、不执行登录,也不探测账户。子进程 seam 会先移除具有凭证特征的环境变量,再应用显式 `env` 覆盖。
生产环境会解析锁定的 `@openai/codex@0.149.1` 依赖所声明的 `codex` bin,并使用当前 Node 可执行文件启动该 JavaScript wrapper。Wrapper 会选择匹配的原生平台载荷;提供方既不检查也不回退 `PATH` 中的宿主 `codex`。父会话 cwd、`HOME``CODEX_HOME` 继续让原生 Codex 配置和身份验证保持权威。已配置的 `model` 会原样传给每个临时 `thread/start`;省略时不设置该字段,且提供方不会发现模型、改写别名、选择 `modelProvider``serviceTier`,也不会设置 fallback。所选权限模式仍是唯一的另一项线程级覆盖。本插件不创建 `CODEX_HOME`、不执行登录,也不探测账户。子进程 seam 会先移除具有凭证特征的环境变量,再应用显式 `env` 覆盖。
本包是可选的 Profile Bundle。将它安装进目标 Profile 后重启该 Profile;安装会把官方 wrapper 与一个兼容的原生平台载荷带入该 Profile,而包所声明的 `cordis.patch.yml` 层只注册休眠的 `codex` Host provider,不会启动 Codex 进程。移除该包后,下一次 Profile 启动会撤回这一 provider 及其私有运行时闭包。
@@ -45,7 +46,7 @@ dsh plugin --profile <name> remove @deepseek-ai/dsh-subagent-codex
dsh --profile <name>
```
安装决定 Host 可用性,而不是模型权限。Bundle 会提供休眠的默认 `codex` 配置项;Profile 可以替换该配置项的完整 config,也可以挂载更多具有不同 `providerName``permissionMode``env` 的配置项。加载实例本身不会在绑定工具调用前启动 Codex 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output``job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
安装决定 Host 可用性,而不是模型权限。Bundle 会提供休眠的默认 `codex` 配置项;Profile 可以替换该配置项的完整 config,也可以挂载更多具有不同 `providerName``model``permissionMode``env` 的配置项。加载实例本身不会在绑定工具调用前启动 Codex 进程。每个 `dsh-tool-subagent` 配置项指定一个提供方,并需要独立的 `toolName`,因此模型看到的是静态工具,而不是动态提供方或模型选择器。完整 Agent Preset 携带对应的默认产品工具行并设置 `disabled: true`;复制一个 preset 后删除该字段,即可只向由该副本组装的 agent 暴露 `subagent_codex`。其 `one-shot` 策略会让省略 `run_in_background` 或传入 `false` 的调用继续在前台等待,而显式传入 `true` 会返回由父 agent 拥有的 Job ID,供 `job_output``job_kill` 使用。base host(基础宿主)与完整 preset 已提供通用作业注册表和控制工具。
下列独立组装展示完整的显式能力。基于 `@deepseek-ai/dsh-base` 的 Profile 保留已有 Job 配置项,新增产品提供方与工具配置项,而且不重复挂载 Job 服务。
@@ -54,6 +55,7 @@ dsh --profile <name>
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-safe
model: approved-review-model
permissionMode: never
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
@@ -62,6 +64,7 @@ dsh --profile <name>
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-bypass
model: approved-edit-model
permissionMode: dangerously-bypass-approvals-and-sandbox
env:
OPENAI_API_KEY: !!js process.env.OPENAI_API_KEY
@@ -94,9 +97,9 @@ dsh --profile <name>
## 产品兼容性与证据
生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。运行时依赖与六个 optional-dependency alias 均锁定到 `@openai/codex@0.147.0` / `codex-cli 0.147.0`。普通安装会按当前操作系统与 CPU 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json @openai/codex@0.147.0-darwin-arm64` 报告压缩包为 111,199,052 字节、解包后为 274,777,843 字节。该包包含原生 `codex``codex-code-mode-host``rg``zsh` 资源;其他平台可能不同,这些数值只用于披露而不是安装阈值。
生产环境的协议层有意只实现这一单次执行约定所需的 app-server 方法。运行时依赖与六个 optional-dependency alias 均锁定到 `@openai/codex@0.149.1` / `codex-cli 0.149.1`。普通安装会按当前操作系统与 CPU 选择一个载荷。对于当前 darwin-arm64 载荷,`npm pack --dry-run --json @openai/codex@0.149.1-darwin-arm64` 报告压缩包为 114,152,335 字节、解包后为 282,491,555 字节。该包包含原生 `codex``codex-code-mode-host``rg``zsh` 资源;其他平台可能不同,这些数值只用于披露而不是安装阈值。
生成的 schema 证据与包测试会固定全部十六种 error-info variant、HTTP status 所在位置、六个生命周期阶段、进程结果、终止原因映射、unknown 回退、脱敏、权限顺序、取消、并发与清理聚合。无密钥真实产品测试会驱动包内 wrapper 连接回环 Responses fixture,并观测包内 argv、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、线程级 `never` 对环境中 `on-request` 的覆盖、自动评审启动、不产生文件副作用的无人值守拒绝、真实 `internalServerError`、测试拥有临时存储中的显式危险绕过写入、携带安全退出事实的进程/协议失败,以及 wrapper/原生进程完全停稳。同一层级还会证明两个命名实例保留彼此独立的环境与原生模式。
生成的 schema 证据会固定可选的 `ThreadStartParams.model` 字段,包测试则覆盖代表性的错误分组、适用 HTTP status、六个生命周期阶段、进程结果、终止原因映射、unknown 回退、脱敏、结构化权限顺序、取消、并发与清理聚合。无密钥真实产品测试会驱动包内 wrapper 连接回环 Responses fixture,并观测包内 argv、省略模型继承、两个显式实例模型、确切的 Bearer 密钥、原始任务、逐字节完全一致的最终回答、线程级 `never` 对环境中 `on-request` 的覆盖、自动评审启动、不产生文件副作用的无人值守拒绝、真实服务失败、测试拥有临时存储中的显式危险绕过写入、携带安全退出事实的进程/协议失败,以及 wrapper/原生进程完全停稳。
如果安装时省略 optional dependencies、当前平台不受支持,或所选载荷缺失,第一次委派会在 `initialize` 阶段以安全 `unknown` 类别和已观测到的进程结果失败。原始 wrapper 文本只保留在 Host stderr;提供方既不会探测宿主 CLI,也不会用它重试。独立 wrapper fixture 会另行证明原生载荷失败与不存在宿主回退。
@@ -106,7 +109,7 @@ dsh --profile <name>
#### 模型看到的内容
Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;其模型、系统指令、工具和身份验证来自原生 Codex 配置,所选提供方实例的 Profile 配置会固定该线程的环境、非交互审批策略与沙箱模式,而可执行版本来自 Bundle 锁定的平台载荷。
Codex 子级会在一个全新的临时线程中,以单个轮次接收这些独立文本块。它的工作区是父会话 cwd;所选提供方实例会固定已配置的模型、环境、非交互审批策略与沙箱模式,而省略的模型及其余产品设置来自 Codex 原生配置。可执行版本来自 Bundle 锁定的平台载荷。
#### 对 token 的影响
@@ -120,7 +123,7 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
#### 模型看到的内容
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案;若结果未完成,错误中会包含终止原因和可选的安全诊断。该诊断可以区分固定 error-info 类别、协议阶段、数值 HTTP status 和已观测的进程结果,而不复制产品正文。后台调用会先返回 Job id;随后通用作业控制面会送达完成通知,通过 `job_output` 公开同一最终答案或失败状态 detail,并允许 `job_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、原始 stderr、工作区差异、用量信息、产品标识符、命令、路径和协议载荷均不会复制到父会话。
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案;若结果未完成,错误中会包含终止原因和可选的安全诊断。该诊断可以区分粗粒度行动类别、协议阶段、适用的数值 HTTP status 和已观测的进程结果,而不复制产品正文或 stderr。后台调用会先返回 Job id;随后通用作业控制面会送达完成通知,通过 `job_output` 公开同一最终答案或失败状态 detail,并允许 `job_kill` 请求取消。Codex 的过程说明、推理(reasoning)、工具活动、原始 stderr、工作区差异、用量信息、产品标识符、命令、路径和协议载荷均不会复制到父会话。
#### 对 token 的影响
@@ -133,10 +136,10 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些
## 已知限制与后续工作
- **每次运行均新建一个进程、一个线程和一个轮次**:不支持续接、恢复、池化、进度流或产品会话持久化。
- **静态选择实例**:Profile 配置项固定提供方名称与工具绑定;调用无法动态选择提供方,而且每个公开工具都需要唯一的 `toolName`
- **静态选择实例**Profile 配置项固定提供方名称、可选模型与工具绑定;调用无法动态选择或修改提供方与模型,而且每个公开工具都需要唯一的 `toolName`
- **身份验证与账户状态仍由原生机制管理**:Bundle 会提供 CLI,但不会创建账户、登录、信任项目或改写 Codex 设置;配置与身份验证失败会公开其生命周期阶段与安全的 `unknown` 回退,而不会增加单独的公开分类体系。
- **委派时必须存在原生平台载荷**:省略 optional dependencies 的安装、不受支持的平台以及缺失或损坏的载荷都会在第一次运行时失败;不会回退到宿主 CLI。
- **兼容性由开发证据锁定**:若要从已验证的 0.147.0 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **兼容性由开发证据锁定**:若要从已验证的 0.149.1 协议基线升级,必须重新生成上游 schema 证据,并重新运行握手、答案选择、审批、取消、无密钥真实产品以及带密钥的 DeepSeek 随机数测试。
- **没有人工审批路径**:已知的无人值守审批请求会被拒绝,未知服务器请求会以默认拒绝方式使运行失败;三种 Profile 模式都不会创建 DSH 交互通道或逐次调用 allow 策略。
- **assistant 载荷仅包含最终文本**:失败运行可以额外公开独立的安全诊断;推理、过程说明、中间消息、工具通信、用量信息、原始 stderr 和工作区差异不会进入父会话,通用 Job id、通知与状态来自共享作业运行时。
- **没有可选的共享能力**:对于本提供方,共享服务会拒绝 `agentOptions`、输出 schema、子任务角色设定、工具筛选和 harness 深度强制约束。
@@ -49,7 +49,7 @@
"dependencies": {
"@deepseek-ai/dsh-sdk-protocol": "workspace:^",
"@deepseek-ai/schemastery": "workspace:^",
"@openai/codex": "0.147.0"
"@openai/codex": "0.149.1"
},
"devDependencies": {
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
@@ -32,10 +32,12 @@ export const inject = ['subagents', 'subprocess']
const DEFAULT_PROVIDER_NAME = 'codex'
/** Deployment-owned permission, environment, and process-release settings. */
/** Deployment-owned model, permission, environment, and process-release settings. */
export interface Config {
/** Provider name on `ctx.subagents` (default `codex`). */
providerName?: string
/** Native Codex model fixed for this instance; omitted to inherit Codex settings. */
model?: string
/**
* Explicit environment entries layered over the subprocess seam's
* credential-scrubbed parent environment.
@@ -49,13 +51,14 @@ export interface Config {
export const Config: z<Config> = z.object({
providerName: z.string().min(1).default(DEFAULT_PROVIDER_NAME),
model: z.string().min(1),
env: z.dict(z.string()).default({}),
permissionMode: z.union([...CODEX_PERMISSION_MODES])
.default(DEFAULT_CODEX_PERMISSION_MODE),
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
})
type ResolvedConfig = Required<Config>
type ResolvedConfig = Omit<Required<Config>, 'model'> & Pick<Config, 'model'>
class CodexProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
@@ -91,6 +94,7 @@ class CodexProvider implements SubagentProvider {
}
const spec: CodexRunSpec = {
cwd,
...this.config.model === undefined ? {} : { model: this.config.model },
permissionMode: this.config.permissionMode,
env: this.config.env,
disposeGraceMs: this.config.disposeGraceMs,
@@ -108,11 +112,12 @@ class CodexProvider implements SubagentProvider {
/**
* Register one Profile-named Codex provider.
* @param ctx - context carrying shared subagent and subprocess services.
* @param config - registry name, permission mode, child environment, and disposal grace.
* @param config - registry name, optional model, permission mode, child environment, and disposal grace.
*/
export function apply(ctx: Context, config: Config): void {
const resolved: ResolvedConfig = {
providerName: config.providerName ?? DEFAULT_PROVIDER_NAME,
...config.model === undefined ? {} : { model: config.model },
env: config.env as Record<string, string>,
permissionMode: config.permissionMode ?? DEFAULT_CODEX_PERMISSION_MODE,
disposeGraceMs: config.disposeGraceMs as number,
+13 -9
View File
@@ -74,9 +74,11 @@ type CodexFailureStage =
| 'process'
| 'teardown'
type CodexFailureCategory = CodexWireFailureFacts['category'] | 'process'
interface CodexFailureFacts {
readonly stage: CodexFailureStage
readonly category: string
readonly category: CodexFailureCategory
readonly httpStatus?: number | undefined
readonly outcome?: SubprocessOutcome | undefined
}
@@ -137,6 +139,8 @@ export function codexAppServerArgv(): string[] {
export interface CodexRunSpec {
/** Parent Session workspace, also supplied to `thread/start`. */
readonly cwd: string
/** Profile-selected native model; omitted to preserve Codex settings. */
readonly model?: string
/** Profile-selected native non-interactive permission mode. */
readonly permissionMode: CodexPermissionMode
/** Explicit deployment/test environment layered after the shared scrub. */
@@ -251,10 +255,10 @@ export async function startCodexRun(
child.stdout as NonNullable<SubprocessHandle['stdout']>,
child.stdin as NonNullable<SubprocessHandle['stdin']>,
spec.permissionMode,
spec.model,
)
const onStderr = (chunk: Buffer | string): void => {
const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk
wire.observeStderr(bytes.toString())
try {
// Synchronous fd forwarding preserves byte order without owning a
// backpressure queue. A slow host sink can block this event-loop turn.
@@ -272,8 +276,8 @@ export async function startCodexRun(
const disposeProcess = async (): Promise<void> => {
try {
await disposeCodexChild(wire, child)
// Let stderr already queued by the process close reach both bounded
// diagnostic consumers before their listeners are detached.
// Let stderr already queued by the process close reach the Host before
// its forwarding listeners are detached.
await new Promise<void>((resolve) => { setImmediate(resolve) })
} finally {
child.stderr?.off('data', onStderr)
@@ -286,7 +290,7 @@ export async function startCodexRun(
(outcome) => {
processFailureFacts = {
stage: 'process',
category: 'process-exit',
category: 'process',
outcome,
}
throw new CodexRunFailure(processFailureFacts)
@@ -385,14 +389,14 @@ export async function startCodexRun(
publishedProcessFailure,
])
if (terminal.stopReason === 'completed') return terminal
// Let stderr already queued with the terminal frame contribute its
// fixed permission fact before the non-completed result is snapshotted.
// Let stderr already queued with the terminal frame reach the Host
// before the non-completed result settles.
await new Promise<void>((resolve) => { setImmediate(resolve) })
const facts = withProcessOutcome(wire.collectFailure())
return { ...terminal, diagnostic: recordFailureDiagnostic(facts) }
} catch (error: unknown) {
// Give stderr data already queued in Node one turn to reach the wire
// before settlement snapshots the diagnostic.
// Give stderr data already queued in Node one turn to reach the Host
// before error settlement.
await new Promise<void>((resolve) => { setImmediate(resolve) })
const endedBeforeTerminal = wire.endedBeforeTerminal()
if (
+34 -75
View File
@@ -1,5 +1,5 @@
/**
* Minimal Codex app-server 0.147.0 protocol adapter. The shared JSON-RPC
* Minimal Codex app-server 0.149.1 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.
@@ -18,7 +18,14 @@ type JsonObject = Record<string, unknown>
/** Product facts owned by the Codex wire after publication. */
export interface CodexWireFailureFacts {
readonly stage: 'turn-start' | 'turn'
readonly category: string
readonly category:
| 'limit'
| 'access-policy'
| 'service'
| 'transport'
| 'product-error'
| 'invalid-result'
| 'unknown'
readonly httpStatus?: number | undefined
}
@@ -35,40 +42,6 @@ const THREAD_PERMISSION_PARAMS: Readonly<Record<CodexPermissionMode, JsonObject>
},
}
const STDERR_PERMISSION_SIGNATURES = [
{
text: 'approval policy is Never; reject command',
request: 'command execution',
decision: 'denied',
reason: 'Codex rejected an escalation because the selected policy never asks for approval',
},
{
text: 'recorded sandbox violation:',
request: 'sandbox execution',
decision: 'failed',
reason: 'Codex reported a sandbox violation',
},
] as const
const STDERR_SIGNATURE_TAIL_CHARS = Math.max(
...STDERR_PERMISSION_SIGNATURES.map(signature => signature.text.length),
) - 1
function stderrSignatureTail(value: string): string {
for (
let length = Math.min(STDERR_SIGNATURE_TAIL_CHARS, value.length)
; length > 0
; length -= 1
) {
const tail = value.slice(-length)
if (STDERR_PERMISSION_SIGNATURES.some(signature =>
tail.length < signature.text.length && signature.text.startsWith(tail))) {
return tail
}
}
return ''
}
function object(value: unknown, label: string): JsonObject {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`subagent-codex: app-server returned invalid ${label}`)
@@ -102,10 +75,14 @@ function numericHttpStatus(value: unknown): number | undefined {
: undefined
}
function objectFailureInfo(value: JsonObject): {
readonly category: string
interface ParsedFailureInfo {
readonly category: CodexWireFailureFacts['category']
readonly httpStatus?: number | undefined
} {
readonly maxTokens?: true
readonly sandboxFailure?: true
}
function objectFailureInfo(value: JsonObject): ParsedFailureInfo {
const keys = Object.keys(value)
const category = keys[0]
if (keys.length !== 1 || category === undefined) {
@@ -124,20 +101,17 @@ function objectFailureInfo(value: JsonObject): {
{
const httpStatus = numericHttpStatus(fields.httpStatusCode)
return httpStatus === undefined
? { category }
: { category, httpStatus }
? { category: 'transport' }
: { category: 'transport', httpStatus }
}
case 'activeTurnNotSteerable':
return { category }
return { category: 'product-error' }
default:
return { category: 'unknown' }
}
}
function failureInfo(turn: JsonObject): {
readonly category: string
readonly httpStatus?: number | undefined
} {
function failureInfo(turn: JsonObject): ParsedFailureInfo {
if (turn.status !== 'failed') return { category: 'unknown' }
const error = turn.error
if (error === null || typeof error !== 'object' || Array.isArray(error)) {
@@ -147,17 +121,23 @@ function failureInfo(turn: JsonObject): {
if (typeof info === 'string') {
switch (info) {
case 'contextWindowExceeded':
return { category: 'limit', maxTokens: true }
case 'sessionBudgetExceeded':
case 'usageLimitExceeded':
return { category: 'limit' }
case 'serverOverloaded':
case 'cyberPolicy':
case 'internalServerError':
return { category: 'service' }
case 'cyberPolicy':
case 'misalignmentPolicyViolation':
case 'unauthorized':
return { category: 'access-policy' }
case 'badRequest':
case 'threadRollbackFailed':
case 'sandboxError':
case 'other':
return { category: info }
return { category: 'product-error' }
case 'sandboxError':
return { category: 'access-policy', sandboxFailure: true }
default:
return { category: 'unknown' }
}
@@ -236,7 +216,6 @@ export class CodexAppServerWire {
readonly decision: Parameters<typeof unattendedDiagnostic>[2]
readonly reason: string
} | undefined
private stderrTail = ''
private inputEnded = false
private terminalObserved = false
private closed = false
@@ -245,6 +224,7 @@ export class CodexAppServerWire {
private readonly input: Readable,
output: Writable,
private readonly permissionMode: CodexPermissionMode,
private readonly model?: string,
) {
this.transport = new JsonRpcLineTransport(input, output)
// Fatal protocol state can arrive after the current guarded operation has
@@ -309,6 +289,7 @@ export class CodexAppServerWire {
const response = object(await this.guarded(this.transport.request('thread/start', {
cwd,
ephemeral: true,
...this.model === undefined ? {} : { model: this.model },
...THREAD_PERMISSION_PARAMS[this.permissionMode],
}, signal), signal), 'thread/start response')
const thread = object(response.thread, 'thread/start thread')
@@ -370,7 +351,7 @@ export class CodexAppServerWire {
category: parsed.category,
httpStatus: parsed.httpStatus,
})
if (parsed.category === 'sandboxError') {
if (parsed.sandboxFailure) {
this.recordDiagnostic(
'sandbox execution',
'failed',
@@ -378,7 +359,7 @@ export class CodexAppServerWire {
completed.order,
)
}
if (parsed.category === 'contextWindowExceeded') {
if (parsed.maxTokens) {
return { output: this.collectOutput(), stopReason: 'max-tokens' }
}
const detail = status === 'failed' ? `: ${parsed.category}` : ''
@@ -386,7 +367,7 @@ export class CodexAppServerWire {
}
const output = this.collectOutput()
if (output.length === 0) {
this.recordFailure({ stage: 'turn', category: 'unknown' })
this.recordFailure({ stage: 'turn', category: 'invalid-result' })
throw new Error('subagent-codex: Codex completed without a final answer')
}
return { output, stopReason: 'completed' }
@@ -432,28 +413,6 @@ export class CodexAppServerWire {
return this.failure as CodexWireFailureFacts
}
/**
* Observe product stderr while retaining only enough tail to recognize fixed
* permission signatures. The raw text is never copied into the diagnostic.
* @param chunk - one decoded stderr chunk already forwarded to the host.
*/
observeStderr(chunk: string): void {
const observed = `${this.stderrTail}${chunk}`
let latestIndex = -1
let latest: (typeof STDERR_PERMISSION_SIGNATURES)[number] | undefined
for (const signature of STDERR_PERMISSION_SIGNATURES) {
const index = observed.lastIndexOf(signature.text)
if (index > latestIndex) {
latestIndex = index
latest = signature
}
}
if (latest !== undefined) {
this.recordDiagnostic(latest.request, latest.decision, latest.reason)
}
this.stderrTail = stderrSignatureTail(observed)
}
/** Detach JSON-RPC listeners and reject outstanding requests. Idempotent. */
close(): void {
if (this.closed) return
@@ -13,11 +13,13 @@
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-primary
model: codex-primary-model
- id: subagent-codex-secondary
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-secondary
model: codex-secondary-model
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
@@ -109,8 +109,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
env: { ...process.env, ...env },
})
expect(codexPackage.version).toBe('0.147.0')
expect(version.stdout.trim()).toBe('codex-cli 0.147.0')
expect(codexPackage.version).toBe('0.149.1')
expect(version.stdout.trim()).toBe('codex-cli 0.149.1')
const parent = {
id: 'deepseek-e2e-parent',
@@ -183,7 +183,7 @@ function expectedProcessExitDiagnostic(outcome: SubprocessOutcome): string {
const fields = [
'product: Codex',
'stage: process',
'category: process-exit',
'category: process',
]
if (outcome.exitCode !== null) fields.push(`exit code: ${outcome.exitCode}`)
if (outcome.signal !== null) fields.push(`signal: ${outcome.signal}`)
@@ -191,9 +191,6 @@ function expectedProcessExitDiagnostic(outcome: SubprocessOutcome): string {
}
interface JsonSchemaNode {
readonly enum?: string[]
readonly format?: string
readonly minimum?: number
readonly properties?: Record<string, JsonSchemaNode>
readonly required?: string[]
readonly type?: string | string[]
@@ -215,18 +212,18 @@ function responseInputTexts(body: Record<string, unknown>): string[] {
})
}
describe('real @openai/codex 0.147.0 product', () => {
describe('real @openai/codex 0.149.1 product', () => {
it('starts approve-for-me through the real app-server and returns exact text', async () => {
const sentinel = 'REAL_CODEX_SENTINEL_0_147_0'
const sentinel = 'REAL_CODEX_SENTINEL_0_149_1'
const task = 'Return the fixture sentinel exactly.'
const { harness, fixture } = await realHarness([
{ kind: 'complete', text: sentinel },
], 'approve-for-me')
expect(codexPackage.version).toBe('0.147.0')
expect(codexPackage.version).toBe('0.149.1')
const version = await execFileAsync(process.execPath, [codexEntry, '--version'], {
env: { ...process.env, ...harness.env },
})
expect(version.stdout.trim()).toBe('codex-cli 0.147.0')
expect(version.stdout.trim()).toBe('codex-cli 0.149.1')
const schemaRoot = mkdtempSync(join(tmpdir(), 'dsh-codex-schema-'))
roots.push(schemaRoot)
await execFileAsync(process.execPath, [
@@ -237,46 +234,17 @@ describe('real @openai/codex 0.147.0 product', () => {
schemaRoot,
], { env: { ...process.env, ...harness.env } })
const schema = JSON.parse(readFileSync(
join(schemaRoot, 'ServerNotification.json'),
join(schemaRoot, 'ClientRequest.json'),
'utf8',
)) as {
definitions: {
CodexErrorInfo: {
oneOf: JsonSchemaNode[]
}
ThreadStartParams: JsonSchemaNode
}
}
expect(schema.definitions.CodexErrorInfo.oneOf[0]?.enum).toEqual([
'contextWindowExceeded',
'sessionBudgetExceeded',
'usageLimitExceeded',
'serverOverloaded',
'cyberPolicy',
'internalServerError',
'unauthorized',
'badRequest',
'threadRollbackFailed',
'sandboxError',
'other',
])
expect(schema.definitions.CodexErrorInfo.oneOf.slice(1).map(variant =>
Object.keys(variant.properties ?? {})[0])).toEqual([
'httpConnectionFailed',
'responseStreamConnectionFailed',
'responseStreamDisconnected',
'responseTooManyFailedAttempts',
'activeTurnNotSteerable',
])
for (const variant of schema.definitions.CodexErrorInfo.oneOf.slice(1, 5)) {
const category = Object.keys(variant.properties ?? {})[0]!
const detail = variant.properties?.[category]
expect(detail?.required).toBeUndefined()
expect(detail?.properties?.httpStatusCode).toEqual({
format: 'uint16',
minimum: 0,
type: ['integer', 'null'],
})
}
expect(schema.definitions.ThreadStartParams.properties?.model).toEqual({
type: ['string', 'null'],
})
expect(schema.definitions.ThreadStartParams.required).toBeUndefined()
const run = await harness.ctx.subagents.start('codex', {
prompt: [{ type: 'text', text: task }],
@@ -301,6 +269,7 @@ describe('real @openai/codex 0.147.0 product', () => {
expect(recorded.method).toBe('POST')
expect(recorded.path).toBe('/v1/responses')
expect(recorded.headers.authorization).toBe('Bearer dsh-fake-openai-key')
expect(recorded.body.model).toBe('fixture-model')
expect(responseInputTexts(recorded.body)).toContain(task)
await expectQuiescent(harness.handles)
}, 60_000)
@@ -332,12 +301,14 @@ describe('real @openai/codex 0.147.0 product', () => {
const { ctx, handles, spawnSpecs } = await realRuntime()
const safeFiber = await ctx.plugin(codex, {
providerName: 'codex-safe',
model: 'codex-safe-model',
env: safeInstance.env,
permissionMode: 'never',
disposeGraceMs: 2_000,
})
const bypassFiber = await ctx.plugin(codex, {
providerName: 'codex-bypass',
model: 'codex-bypass-model',
env: bypassInstance.env,
permissionMode: 'dangerously-bypass-approvals-and-sandbox',
disposeGraceMs: 2_000,
@@ -385,6 +356,8 @@ describe('real @openai/codex 0.147.0 product', () => {
await Promise.all([safeRun.dispose(), bypassRun.dispose()])
expect(safeInstance.fixture.requests).toHaveLength(1)
expect(bypassInstance.fixture.requests).toHaveLength(1)
expect(safeInstance.fixture.requests[0]?.body.model).toBe('codex-safe-model')
expect(bypassInstance.fixture.requests[0]?.body.model).toBe('codex-bypass-model')
expect(safeInstance.fixture.requests[0]?.body.input)
.not.toEqual(bypassInstance.fixture.requests[0]?.body.input)
expect(spawnSpecs.map(spec => spec.env?.CODEX_HOME).sort()).toEqual([
@@ -440,13 +413,9 @@ describe('real @openai/codex 0.147.0 product', () => {
expect(result.stopReason).toBe('error')
const diagnosticLines = result.diagnostic?.split('\n') ?? []
expect(diagnosticLines[0]).toBe(
'Product subagent failure (product: Codex; stage: turn; category: other)',
'Product subagent failure (product: Codex; stage: turn; category: product-error)',
)
expect([
'Codex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval',
'Codex unattended decision (mode: never; request: sandbox execution; decision: failed): Codex reported a sandbox failure',
'Codex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval',
]).toContain(diagnosticLines[1])
expect(diagnosticLines).toHaveLength(1)
expect(result.diagnostic).not.toContain(command)
expect(result.diagnostic).not.toContain(harness.workspace)
await run.dispose()
@@ -478,7 +447,7 @@ describe('real @openai/codex 0.147.0 product', () => {
const result = await run.result
expect(result).toMatchObject({ output: [], stopReason: 'error' })
expect(result.diagnostic).toBe(
'Product subagent failure (product: Codex; stage: turn; category: internalServerError)',
'Product subagent failure (product: Codex; stage: turn; category: service)',
)
expect(result.diagnostic).not.toContain('SECRET_TOKEN')
expect(result.diagnostic).not.toContain('/private/secret.txt')
@@ -94,7 +94,7 @@ function responseObject(text: string): Record<string, unknown> {
}
/**
* Build the minimal Responses SSE event sequence consumed by Codex 0.147.0.
* Build the minimal Responses SSE event sequence consumed by Codex 0.149.1.
* @param text - exact assistant answer.
* @returns ordered response lifecycle events.
*/
@@ -65,7 +65,7 @@ vi.mock('node:fs', async (importOriginal) => {
type JsonObject = Record<string, unknown>
const CODEX_VERSION = '0.147.0'
const CODEX_VERSION = '0.149.1'
const CODEX_PLATFORM_PACKAGES = [
'@openai/codex-darwin-arm64',
'@openai/codex-darwin-x64',
@@ -266,7 +266,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.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await initializing
expect(await child.peer.nextMethod('initialized')).toEqual({
jsonrpc: '2.0',
@@ -286,7 +286,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.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })
@@ -479,12 +479,14 @@ describe('task admission and package contracts', () => {
ctx.on('subagent/provider-removed', providerName => void removed.push(providerName))
const safeFiber = await ctx.plugin(codex, {
providerName: 'codex-safe',
model: 'codex-safe-model',
env: { DSH_CODEX_INSTANCE: 'safe' },
permissionMode: 'never',
disposeGraceMs: 11,
})
const bypassFiber = await ctx.plugin(codex, {
providerName: 'codex-bypass',
model: 'codex-bypass-model',
env: { DSH_CODEX_INSTANCE: 'bypass' },
permissionMode: 'dangerously-bypass-approvals-and-sandbox',
disposeGraceMs: 29,
@@ -498,11 +500,15 @@ describe('task admission and package contracts', () => {
request(undefined, safeController.signal),
)
const bypassStarting = ctx.subagents.start('codex-bypass', request())
for (const child of [safeChild, bypassChild]) {
for (const [child, model] of [
[safeChild, 'codex-safe-model'],
[bypassChild, 'codex-bypass-model'],
] as const) {
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
expect(threadStart.params).toMatchObject({ model })
child.peer.respond(threadStart, {
thread: { id: 'thread-1', ephemeral: true },
})
@@ -570,11 +576,14 @@ describe('task admission and package contracts', () => {
await ctx.fiber.dispose()
})
it('accepts only the three fixed non-interactive permission modes', () => {
it('accepts an optional non-empty model and the three fixed permission modes', () => {
expect(codex.Config({}).providerName).toBe('codex')
expect(codex.Config({}).model).toBeUndefined()
expect(codex.Config({ providerName: 'codex-safe' }).providerName)
.toBe('codex-safe')
expect(() => codex.Config({ providerName: '' })).toThrow()
expect(codex.Config({ model: 'gpt-codex' }).model).toBe('gpt-codex')
expect(() => codex.Config({ model: '' })).toThrow()
expect(codex.Config({}).permissionMode).toBe(DEFAULT_CODEX_PERMISSION_MODE)
for (const permissionMode of CODEX_PERMISSION_MODES) {
expect(codex.Config({ permissionMode }).permissionMode).toBe(permissionMode)
@@ -588,8 +597,29 @@ describe('task admission and package contracts', () => {
const ctx = new Context()
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const child = fakeChild()
vi.spyOn(ctx.subprocess, 'spawn').mockReturnValue(child.handle)
codex.apply(ctx, { env: {}, disposeGraceMs: 3_000 })
expect(ctx.subagents.getProvider('codex')).toBeDefined()
const starting = ctx.subagents.start('codex', request())
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
expect(threadStart.params).not.toHaveProperty('model')
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })
const run = await starting
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.send(
{ id: turnStart.id, result: { turn: { id: 'turn-1' } } },
agentMessage('native model answer', 'final_answer'),
turnCompleted('completed'),
)
await expect(run.result).resolves.toEqual({
output: [{ type: 'text', text: 'native model answer' }],
stopReason: 'completed',
})
await run.dispose()
await ctx.fiber.dispose()
})
@@ -614,7 +644,7 @@ describe('task admission and package contracts', () => {
wire.start()
const initializing = wire.initialize(new AbortController().signal)
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await initializing
await child.peer.nextMethod('initialized')
const starting = wire.startThread('/workspace', new AbortController().signal)
@@ -624,6 +654,34 @@ describe('task admission and package contracts', () => {
ephemeral: true,
...expected,
})
expect(threadStart.params).not.toHaveProperty('model')
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })
await starting
wire.close()
})
it('sends an explicit model on each ephemeral thread', async () => {
const child = fakeChild()
const wire = new CodexAppServerWire(
child.handle.stdout!,
child.handle.stdin!,
'never',
'codex-explicit-model',
)
wire.start()
const initializing = wire.initialize(new AbortController().signal)
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await initializing
await child.peer.nextMethod('initialized')
const starting = wire.startThread('/workspace', new AbortController().signal)
const threadStart = await child.peer.nextMethod('thread/start')
expect(threadStart.params).toEqual({
cwd: '/workspace',
ephemeral: true,
model: 'codex-explicit-model',
approvalPolicy: 'never',
})
child.peer.respond(threadStart, { thread: { id: 'thread-1', ephemeral: true } })
await starting
wire.close()
@@ -695,7 +753,7 @@ describe('CodexAppServerWire', () => {
requestAttestation: false,
},
})
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await initializing
await child.peer.nextMethod('initialized')
@@ -770,21 +828,17 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('maps the complete string error union without changing stop reasons', async () => {
const categories = [
'contextWindowExceeded',
'sessionBudgetExceeded',
'usageLimitExceeded',
'serverOverloaded',
'cyberPolicy',
'internalServerError',
'unauthorized',
'badRequest',
'threadRollbackFailed',
'sandboxError',
'other',
it('groups representative string errors without changing stop reasons', async () => {
const scenarios = [
['contextWindowExceeded', 'limit', 'max-tokens'],
['sessionBudgetExceeded', 'limit', 'error'],
['cyberPolicy', 'access-policy', 'error'],
['misalignmentPolicyViolation', 'access-policy', 'error'],
['serverOverloaded', 'service', 'error'],
['badRequest', 'product-error', 'error'],
['sandboxError', 'access-policy', 'error'],
] as const
for (const category of categories) {
for (const [codexErrorInfo, category, stopReason] of scenarios) {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal)
const turnStart = await child.peer.nextMethod('turn/start')
@@ -793,10 +847,10 @@ describe('CodexAppServerWire', () => {
agentMessage('partial answer', null),
turnCompleted('failed', 'turn-1', 'thread-1', {
message: 'SECRET_TOKEN in /private/secret.txt',
codexErrorInfo: category,
codexErrorInfo,
}),
)
if (category === 'contextWindowExceeded') {
if (stopReason === 'max-tokens') {
await expect(result).resolves.toEqual({
output: [{ type: 'text', text: 'partial answer' }],
stopReason: 'max-tokens',
@@ -814,22 +868,21 @@ describe('CodexAppServerWire', () => {
}
})
it('maps all object error variants and only numeric HTTP status', async () => {
it('groups object errors and retains only numeric HTTP status', async () => {
const scenarios = [
['httpConnectionFailed', { httpStatusCode: 503 }, 503],
['responseStreamConnectionFailed', { httpStatusCode: null }, undefined],
['responseStreamDisconnected', {}, undefined],
['responseTooManyFailedAttempts', { httpStatusCode: '503' }, undefined],
['activeTurnNotSteerable', { turnKind: 'review' }, undefined],
['httpConnectionFailed', { httpStatusCode: 503 }, 'transport', 503],
['responseStreamDisconnected', {}, 'transport', undefined],
['responseTooManyFailedAttempts', { httpStatusCode: '503' }, 'transport', undefined],
['activeTurnNotSteerable', { turnKind: 'review' }, 'product-error', undefined],
] as const
for (const [category, detail, httpStatus] of scenarios) {
for (const [codexErrorInfo, detail, category, httpStatus] of scenarios) {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal)
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
child.peer.send(turnCompleted('failed', 'turn-1', 'thread-1', {
message: 'SECRET_TOKEN in /private/secret.txt',
codexErrorInfo: { [category]: detail },
codexErrorInfo: { [codexErrorInfo]: detail },
}))
await expect(result).rejects.toThrow(`status failed: ${category}`)
expect(wire.collectFailure()).toEqual({
@@ -908,10 +961,12 @@ describe('CodexAppServerWire', () => {
const scenarios: Array<{
readonly frames: JsonObject[]
readonly message: string
readonly category: 'invalid-result' | 'unknown'
}> = [
{
frames: [turnCompleted('completed')],
message: 'without a final answer',
category: 'invalid-result',
},
{
frames: [
@@ -920,30 +975,37 @@ describe('CodexAppServerWire', () => {
turnCompleted('completed'),
],
message: 'without a final answer',
category: 'invalid-result',
},
{
frames: [agentMessage(42, 'final_answer')],
message: 'invalid agent message',
category: 'unknown',
},
{
frames: [agentMessage('answer', 'future_phase')],
message: 'unknown agent message phase',
category: 'unknown',
},
{
frames: [turnCompleted('failed', 'turn-1', 'thread-1', { message: 'no' })],
message: 'status failed',
category: 'unknown',
},
{
frames: [turnCompleted('failed', 'turn-1', 'thread-1', 'SECRET_TOKEN')],
message: 'status failed',
category: 'unknown',
},
{
frames: [turnCompleted('interrupted')],
message: 'status interrupted',
category: 'unknown',
},
{
frames: [turnCompleted('inProgress')],
message: 'invalid terminal turn status',
category: 'unknown',
},
]
for (const scenario of scenarios) {
@@ -956,7 +1018,7 @@ describe('CodexAppServerWire', () => {
await expect(result).rejects.toThrow(scenario.message)
expect(wire.collectFailure()).toEqual({
stage: 'turn',
category: 'unknown',
category: scenario.category,
})
wire.close()
}
@@ -1160,75 +1222,6 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('recognizes large, split, and ordered stderr signatures without retaining raw text', () => {
const first = fakeChild()
const largeWire = new CodexAppServerWire(
first.handle.stdout!,
first.handle.stdin!,
'never',
)
largeWire.observeStderr(
`SECRET_TOKEN approval policy is Never; reject command${'x'.repeat(2_048)}`,
)
expect(largeWire.collectDiagnostic()).toBe(
'Codex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval',
)
expect(largeWire.collectDiagnostic()).not.toContain('SECRET_TOKEN')
const second = fakeChild()
const splitWire = new CodexAppServerWire(
second.handle.stdout!,
second.handle.stdin!,
'never',
)
splitWire.observeStderr('SECRET_TOKEN approval policy is Ne')
splitWire.observeStderr('ver; reject command — /private/secret.txt')
expect(splitWire.collectDiagnostic()).toBe(
'Codex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval',
)
expect(splitWire.collectDiagnostic()).not.toContain('SECRET_TOKEN')
expect(splitWire.collectDiagnostic()).not.toContain('/private/secret.txt')
const third = fakeChild()
const orderedWire = new CodexAppServerWire(
third.handle.stdout!,
third.handle.stdin!,
'dangerously-bypass-approvals-and-sandbox',
)
orderedWire.observeStderr(
'approval policy is Never; reject command; recorded sandbox violation: path=/private/secret.txt',
)
expect(orderedWire.collectDiagnostic()).toBe(
'Codex unattended decision (mode: dangerously-bypass-approvals-and-sandbox; request: sandbox execution; decision: failed): Codex reported a sandbox violation',
)
expect(orderedWire.collectDiagnostic()).not.toContain('/private/secret.txt')
})
it('does not reapply an old stderr signature after a newer request diagnostic', async () => {
const { child, wire } = await initializeWire()
wire.observeStderr('recorded sandbox violation:')
const result = wire.runTurn(['task'], new AbortController().signal)
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
await nextTask()
child.peer.send({
id: 'file-approval',
method: 'item/fileChange/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['decline'],
},
})
await child.peer.nextResponse('file-approval')
expect(wire.collectDiagnostic()).toContain('request: file approval')
wire.observeStderr('later benign stderr')
expect(wire.collectDiagnostic()).toContain('request: file approval')
child.peer.send(agentMessage('answer', 'final_answer'), turnCompleted('completed'))
await expect(result).resolves.toMatchObject({ stopReason: 'completed' })
wire.close()
})
it('keeps a newer request diagnostic after replaying an older early item', async () => {
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal)
@@ -1259,25 +1252,6 @@ describe('CodexAppServerWire', () => {
wire.close()
})
it('keeps a newer stderr fact after replaying an older early terminal', async () => {
hostStderrWrite.capture = true
hostStderrWrite.chunks.length = 0
const { child, wire } = await initializeWire()
const result = wire.runTurn(['task'], new AbortController().signal)
const turnStart = await child.peer.nextMethod('turn/start')
child.peer.send(turnCompleted('failed', 'turn-1', 'thread-1', {
message: 'sandbox failure',
codexErrorInfo: 'sandboxError',
}))
await nextTask()
wire.observeStderr('approval policy is Never; reject command')
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
await expect(result).rejects.toThrow('sandboxError')
expect(wire.collectDiagnostic()).toContain('request: command execution')
wire.close()
hostStderrWrite.capture = false
})
it('fails the run on unknown requests or wrong request association', async () => {
for (const serverRequest of [
{
@@ -1529,7 +1503,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.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
expect(published).toBe(false)
@@ -1619,13 +1593,15 @@ describe('run lifecycle and quiescence', () => {
it('preserves representative terminal categories, HTTP status, and mapping', async () => {
const scenarios = [
['contextWindowExceeded', 'max-tokens', undefined],
['sessionBudgetExceeded', 'error', undefined],
[{ httpConnectionFailed: { httpStatusCode: 503 } }, 'error', 503],
[{ activeTurnNotSteerable: { turnKind: 'review' } }, 'error', undefined],
['futureError', 'error', undefined],
['contextWindowExceeded', 'limit', 'max-tokens', undefined],
['sessionBudgetExceeded', 'limit', 'error', undefined],
['unauthorized', 'access-policy', 'error', undefined],
['internalServerError', 'service', 'error', undefined],
[{ httpConnectionFailed: { httpStatusCode: 503 } }, 'transport', 'error', 503],
[{ activeTurnNotSteerable: { turnKind: 'review' } }, 'product-error', 'error', undefined],
['futureError', 'unknown', 'error', undefined],
] as const
for (const [codexErrorInfo, stopReason, httpStatus] of scenarios) {
for (const [codexErrorInfo, category, stopReason, httpStatus] of scenarios) {
const { child, run, turnStart } = await publishRun()
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
child.peer.send(
@@ -1635,12 +1611,6 @@ describe('run lifecycle and quiescence', () => {
codexErrorInfo,
}),
)
const category = typeof codexErrorInfo === 'string'
&& codexErrorInfo !== 'futureError'
? codexErrorInfo
: typeof codexErrorInfo === 'object'
? Object.keys(codexErrorInfo)[0]!
: 'unknown'
const result = await run.result
expect(result).toEqual({
output: [{ type: 'text', text: 'partial answer' }],
@@ -1656,22 +1626,29 @@ describe('run lifecycle and quiescence', () => {
}
})
it('includes a queued stderr permission fact in a max-token result', async () => {
it('includes a structured permission fact in a max-token result', async () => {
const { child, run, turnStart } = await publishRun()
child.peer.respond(turnStart, { turn: { id: 'turn-1' } })
setImmediate(() => {
child.stderr.write('approval policy is Never; reject command')
child.peer.send(
agentMessage('partial answer', null),
turnCompleted('failed', 'turn-1', 'thread-1', {
codexErrorInfo: 'contextWindowExceeded',
}),
)
child.peer.send({
id: 'approval-before-limit',
method: 'item/commandExecution/requestApproval',
params: {
threadId: 'thread-1',
turnId: 'turn-1',
availableDecisions: ['cancel'],
},
})
await child.peer.nextResponse('approval-before-limit')
child.peer.send(
agentMessage('partial answer', null),
turnCompleted('failed', 'turn-1', 'thread-1', {
codexErrorInfo: 'contextWindowExceeded',
}),
)
child.settle({ exitCode: 17, signal: null })
await expect(run.result).resolves.toEqual({
output: [{ type: 'text', text: 'partial answer' }],
diagnostic: `${expectedFailureDiagnostic('turn', 'contextWindowExceeded', { outcome: { exitCode: 17, signal: null } })}\nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval`,
diagnostic: `${expectedFailureDiagnostic('turn', 'limit', { outcome: { exitCode: 17, signal: null } })}\nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
stopReason: 'max-tokens',
})
await run.dispose()
@@ -1692,13 +1669,13 @@ describe('run lifecycle and quiescence', () => {
child.settle(outcome)
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: expectedFailureDiagnostic('process', 'process-exit', {
diagnostic: expectedFailureDiagnostic('process', 'process', {
outcome,
}),
stopReason: 'error',
})
expect(errors.at(-1)).toBe(
`subagent-codex: ${expectedFailureDiagnostic('process', 'process-exit', { outcome })}`,
`subagent-codex: ${expectedFailureDiagnostic('process', 'process', { outcome })}`,
)
await run.dispose().catch(() => {})
}
@@ -1717,7 +1694,7 @@ describe('run lifecycle and quiescence', () => {
child.fromChild.emit('end')
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: expectedFailureDiagnostic('process', 'process-exit', {
diagnostic: expectedFailureDiagnostic('process', 'process', {
outcome,
}),
stopReason: 'error',
@@ -1736,7 +1713,7 @@ describe('run lifecycle and quiescence', () => {
child.settle({ exitCode: 17, signal: null })
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: expectedFailureDiagnostic('turn', 'other', {
diagnostic: expectedFailureDiagnostic('turn', 'product-error', {
outcome: { exitCode: 17, signal: null },
}),
stopReason: 'error',
@@ -1812,13 +1789,13 @@ describe('run lifecycle and quiescence', () => {
}))
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'other')}\nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
diagnostic: `${expectedFailureDiagnostic('turn', 'product-error')}\nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
stopReason: 'error',
})
await run.dispose()
})
it('drains queued stderr before settling a failed published run', async () => {
it('drains queued stderr to the Host without classifying it', async () => {
hostStderrWrite.capture = true
hostStderrWrite.chunks.length = 0
const { child, run, turnStart } = await publishRun()
@@ -1832,14 +1809,16 @@ describe('run lifecycle and quiescence', () => {
})
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'badRequest')}\nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval`,
diagnostic: expectedFailureDiagnostic('turn', 'product-error'),
stopReason: 'error',
})
expect(Buffer.concat(hostStderrWrite.chunks).toString())
.toContain('approval policy is Never; reject command')
await run.dispose()
hostStderrWrite.capture = false
})
it('forwards stderr while extracting only a fixed safe permission signature', async () => {
it('forwards stderr without copying or classifying it', async () => {
const child = fakeChild()
hostStderrWrite.capture = true
hostStderrWrite.chunks.length = 0
@@ -1854,7 +1833,7 @@ describe('run lifecycle and quiescence', () => {
}))
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'badRequest')}\nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval`,
diagnostic: expectedFailureDiagnostic('turn', 'product-error'),
stopReason: 'error',
})
expect(Buffer.concat(hostStderrWrite.chunks).toString()).toContain('SECRET_TOKEN')
@@ -1877,7 +1856,7 @@ describe('run lifecycle and quiescence', () => {
}))
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'badRequest')}\nCodex unattended decision (mode: never; request: command execution; decision: denied): Codex rejected an escalation because the selected policy never asks for approval`,
diagnostic: expectedFailureDiagnostic('turn', 'product-error'),
stopReason: 'error',
})
await run.dispose()
@@ -1975,7 +1954,7 @@ describe('run lifecycle and quiescence', () => {
const threadChild = fakeChild()
const threadStarting = startCodexRun(request(), runSpec(threadChild))
const threadInitialize = await threadChild.peer.nextMethod('initialize')
threadChild.peer.respond(threadInitialize, { userAgent: 'codex-cli 0.147.0' })
threadChild.peer.respond(threadInitialize, { userAgent: 'codex-cli 0.149.1' })
await threadChild.peer.nextMethod('initialized')
const invalidThread = await threadChild.peer.nextMethod('thread/start')
threadChild.peer.respond(invalidThread, { thread: { id: '', ephemeral: true } })
@@ -1990,7 +1969,7 @@ describe('run lifecycle and quiescence', () => {
)
const exitedThreadInitialize = await exitedThreadChild.peer.nextMethod('initialize')
exitedThreadChild.peer.respond(exitedThreadInitialize, {
userAgent: 'codex-cli 0.147.0',
userAgent: 'codex-cli 0.149.1',
})
await exitedThreadChild.peer.nextMethod('initialized')
await exitedThreadChild.peer.nextMethod('thread/start')
@@ -2009,7 +1988,7 @@ describe('run lifecycle and quiescence', () => {
const eofBeforeCloseInitialize = await eofBeforeCloseChild.peer
.nextMethod('initialize')
eofBeforeCloseChild.peer.respond(eofBeforeCloseInitialize, {
userAgent: 'codex-cli 0.147.0',
userAgent: 'codex-cli 0.149.1',
})
await eofBeforeCloseChild.peer.nextMethod('initialized')
await eofBeforeCloseChild.peer.nextMethod('thread/start')
@@ -2027,7 +2006,7 @@ describe('run lifecycle and quiescence', () => {
const stderrStarting = startCodexRun(request(), runSpec(stderrChild))
const stderrInitialize = await stderrChild.peer.nextMethod('initialize')
stderrChild.stderr.emit('error', new Error('startup stderr broke'))
stderrChild.peer.respond(stderrInitialize, { userAgent: 'codex-cli 0.147.0' })
stderrChild.peer.respond(stderrInitialize, { userAgent: 'codex-cli 0.149.1' })
await stderrChild.peer.nextMethod('initialized')
const stderrThreadStart = await stderrChild.peer.nextMethod('thread/start')
stderrChild.peer.respond(stderrThreadStart, {
@@ -2053,7 +2032,7 @@ describe('run lifecycle and quiescence', () => {
runSpec(child),
)
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
expect(threadStart.params).toEqual({
@@ -2139,12 +2118,12 @@ describe('run lifecycle and quiescence', () => {
}))
await expect(first.run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'other')}\nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
diagnostic: `${expectedFailureDiagnostic('turn', 'product-error')}\nCodex unattended decision (mode: never; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
stopReason: 'error',
})
await expect(second.run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'other')}\nCodex unattended decision (mode: dangerously-bypass-approvals-and-sandbox; request: MCP elicitation; decision: declined): the provider does not collect interactive MCP input`,
diagnostic: `${expectedFailureDiagnostic('turn', 'product-error')}\nCodex unattended decision (mode: dangerously-bypass-approvals-and-sandbox; request: MCP elicitation; decision: declined): the provider does not collect interactive MCP input`,
stopReason: 'error',
})
await Promise.all([first.run.dispose(), second.run.dispose()])
@@ -2162,6 +2141,7 @@ describe('run lifecycle and quiescence', () => {
}) as typeof ctx.logger.warn
await ctx.plugin(codex, {
providerName: 'codex-diagnostic',
model: 'codex-diagnostic-model',
env: { OPENAI_API_KEY: 'fake' },
permissionMode: 'approve-for-me',
disposeGraceMs: 25,
@@ -2207,12 +2187,13 @@ describe('run lifecycle and quiescence', () => {
signal: new AbortController().signal,
})
const initialize = await child.peer.nextMethod('initialize')
child.peer.respond(initialize, { userAgent: 'codex-cli 0.147.0' })
child.peer.respond(initialize, { userAgent: 'codex-cli 0.149.1' })
await child.peer.nextMethod('initialized')
const threadStart = await child.peer.nextMethod('thread/start')
expect(threadStart.params).toEqual({
cwd: process.cwd(),
ephemeral: true,
model: 'codex-diagnostic-model',
approvalPolicy: 'on-request',
approvalsReviewer: 'auto_review',
sandbox: 'workspace-write',
@@ -2239,7 +2220,7 @@ describe('run lifecycle and quiescence', () => {
}))
await expect(run.result).resolves.toEqual({
output: [],
diagnostic: `${expectedFailureDiagnostic('turn', 'other')}\nCodex unattended decision (mode: approve-for-me; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
diagnostic: `${expectedFailureDiagnostic('turn', 'product-error')}\nCodex unattended decision (mode: approve-for-me; request: command approval; decision: cancelled): the provider does not grant interactive approval`,
stopReason: 'error',
})
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
@@ -2249,7 +2230,7 @@ describe('run lifecycle and quiescence', () => {
}))
expect(warnings).toEqual([
expect.stringContaining(
`subagent-codex "codex-diagnostic": child run failed (error): subagent-codex: ${expectedFailureDiagnostic('turn', 'other')}`,
`subagent-codex "codex-diagnostic": child run failed (error): subagent-codex: ${expectedFailureDiagnostic('turn', 'product-error')}`,
),
])
expect(warnings.join('\n')).not.toContain('SECRET_TOKEN')
@@ -24,12 +24,12 @@ const RESULTS = [
},
{
id: '00000000-0000-4000-8000-0000000000d3',
diagnostic: 'Product subagent failure (product: Codex; stage: turn; category: httpConnectionFailed; HTTP status: 503)',
diagnostic: 'Product subagent failure (product: Codex; stage: turn; category: transport; HTTP status: 503)',
output: [{ type: 'text' as const, text: 'partial assistant text' }],
},
{
id: '00000000-0000-4000-8000-0000000000d4',
diagnostic: 'Product subagent failure (product: Codex; stage: turn; category: httpConnectionFailed; HTTP status: 503)',
diagnostic: 'Product subagent failure (product: Codex; stage: turn; category: transport; HTTP status: 503)',
output: [],
},
] as const
+29 -29
View File
@@ -8250,8 +8250,8 @@ importers:
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
'@openai/codex':
specifier: 0.147.0
version: 0.147.0
specifier: 0.149.1
version: 0.149.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -11566,43 +11566,43 @@ packages:
resolution: {integrity: sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==}
engines: {node: '>= 20'}
'@openai/codex@0.147.0':
resolution: {integrity: sha512-EQLEXecAG2ptxI7UpBMo2TR/ga5596/c/OsYF/0LoUDh5JANZ7IoGqlzBEWbuEVQ76JePIbtTW/ihCkp1a7Z3w==}
'@openai/codex@0.149.1':
resolution: {integrity: sha512-6q5pbcpFbJbqOpkubSDBwXmktQ55aD8eUzGzBF1zASob2DjwhBKDSNGtdZKalfrNJUdTDTPDMmzCXEXs5tMBYA==}
engines: {node: '>=16'}
hasBin: true
'@openai/codex@0.147.0-darwin-arm64':
resolution: {integrity: sha512-BEUVkiOW7kLcRyrMLfAr/h9wF8sRVJyZDy6OHtVn6QGDXiv3BvAZVTY1Pu9xF7KdIdkYXbp4uayN0aDQQaAUJw==}
'@openai/codex@0.149.1-darwin-arm64':
resolution: {integrity: sha512-6X84kTCbnTgPIJ2EdcPsrvwS0Wxsqpa+bCswGmRf4BjhcQ5nPMnBC6yCAaCMj+vrbXQHj+L6sa9FaR4QkmA1qw==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@openai/codex@0.147.0-darwin-x64':
resolution: {integrity: sha512-Tb8McE5SvJIH0Vs5R6sq7u+quiC931yan2KOOl6km1OdZ82+Wi7eF5XrSFPs5CF7xCgoIK4Vs+byMbT5hN+ZUw==}
'@openai/codex@0.149.1-darwin-x64':
resolution: {integrity: sha512-MfLBQLfcElJL9tvj6y45qVHHMGSXCPnQOixuD3/Zq0g1BW/eFizkrGLdn48cFpc+l8cK+gt5nYG5pQYwVs6g4A==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@openai/codex@0.147.0-linux-arm64':
resolution: {integrity: sha512-SLC1JXw2TYfr/c3HhrJubyyLelq7vTOLWVmiThFA+z0+WgzCPmaseJ/kzDD3Gge/TO7fCnnj7UcPmC0d2c8XAg==}
'@openai/codex@0.149.1-linux-arm64':
resolution: {integrity: sha512-OqxUfZ1TVvHd18zHPKK/8ZRlpk8Vy11mg5CMHaLxNWldTbwVImDKtSLWT+m8m4NM5Sz4PbjtZMrVT/RfpBW/mQ==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@openai/codex@0.147.0-linux-x64':
resolution: {integrity: sha512-0W9MBxPpWW0cSkNqrTDN2jR7rzzT7oNMhQY5446lT2Lw5cz5yhDTck4Va9rjkQEm+HlFzP/dmEMSZbXfJsINmw==}
'@openai/codex@0.149.1-linux-x64':
resolution: {integrity: sha512-Of5fGYgr7tAMsyj6vhXb4/RM/UoA3Zq8BLegUBDC09UNy1XTLGYP/2XD+UX8z3qh0NDwxYdCjFIWdDNijKZggQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@openai/codex@0.147.0-win32-arm64':
resolution: {integrity: sha512-e2ZstJ8zT8Rm1nvR7CUVO+Gr3cTChE41+VfOzGhynzDXEoW0wfbjUQbc2bWbh1arG94LMm4y3dqBtUIbSrfeGA==}
'@openai/codex@0.149.1-win32-arm64':
resolution: {integrity: sha512-5K0DmOKGK9Bos627p8sK8ATHjovPK0sDyT6h9Cb+4v+5CW5SGw1HLgjGxoLfJ8g3cg6mtg/pRCXXo2L/j71UVA==}
engines: {node: '>=16'}
cpu: [arm64]
os: [win32]
'@openai/codex@0.147.0-win32-x64':
resolution: {integrity: sha512-oT7Ss5fAPf2fiWE9QNURqZcQGAAawSVxmIUdgPzckq4KFZAM+pRz9JbM4Rr498CjtbNgTOjWvDJ+DXvIBSfOPA==}
'@openai/codex@0.149.1-win32-x64':
resolution: {integrity: sha512-G3QXGAg7nyyhqOeooAMUekBCeHd8a1QByhKcVAFyzNBaI06t6Ft7nsF+1SzFS0spuIdU4YyMi5YD26ukADBQUQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
@@ -17223,31 +17223,31 @@ snapshots:
'@octokit/request-error': 7.1.1
'@octokit/webhooks-methods': 6.0.0
'@openai/codex@0.147.0':
'@openai/codex@0.149.1':
optionalDependencies:
'@openai/codex-darwin-arm64': '@openai/codex@0.147.0-darwin-arm64'
'@openai/codex-darwin-x64': '@openai/codex@0.147.0-darwin-x64'
'@openai/codex-linux-arm64': '@openai/codex@0.147.0-linux-arm64'
'@openai/codex-linux-x64': '@openai/codex@0.147.0-linux-x64'
'@openai/codex-win32-arm64': '@openai/codex@0.147.0-win32-arm64'
'@openai/codex-win32-x64': '@openai/codex@0.147.0-win32-x64'
'@openai/codex-darwin-arm64': '@openai/codex@0.149.1-darwin-arm64'
'@openai/codex-darwin-x64': '@openai/codex@0.149.1-darwin-x64'
'@openai/codex-linux-arm64': '@openai/codex@0.149.1-linux-arm64'
'@openai/codex-linux-x64': '@openai/codex@0.149.1-linux-x64'
'@openai/codex-win32-arm64': '@openai/codex@0.149.1-win32-arm64'
'@openai/codex-win32-x64': '@openai/codex@0.149.1-win32-x64'
'@openai/codex@0.147.0-darwin-arm64':
'@openai/codex@0.149.1-darwin-arm64':
optional: true
'@openai/codex@0.147.0-darwin-x64':
'@openai/codex@0.149.1-darwin-x64':
optional: true
'@openai/codex@0.147.0-linux-arm64':
'@openai/codex@0.149.1-linux-arm64':
optional: true
'@openai/codex@0.147.0-linux-x64':
'@openai/codex@0.149.1-linux-x64':
optional: true
'@openai/codex@0.147.0-win32-arm64':
'@openai/codex@0.149.1-win32-arm64':
optional: true
'@openai/codex@0.147.0-win32-x64':
'@openai/codex@0.149.1-win32-x64':
optional: true
'@opentelemetry/api-logs@0.220.0':
+3
View File
@@ -70,6 +70,9 @@ minimumReleaseAgeExclude:
- '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.241'
- '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.241'
- '@anthropic-ai/claude-agent-sdk@0.3.241'
# All platform aliases resolve to the @openai/codex package name, so their
# reviewed exact versions must share one version-union policy entry.
- '@openai/codex@0.149.1||0.149.1-darwin-arm64||0.149.1-darwin-x64||0.149.1-linux-arm64||0.149.1-linux-x64||0.149.1-win32-arm64||0.149.1-win32-x64'
patchedDependencies:
node-pty@1.2.0-beta.15: patches/node-pty@1.2.0-beta.15.patch
@@ -18,10 +18,12 @@
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-primary
model: codex-primary-model
- id: subagent-codex-secondary
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-secondary
model: codex-secondary-model
- id: subagent-claude-primary
name: '@deepseek-ai/dsh-subagent-claude-code'
config:
@@ -6,10 +6,12 @@
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-primary
model: codex-primary-model
- id: subagent-codex-secondary
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-secondary
model: codex-secondary-model
- id: subagent-claude-primary
name: '@deepseek-ai/dsh-subagent-claude-code'
config:
@@ -18,10 +18,12 @@
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-primary
model: codex-primary-model
- id: subagent-codex-secondary
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-secondary
model: codex-secondary-model
- id: tool-subagent-codex-primary
name: '@deepseek-ai/dsh-tool-subagent'
config:
@@ -6,10 +6,12 @@
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-primary
model: codex-primary-model
- id: subagent-codex-secondary
name: '@deepseek-ai/dsh-subagent-codex'
config:
providerName: codex-secondary
model: codex-secondary-model
- id: tool-subagent-codex-primary
name: '@deepseek-ai/dsh-tool-subagent'
config:
@@ -51,7 +51,7 @@
{"type":"assistant/chunk","data":{"turn":1,"step":4,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":4,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_codex_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe Codex foreground diagnostic\",\"prompt\":\"Return the Codex diagnostic failure.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:10}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[45,46,47,48,49],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":4,"callId":"call_codex_foreground","name":"subagent_codex","arguments":"{\"description\":\"Observe Codex foreground diagnostic\",\"prompt\":\"Return the Codex diagnostic failure.\",\"run_in_background\":false}"}}
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_codex_foreground"},"content":[{"type":"tool-result","toolCallId":"call_codex_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Product subagent failure (product: Codex; stage: turn; category: httpConnectionFailed; HTTP status: 503)\nPartial output before the run ended:\npartial assistant text"}],"isError":true}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":4,"message":{"source":{"kind":"tool","callId":"call_codex_foreground"},"content":[{"type":"tool-result","toolCallId":"call_codex_foreground","content":[{"type":"text","text":"Error: subagent run failed\nDiagnostic: Product subagent failure (product: Codex; stage: turn; category: transport; HTTP status: 503)\nPartial output before the run ended:\npartial assistant text"}],"isError":true}],"role":"user","id":"{{message:11}}"}},"sourceEventSeqs":[51],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":4}}
{"type":"step/start","data":{"turn":1,"step":5}}
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
@@ -61,12 +61,12 @@
{"type":"assistant/chunk","data":{"turn":1,"step":5,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":5,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_codex_background","name":"subagent_codex","arguments":"{\"description\":\"Observe Codex background diagnostic\",\"prompt\":\"Return the Codex diagnostic failure.\",\"run_in_background\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:12}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[55,56,57,58,59],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":5,"callId":"call_codex_background","name":"subagent_codex","arguments":"{\"description\":\"Observe Codex background diagnostic\",\"prompt\":\"Return the Codex diagnostic failure.\",\"run_in_background\":true}"}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job subagent-2 (subagent: Observe Codex background diagnostic) finished [status: failed, error; diagnostic: Product subagent failure (product: Codex; stage: turn; category: httpConnectionFailed; HTTP status: 503)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe Codex background diagnostic [status: failed, error; diagnostic: Product subagent failure (product: Cod…"},"role":"user","id":"{{message:13}}"}]}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job subagent-2 (subagent: Observe Codex background diagnostic) finished [status: failed, error; diagnostic: Product subagent failure (product: Codex; stage: turn; category: transport; HTTP status: 503)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe Codex background diagnostic [status: failed, error; diagnostic: Product subagent failure (product: Cod…"},"role":"user","id":"{{message:13}}"}]}}
{"type":"tool/result","data":{"turn":1,"step":5,"message":{"source":{"kind":"tool","callId":"call_codex_background"},"content":[{"type":"tool-result","toolCallId":"call_codex_background","content":[{"type":"text","text":"started background subagent job subagent-2"}],"isError":false}],"role":"user","id":"{{message:14}}"}},"sourceEventSeqs":[61],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":5}}
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
{"type":"step/start","data":{"turn":1,"step":6}}
{"type":"user/message","data":{"content":[{"type":"text","text":"background job subagent-2 (subagent: Observe Codex background diagnostic) finished [status: failed, error; diagnostic: Product subagent failure (product: Codex; stage: turn; category: httpConnectionFailed; HTTP status: 503)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe Codex background diagnostic [status: failed, error; diagnostic: Product subagent failure (product: Cod…"},"role":"user","id":"{{message:13}}"},"surfaceOp":"append"}
{"type":"user/message","data":{"content":[{"type":"text","text":"background job subagent-2 (subagent: Observe Codex background diagnostic) finished [status: failed, error; diagnostic: Product subagent failure (product: Codex; stage: turn; category: transport; HTTP status: 503)]. Read its output with job_output."}],"source":{"kind":"plugin","plugin":"tool-jobs","form":"notice","summary":"subagent Observe Codex background diagnostic [status: failed, error; diagnostic: Product subagent failure (product: Cod…"},"role":"user","id":"{{message:13}}"},"surfaceOp":"append"}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"tool-call-delta","index":0,"id":"call_codex_output","name":"job_output","argumentsDelta":"{\"job_id\":\"subagent-2\",\"wait\":true}"}}}
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_codex_output","name":"job_output","arguments":"{\"job_id\":\"subagent-2\",\"wait\":true}"}}}}
@@ -74,7 +74,7 @@
{"type":"assistant/chunk","data":{"turn":1,"step":6,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","data":{"turn":1,"step":6,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_codex_output","name":"job_output","arguments":"{\"job_id\":\"subagent-2\",\"wait\":true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-pro"},"id":"{{message:15}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[68,69,70,71,72],"surfaceOp":"append"}
{"type":"tool/call","data":{"turn":1,"step":6,"callId":"call_codex_output","name":"job_output","arguments":"{\"job_id\":\"subagent-2\",\"wait\":true}"}}
{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"call_codex_output"},"content":[{"type":"tool-result","toolCallId":"call_codex_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Product subagent failure (product: Codex; stage: turn; category: httpConnectionFailed; HTTP status: 503)]"}],"isError":false}],"role":"user","id":"{{message:16}}"}},"sourceEventSeqs":[74],"surfaceOp":"append"}
{"type":"tool/result","data":{"turn":1,"step":6,"message":{"source":{"kind":"tool","callId":"call_codex_output"},"content":[{"type":"tool-result","toolCallId":"call_codex_output","content":[{"type":"text","text":"(no new output)\n[status: failed, error; diagnostic: Product subagent failure (product: Codex; stage: turn; category: transport; HTTP status: 503)]"}],"isError":false}],"role":"user","id":"{{message:16}}"}},"sourceEventSeqs":[74],"surfaceOp":"append"}
{"type":"step/end","data":{"turn":1,"step":6}}
{"type":"step/start","data":{"turn":1,"step":7}}
{"type":"assistant/chunk","data":{"turn":1,"step":7,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}