Skip to content

Commit f91a340

Browse files
authored
docs + fix: followup #39 + README 精简 + apply_patch prompt 加强 normative + release-notes v2.1.13 (#241)
post-PR #240 收尾 batch: - followup #39 conditional active(PR #239 server-side normalize 兜底,等 prompt-only 失效再激活) - README 顶部红字精简(删 "立即销毁,绝不挪作他用") - apply_patch prompt 加强 normative:ALWAYS use apply_patch / NEVER shell `>` redirect 写文件内容 + 全文 rewrite 用 Delete + Add(用户实测反馈 2026-05-22) - prompt rule 5 / gotcha 3 carve-out:`printf '\n' > <path>` seed 空文件是 apply_patch 预备步骤,不算 bypass(Devin pre-merge review fix × 2) - docs/release-notes/v2.1.13.md 起草 + 精简到 [2500, 3200] 区间(memory rule) - Cargo.lock sync v2.1.13(PR #242 漏 commit 补上) Refs #235
1 parent 96d9399 commit f91a340

9 files changed

Lines changed: 166 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
>
88
> Other built-in chat-completions-compatible providers (including **DeepSeek, Kimi (Moonshot Platform), Xiaomi MiMo (Pay for Token), Zhipu GLM, Aliyun Bailian (API Key / Token Plan), MiniMax**) **have not undergone long-term real-world regression** — they sit at unit-test + occasional user-report level only.
99
>
10-
> If you'd be willing to **provide an API key from another provider for testing**, it would be deeply appreciated! Reach out via **QQ: `3216202644`** or email. The author guarantees the **API key will only be used for actual testing of this project**, destroyed immediately after testing, never repurposed.
10+
> If you'd be willing to **provide an API key from another provider for testing**, it would be deeply appreciated! Reach out via **QQ: `3216202644`** or email — the author guarantees the **API key will only be used for actual testing of this project**.
1111
1212
<p align="center">
1313
<a href="README.md">简体中文</a> |

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
>
88
> 其他已内置的 chat-completions 兼容供应商(包括 **DeepSeek、Kimi(月之暗面)、Xiaomi MiMo(Pay for Token)、智谱 GLM、阿里云百炼(API Key / Token Plan)、MiniMax**)**未做长期真机回归**,仅停留在单元测试 + 偶发用户反馈层面。
99
>
10-
> 如果你愿意**提供其他供应商的 API key 用于测试**,将万分感激!可通过 **QQ:`3216202644`** 或邮箱联系作者作者保证 **API key 仅用于本项目实际测试**,测试结束后立即销毁,绝不挪作他用
10+
> 如果你愿意**提供其他供应商的 API key 用于测试**,将万分感激!可通过 **QQ:`3216202644`** 或邮箱联系作者,作者保证 **API key 仅用于本项目实际测试**
1111
1212
<p align="center">
1313
<a href="README.md">简体中文</a> |

