Skip to content

Agent无感透明分批写入:解决Windows大文件patch argv长度限制 #48

@ARCJ137442

Description

@ARCJ137442

上游关联

  • 上游 Issue: openai/codex#9688 — "Codex fails append to a file if code is large"
  • 上游状态: 🟡 OPEN(2026-01-22 报告,至今未修复)
  • 上游确认根因(etraut-openai):

    "It's because we're passing the full patch to the tool, and this overflows the length limit imposed by CreateProcess on Windows."


问题描述

当 Codex 一次性生成较大的 patch(通常几百行以上)时,在 Windows 上会触发以下错误:

✘ Failed to apply patch
  └─ execution error: Io(Os { code: 206, kind: InvalidFilename, message: "文件名或扩展名太长。" })

触发条件(可 100% 复现):

  • 单次 patch 内容 > ~30,000 字符(Windows CreateProcess 命令行长度限制)
  • 大规模重构:一次性写入多个大文件
  • Codex 生成 1000+ 行代码

根因定位(已确认)

ApplyPatchRuntime::build_command_spec()
  └─ args: vec![CODEX_CORE_APPLY_PATCH_ARG1, req.action.patch.clone()]
     └─ tokio::process::Command::args()
        └─ Windows: CreateProcessAsUserW(cmdline_str.join(" "))
           └─ Windows 限制: 32,767 字符
           └─ ERROR_FILENAME_EXCED_RANGE (code 206)

关键文件

  • codex-rs/core/src/tools/runtimes/apply_patch.rs:65-98 — 把 patch 塞进 argv
  • codex-rs/windows-sandbox-rs/src/process.rs:72-77CreateProcessAsUserW 拼接命令行
  • codex-rs/apply-patch/src/standalone_executable.rs:11-40stdin fallback 存在但从未被使用

解决思路

核心设计原则

Agent 无感:在 Codex Agent 层表现为一次性写入大文件,实际执行时分批写入,Agent 不知道底层做了分片。


实现方案

架构设计

ApplyPatchHandler
  └─ ApplyPatchRuntime
       └─ [新增] ChunkedApplyPatchRuntime (wraps ApplyPatchRuntime)
            ├─ 检测 patch 大小是否超过阈值
            ├─ [超过阈值] → PatchChunker.split() → 分批执行
            └─ [未超阈值] → 直接透传到 ApplyPatchRuntime

新增模块:PatchChunker

文件位置codex-rs/core/src/tools/runtimes/patch_chunker.rs(新文件)

核心数据结构

/// Patch 分片策略配置
const CHUNK_THRESHOLD_CHARS: usize = 20_000;  // 每批字符数上限,留安全余量
const CONTEXT_LINES: usize = 3;               // 边界保留的上下文行数

/// 分割后的子 Patch 片段
pub struct Chunk {
    pub patch: String,          // 可独立执行的 patch 文本
    pub file_count: usize,     // 涉及的文件数量
    pub is_first: bool,        // 是否为首批
    pub is_last: bool,         // 是否为最后一批
}

分割策略

策略 1:AddFile(新文件)的分割

对于大文件 AddFile,分割为:

批次 Patch 操作 关键机制
首批 AddFile: <path> + 首批内容 创建文件
后继批 UpdateFile: <path> + @@ End of File + 追加内容 使用 is_end_of_file: true 追加到文件末尾

具体示例:500 行的新文件 src/app.py

# Patch 1(首批)
*** Begin Patch
*** Add File: src/app.py
+line1
+line2
+...(前200行)
*** End Patch

# Patch 2(追加)
*** Begin Patch
*** Update File: src/app.py
@@ End of File
+line201
+line202
+...(中间200行)
*** End of File
*** End Patch

# Patch 3(追加)
*** Begin Patch
*** Update File: src/app.py
@@ End of File
+line401
+line402
+...(最后100行)
*** End of File
*** End Patch

策略 2:UpdateFile(更新现有文件)的分割

对于大文件的 UpdateFile,按以下原则分割:

  • 保持 UpdateFile 整体性:单个文件的 UpdateFile 不拆分到不同 Patch
  • 多文件时按 Patch 分割:在文件级别分割,确保每个子 patch 不超过阈值
  • 保留上下文行:在 @@ 行处分割,保证后续 patch 仍能找到正确位置

具体示例:3 个文件的 Patch,其中 large.py 很大

# Patch 1(首批)
*** Begin Patch
*** Add File: small.py
+small content
*** End Patch

# Patch 2(large.py 的首批)
*** Begin Patch
*** Update File: large.py
@@ class Foo
 old_line
+new_line
+...(大文件主体内容)
*** End Patch

# Patch 3(large.py 的追加 + another.py)
*** Begin Patch
*** Update File: large.py
@@ End of File
+more_large_content
+...
*** End Patch
*** Add File: another.py
+content
*** End Patch