crates/adapters/src/responses/request.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2315,6 +2315,9 @@ use tools::{
23152315
/// 4. 加 "If Update repeatedly fails, fall back to Delete + Add File" 兜底
23162316
const APPLY_PATCH_CHAT_PATH_SYSTEM_GUIDANCE: &str = concat!(
23172317
"[apply_patch chat-path guidance — injected by codex-app-transfer adapter because the upstream lark grammar constraint is unavailable on chat function-call providers]\n",
2318+
"\n",
2319+
"**ALWAYS use the `apply_patch` tool to write file content** — new files, single-line edits, and full-file rewrites alike. **NEVER use shell `cat <<EOF > file` / `printf '<content>' > file` / `echo '<content>' > file` / any `>` redirect to write actual file content** — doing so bypasses the Codex diff UI and audit trail. (The narrow exception in rule 5 below — `printf '\\n' > <path>` to seed an empty file before `*** Update File:` — is an apply_patch preparation step, not a content bypass.) For full-file rewrites or large changes where almost every line differs, use `*** Delete File: <path>` followed by `*** Add File: <path>` (every line of the new content prefixed with `+`) inside the same patch — this is more concise than a long `-`/`+` diff and is the correct apply_patch idiom for large rewrites.\n",
2320+
"\n",
23182321
"When you call the `apply_patch` tool, follow these rules empirically observed with non-OpenAI chat providers:\n",
23192322
"\n",
23202323
"1. PREFERRED Update File form is MINIMAL: just `-line` (the row to remove, byte-exact) and `+line` (the new row) directly after `*** Update File: <path>` — NO `@@`, NO context lines. ",

crates/adapters/src/responses/request/tests.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,27 @@ fn apply_patch_chat_path_guidance_injected_when_tool_registered() {
20312031
guidance.contains("For pure rename"),
20322032
"guidance 必须给纯重命名的 Delete+Add 替代方案:{guidance}"
20332033
);
2034+
// 用户实测反馈(2026-05-22):模型在"几乎全替换"场景倾向用 `cat > file`
2035+
// 绕过 apply_patch。prompt 必须强 normative 明示:任何文件操作走 apply_patch,
2036+
// 全文 rewrite 用 Delete File + Add File。
2037+
assert!(
2038+
guidance.contains("ALWAYS use the `apply_patch` tool")
2039+
&& guidance.contains("NEVER use shell"),
2040+
"guidance 必须强 normative 禁 shell redirect:{guidance}"
2041+
);
2042+
assert!(
2043+
guidance.contains("full-file rewrites")
2044+
&& guidance.contains("`*** Delete File:")
2045+
&& guidance.contains("`*** Add File:"),
2046+
"guidance 必须明示 large rewrite 走 Delete File + Add File:{guidance}"
2047+
);
2048+
// 必须含 rule 5 `printf '\n' > <path>` carve-out 明示
2049+
// (防 Devin pre-merge review 报跟 rule 5 冲突,2026-05-22)
2050+
assert!(
2051+
guidance.contains("narrow exception in rule 5")
2052+
&& guidance.contains("preparation step, not a content bypass"),
2053+
"guidance 必须 carve-out rule 5 的 `printf '\\n' > <path>` setup 用法:{guidance}"
2054+
);
20342055
}
20352056

20362057
#[test]
@@ -2993,6 +3014,19 @@ fn tools_custom_apply_patch_injects_v4a_format_hint() {
29933014
outer.contains("pure rename") && outer.contains("Delete File"),
29943015
"outer description 必须给纯重命名的 Delete+Add 替代方案:{outer}"
29953016
);
3017+
// 用户实测反馈(2026-05-22)— 全文 rewrite 倾向 shell 绕过:
3018+
assert!(
3019+
outer.contains("ALWAYS use this tool") && outer.contains("NEVER use shell"),
3020+
"outer description 必须强 normative 禁 shell redirect:{outer}"
3021+
);
3022+
assert!(
3023+
outer.contains("full-file rewrites") && outer.contains("`*** Delete File:"),
3024+
"outer description 必须明示 large rewrite 走 Delete + Add File:{outer}"
3025+
);
3026+
assert!(
3027+
outer.contains("narrow exception is seeding a totally empty file"),
3028+
"outer description 必须 carve-out 空文件 seed 用法:{outer}"
3029+
);
29963030

29973031
// 参数描述紧凑版必须含同样核心规则(round 4 修复后)
29983032
assert!(

crates/adapters/src/responses/request/tools.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ pub(crate) const APPLY_PATCH_TOOL_NAME: &str = "apply_patch";
3030
/// 可执行的更新文件 example 让模型看到正确形态。
3131
pub(crate) const APPLY_PATCH_TOOL_DESCRIPTION_FOR_CHAT: &str = concat!(
3232
"Edit files using the apply_patch tool. ",
33+
"**ALWAYS use this tool to write file content** — new files, single-line edits, and full-file rewrites alike. ",
34+
"**NEVER use shell `cat <<EOF > file` / `printf '<content>' > file` / `echo '<content>' > file` / any `>` redirect to write actual file content** — doing so bypasses the Codex diff UI and audit trail. ",
35+
"(The narrow exception is seeding a totally empty file with `printf '\\n' > <path>` before calling `*** Update File:` — see gotcha 3; that's a setup step, not a content bypass.) ",
36+
"For full-file rewrites or large changes where almost every line differs, use `*** Delete File: <path>` followed by `*** Add File: <path>` (with `+` prefix on every line of the new content) inside a single patch — this is more concise than a long `-`/`+` diff and is the correct apply_patch idiom for large rewrites. ",
3337
"Call this function with a single `input` string containing a V4A patch. ",
3438
"**The patch MUST start with `*** Begin Patch` as the literal first line** (no leading whitespace, no other content before it), and end with `*** End Patch`. ",
3539
"Each file operation header is one of `*** Add File: <path>`, ",

docs/followup-tracker.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ related_pr: <PR# 或 null>
8080
## Active
8181

8282
- [#32 P2 Plugin Unlock macOS:setAuthMethod 触发 React 整树重渲(物理消除可行性调研)](followup/32-plugin-unlock-react-context-rerender.md) — PR #191 已 P0 缓解,长期消除需 hook Codex Desktop preload 跨版本不稳
83+
- [#39 P3 shell→apply_patch normalize 兜底(prompt-only 失效再激活)](followup/39-shell-to-apply-patch-normalize-fallback.md) — issue #235 通过 PR #236+#240 prompt-only 修复,PR #239 server-side normalize 暂时不需要;若未来某 provider 模型完全无视 prompt 走 shell file-write 再 cherry-pick PR #239 复活
8384

8485
---
8586

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
id: 39
3+
priority: P3
4+
type: research
5+
status: active
6+
created: 2026-05-22
7+
related_pr: 239
8+
---
9+
10+
# shell→apply_patch normalize 兜底(等 prompt-only 路径失效再激活 PR #239 复活)
11+
12+
## 触发上下文
13+
14+
issue #235 收尾时(2026-05-21),`apply_patch` 在 chat-completions provider 上不渲染 diff UI 的问题通过 **prompt-only** 方案修复:
15+
16+
- **PR #236**(merged 2026-05-21):wire`custom_tool_call` SSE 桥接 + 多 commit 演进的 V4A prompt(单端 `@@` / minimal Update form / Add File `+` 前缀 / prefix 无空格)
17+
- **PR #240**(merged 2026-05-22):Move 空 hunk + `*** Begin Patch` first-line 显式约束
18+
19+
真机三 provider round 7-9 数据证明 **prompt-only 方案足够**:
20+
- Kimi For Coding round 7:14 个 apply_patch 调用 / 12 success / 85.7%(2 fail 已在 PR #240 修复)
21+
- Xiaomi MiMo (Token Plan) round 8:用户反馈基本无问题
22+
- DeepSeek V4 Pro round 9:9 个 apply_patch 调用 / 9 success / **100%** / reasoning 零 self-correction
23+
24+
模型在新 prompt 下**主动**`apply_patch` 工具,几乎不再走 `cat <<EOF > file` / `printf > file` / `echo > file` 等 shell file-write 路径。
25+
26+
并行尝试的 **PR #239 server-side normalize 兜底方案已 CLOSED**(2026-05-22),原因:
27+
- round 3 真机数据显示 retry 风暴(72.7% fail rate)
28+
- detector 边界模糊(`>>` / `sed -i` / `tee` / 变量替换 / 多命令组合等需要 reject 透传)
29+
- prompt-only 方案更优雅,直接消除 shell file-write 来源
30+
31+
## 问题描述
32+
33+
### 现状(已 ship)
34+
35+
模型读 PR #236 + #240 的 prompt guidance → 主动调 `apply_patch` 工具 → adapter wire 桥接 → Codex Desktop 渲染 diff card(行号 / `+N -M` 增减 / 文件清单 / 颜色高亮 / 工具调用抬头 / 可点击文件名 6 个元素全部正常)。
36+
37+
### 假设激活条件(P3 触发信号)
38+
39+
如未来出现以下任一情况,**重新评估**是否复活 PR #239 server-side normalize:
40+
41+
1. 新接入 provider / 模型完全无视 prompt guidance,绕过 `apply_patch` 工具直接 shell `cat > file`(真机数据 ≥3 次用户反馈)
42+
2. Codex CLI / Codex App 升级后 `apply_patch` wire 协议改动,prompt 修复在新协议下失效
43+
3. 真机日志统计显示 `apply_patch` 调用率显著下降而 shell file-write 上升(需要观测 hook,目前无)
44+
45+
### 期望(若激活)
46+
47+
复活 PR #239`crates/adapters/src/responses/shell_to_apply_patch.rs` detector(commit history 仍在 GitHub `pull/239/commits`,branch 已删但 SHA 可访问) + `converter.rs::close_tool_call` 接 detector + 重写 SSE 路径,**作为 prompt-only 失效场景的兜底**,而非主路径。
48+
49+
## 已有调研
50+
51+
- **PR #239 实施代码**(closed):`crates/adapters/src/responses/shell_to_apply_patch.rs` 含 21 单测覆盖 `cat <<EOF >` / `printf >` / `echo >` 三种 happy + `>>` / `sed -i` / `tee` / 多命令 / 变量替换等 reject case
52+
- **Antigravity Go agent 借鉴**(`~/.local/bin/agy`):`FILE_CHANGE_TYPE_EDIT` / `file_diff` / `trajectory_file_diffs` token 印证 server-side 统一 diff 事件模型
53+
- **round 3 真机数据**(`~/.codex-app-transfer/logs/apply-patch-debug/round3/`):PR #239 实测 retry 风暴根因 — converter 未及时关闭 stream + detector 边界含糊
54+
55+
## 风险 / 不确定性
56+
57+
- **retry 风暴重现**:PR #239 当时已修复 `args_acc` sync / bare `$VAR` reject / interrupted incomplete 三 BUG(pre-push 3-agent review 捕获),但实际复活时还需重 review converter 跟 normalize 路径的交互
58+
- **Edit / append 仍不支持**:PR #239 MVP 只覆盖 Add File / overwrite,Edit / `>>` / `sed -i` in-place 需要 old file content(adapter 无法直接读),需要 IDE hook 或上下文缓存
59+
- **底层假设变了**:如果 Codex CLI 协议升级,detector 的输入输出形态可能不再适用
60+
61+
## 建议方向(若激活)
62+
63+
1. 重新评估 prompt-only 失效是真是假(可能是新增 prompt 微修就能修,不必动 adapter 层)
64+
2. 若必须 server-side 兜底,**从 PR #239 closed branch cherry-pick** 而非重新实现:
65+
- GitHub 仍保留 commits:`gh pr view 239 --json commits --jq '.commits[].oid'`
66+
- `git fetch origin pull/239/head:revive-239` 拉取 closed PR 的 commits
67+
3. 真机验证(round N+)对照 round 3 看 retry 风暴是否仍存在
68+
4. scope 收紧到**只兜底无法用 prompt 修复的场景**,主路径仍是 prompt
69+
70+
## 关联资源
71+
72+
- **主 PR 链**:PR #236 (merged) + PR #240 (merged) — issue #235 ship 路径
73+
- **PR #239 (closed)**:`https://github.com/Cmochance/codex-app-transfer/pull/239` — server-side normalize 实施代码 + close 时的评估评论
74+
- **真机数据归档**:`~/.codex-app-transfer/logs/apply-patch-debug/round{1..9}/` 246MB
75+
- **上游借鉴**:[`Antigravity Go agent`](file:///Users/alysechen/.local/bin/agy) strings dump
76+
- **关联 followup**:无(独立兜底方案,跟 #32 Plugin Unlock React 重渲无关)

docs/release-notes/v2.1.13.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Codex App Transfer v2.1.13
2+
3+
## 中文
4+
5+
### `apply_patch` diff UI 在 chat-completions provider 上工作
6+
7+
修复 Codex App 在 chat-completions provider(DeepSeek / Kimi / MiMo 等)调用 `apply_patch` 不渲染 diff UI 的问题。根因是 `custom_tool_call``function_call` wire schema 不匹配,叠加旧 V4A 教学 prompt 多处误导,patch 被 V4A applier 报 `Failed to find context``empty hunk`
8+
9+
本次修复:
10+
11+
- adapter 双向桥接 `custom_tool_call` ↔ chat-completions `function_call` SSE,补 `previous_response_id` 多轮历史回放,模型可见上一轮 `apply_patch` 调用。
12+
- 重写 chat-path V4A 教学 prompt:单端 `@@ <header>`、minimal Update form、Add File 全 `+` 前缀、`*** Begin Patch` literal 第一行、Update + Move ≥1 hunk(纯重命名用 Delete + Add)、强 normative 禁 shell `>` redirect 绕过(大改也用 Delete + Add)。
13+
- envelope `output[]` interrupted `apply_patch` status 跟流式 done event 一致(都 `incomplete`),严格客户端不再误执行 partial patch。
14+
- chat-path system guidance 仅 first turn 注入,后续 turn 由历史回放带回,消除每 turn 累积 ~2KB 上下文浪费。
15+
- README 加测试覆盖说明、API key 征集、macOS 未签名提示。
16+
17+
### 验证
18+
19+
- `cargo fmt --check`
20+
- `cargo test -p codex-app-transfer-adapters --lib`(510 tests pass)
21+
- Kimi For Coding 14 次 `apply_patch` 调用 12 success
22+
- Xiaomi MiMo (Token Plan) 用户反馈无明显问题
23+
- DeepSeek V4 Pro 9 次 `apply_patch` 调用全 success
24+
25+
## English
26+
27+
### `apply_patch` diff UI now works on chat-completions providers
28+
29+
Fixes the missing diff UI when Codex App calls `apply_patch` on chat-completions providers (DeepSeek / Kimi / MiMo). Two compounding root causes: the `custom_tool_call``function_call` wire schemas did not match, and the old V4A teaching prompt misled models in several places, so patches were rejected with `Failed to find context` or `empty hunk`.
30+
31+
Changes:
32+
33+
- Adapter bridges `custom_tool_call` ↔ chat-completions `function_call` SSE bidirectionally, with `previous_response_id` history replay so the model sees prior `apply_patch` calls.
34+
- Rewrote the chat-path V4A teaching prompt: single-sided `@@ <header>`, minimal Update form, Add File requires `+` on every line, `*** Begin Patch` must be the literal first line, Update + Move needs at least one hunk (use Delete + Add for pure rename), strong normative ban on shell `>` redirect bypass (use Delete + Add for large rewrites too).
35+
- Envelope `output[]` interrupted `apply_patch` status matches the streaming `done` event (both `incomplete`), so strict clients no longer execute partial patches.
36+
- chat-path system guidance is injected only on the first turn; later turns receive it via history replay, eliminating ~2KB accumulation per turn.
37+
- README adds a test-coverage notice, an API-key-testing contact, and a macOS unsigned-build notice.
38+
39+
### Verification
40+
41+
- `cargo fmt --check`
42+
- `cargo test -p codex-app-transfer-adapters --lib` (510 tests pass)
43+
- Kimi For Coding 14 `apply_patch` calls, 12 success
44+
- Xiaomi MiMo (Token Plan): user reported no apparent issues
45+
- DeepSeek V4 Pro 9 `apply_patch` calls, all success

0 commit comments

Comments
 (0)