策略 3:DeleteFile(删除文件)的处理

  • DeleteFile 操作本身很小,不会触发阈值
  • 如果单个 DeleteFile 导致 patch 超限(极端情况),直接跳过分割,正常执行

执行层:分批执行

文件位置codex-rs/core/src/tools/runtimes/patch_chunker.rs

pub struct ChunkedApplyPatchRuntime {
    inner: ApplyPatchRuntime,
    threshold: usize,
}

impl ChunkedApplyPatchRuntime {
    async fn run_chunked(
        &mut self,
        req: &ApplyPatchRequest,
        attempt: &SandboxAttempt<'_>,
        ctx: &ToolCtx,
    ) -> Result<ExecToolCallOutput, ToolError> {
        let chunks = PatchChunker::split(req, self.threshold);
        
        if chunks.len() == 1 {
            // 单批,直接执行
            return self.inner.run(req, attempt, ctx).await;
        }

        let mut outputs = Vec::with_capacity(chunks.len());
        
        for (i, chunk) in chunks.iter().enumerate() {
            // 构建当前批次的 ApplyPatchRequest
            let chunk_req = req.with_patch(chunk.patch.clone());
            
            // 执行
            let result = self.inner.run(&chunk_req, attempt, ctx).await;
            
            match result {
                Ok(out) => outputs.push(out),
                Err(e) => {
                    // 记录已成功执行的批次
                    return Err(ToolError::ChunkedApplyPatchFailed {
                        failed_chunk_index: i,
                        total_chunks: chunks.len(),
                        partial_outputs: outputs,
                        cause: Box::new(e),
                    });
                }
            }
        }

        // 合并所有输出
        Ok(ExecToolCallOutput::merge(outputs))
    }
}

错误处理

#[derive(Error, Debug)]
pub enum ChunkedApplyPatchError {
    #[error("Patch 被分为 {total_chunks} 批,执行到第 {failed_chunk_index} 批时失败")]
    PartialFailure {
        failed_chunk_index: usize,
        total_chunks: usize,
        chunk_summary: String,  // 例如 "批1: 修改 src/app.py, 批2: 新增 large.py + small.py"
    },
}

ToolCtx 输出

当分批执行时,输出合并为:

Success. Updated the following files:
A src/app.py          # 来自批1
M src/app.py          # 来自批2
A src/large.py        # 来自批2
M src/large.py        # 来自批3
A src/another.py      # 来自批3
[已分 3 批透明执行]

实现步骤

Phase 1: 核心模块

  • 新建 codex-rs/core/src/tools/runtimes/patch_chunker.rs
  • 实现 PatchChunker::split() 分割逻辑
  • 实现 ChunkedApplyPatchRuntime 包装器
  • 实现 ApplyPatchRequest::with_patch() 辅助方法

Phase 2: 集成

  • 修改 ApplyPatchHandler 使用 ChunkedApplyPatchRuntime 替代 ApplyPatchRuntime
  • intercept_apply_patch() 中同步更新
  • 添加分批执行的 instrument 日志

Phase 3: 测试

  • 单元测试:PatchChunker::split() 覆盖各类分割场景
  • 集成测试:超大 patch(~100KB)的分批执行
  • Windows 环境验证:确认 argv 长度限制绕过
  • 回归测试:正常大小 patch 不受影响

Phase 4: 配置与文档

  • 将阈值 CHUNK_THRESHOLD 暴露为可配置项
  • 更新 AGENTS.md 记录此行为
  • 添加 changelog 条目

关键文件清单

操作 文件
新增 codex-rs/core/src/tools/runtimes/patch_chunker.rs
修改 codex-rs/core/src/tools/runtimes/mod.rs — 导出新模块
修改 codex-rs/core/src/tools/handlers/apply_patch.rs — 换用 ChunkedRuntime
修改 codex-rs/core/src/tools/handlers/apply_patch.rsintercept_apply_patch 同步更新
修改 codex-rs/apply-patch/src/lib.rs — 可选:增强 ApplyPatchAction::with_patch()

上游关联 Issue

# 标题 状态
#9688 Codex fails append to a file if code is large OPEN
#15003 apply_patch fails for large patches (argv transport) OPEN
#17107 CreateProcessAsUserW failed: 206 (~100KB patches) OPEN
#17259 Content ~1500+ chars hits command-length limit OPEN
#11099 Writing large C++ file triggered command-line limit CLOSED

Definition of Done

  • patch >= 20,000 字符时,自动分批写入,Agent 表现仍为"一次性写入"
  • 每批写入独立成功/失败反馈,失败时可定位到具体哪批
  • 原 argv 路径(小于阈值)不受影响,零行为变化
  • Windows 平台验证:~1500+ 行文件一次性写入成功
  • 与现有 apply_patch 分块语法完全兼容
  • 分片阈值可配置
  • 单元测试 + 集成测试覆盖
  • 无回归:现有 patch 行为不变

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions