SOP — 返工信号修正(proxy ok 判定的六处缺陷)¶
主题目录:
token_proxy/retry-signal-fix-20261009/| 执行日期:2026-10-09 | 类型:C 类(改实现体 + 重启服务) 分三批执行:第一批①②③、第二批④、第三批⑤⑥ —— 每批均由用户在复核实测数据后追加批准。
一、背景¶
收尾自查里的「返工信号:报错步 / 重试步」由 wrapup.py 读 req_shape.jsonl 的 shape.last_tool(proxy.py 落)。用户要求先核实真实性再定方案。
核实结论(现场数据):本会话 6 个「报错步」没有一个是真失败 ——
| 时间 | 记的失败工具 | 实况 |
|---|---|---|
| 02:25:30 / 02:29:32 | Grep | rg 读 600 文件报 Permission denied,但正常返回了结果 |
| 02:28:49 | RunCommand | merge-guard 的 deny(guard 日志可查) |
| 02:31:19 | Read | 读前钩子拦截后原样重发 |
| 02:31:59 | Write | merge-guard deny,随后有 allow_fp(原样重发即放行) |
| 02:32:14 | RunCommand | 无退出码、纯文本命中 |
机制根因(proxy.py _last_tool_result 与 wrapup.py tool_error_stats):
EXIT_RE = r"exit\s*code\s*[:=]?\s*(\d+)"匹配不到 Trae 的格式 —— 实测近 200 步code全为None(连成功步也没有),说明 ①is_error/ ② 退出码两条路径从未命中,ok全部由 ③ 文本扫标记得出;ERR_ANYWHERE里含"permission denied"—— 只读工具扫受限文件必然回吐该串 → 误判;- Hook 拦截造成的
ok=False未在统计层剔除; - 读到的文件正文被当"本工具的错误文本"(02:45:04 读
proxy.py,正文含exit code: 1→ok=False, code=1); - 写类工具的结果回显 diff,同样误判(02:50:24 / 02:51:23 / 02:51:56 三处
Write/SearchReplace,正文正是当轮写进去的exit code: 1注释); - guard 与 shape 的工具名不同源:guard 记
edit、shape 记SearchReplace→ ③ 的同名匹配失效。
二、目标¶
| 项 | 目标态 |
|---|---|
| ① 退出码落值 | 命令步 code 为真实退出码(bash -c 'exit 7' → code=7) |
| ② 权限噪声不误判 | 含 Permission denied 文本的结果不再判 ok=False |
| ③ Hook 拦截不算返工 | 与 guard_events.jsonl 同工具、±150 s 内的 deny 步不计入「报错步」 |
| ④ 只读类不做文本判定 | (第二批,后被 ⑤ 取代)Read / Grep / Glob / LS 只认显式 is_error |
| ⑤ 仅命令类做文本判定 | 只有 COMMAND_TOOLS(RunCommand 等)看退出码 / 错误标记;其余工具一律只认显式 is_error,否则 ok=None |
| ⑥ deny 工具名归一 | SearchReplace → edit 等别名折叠后比较,否则 ③ 形同虚设 |
三、前置条件¶
- 可写
proxy.py/cost_audit/wrapup.py;systemctl --user可用; - 改动前先备份;改
proxy.py前后跑离线回归(本主题offline_verify.py)。
四、影响范围¶
| 对象 | 改动 | 是否需停服 |
|---|---|---|
proxy.py |
① EXIT_RE 放宽;② ERR_ANYWHERE 去掉 permission denied;⑤ 新增 COMMAND_TOOLS 分流;docstring 同步 |
需重启 token-proxy.service(数秒) |
cost_audit/wrapup.py |
③ tool_error_stats 增 deny 剔除;⑥ 新增 _TOOL_CANON / _canon_tool |
不需要 |
不改数据;req_shape.jsonl 仅影响此后新写入的行。
五、分步操作¶
步骤 1:放宽退出码正则(proxy.py)¶
Trae 的退出码文本是 command_exit_code>7<(下划线),原 exit\s*code 匹配不到。
grep -n "EXIT_RE" proxy.py # 旧值:exit\s*code\s*[:=]?\s*(\d+)
EXIT_RE = re.compile(r"(?:command_)?exit[_\s]*(?:code|status)[\s:=<>]*(\d+)", re.I)
步骤 2:收窄错误标记表(proxy.py)¶
去掉 "permission denied"(只读工具扫 600 文件必然回吐,但工具仍正常返回)。
步骤 3:剔除 Hook 拦截步(cost_audit/wrapup.py)¶
新增 DENY_WINDOW_SECONDS = 150 与 _guard_denies() / _near_deny()。
步骤 4:只读类不做文本判定(第二批;后由步骤 5 取代)¶
READ_ONLY_TOOLS = {"read","grep","glob","ls","searchcodebase"}。
步骤 5:仅命令类做文本判定(第三批,取代步骤 4)¶
实测步骤 4 不足:写类工具的结果会回显 diff,正文含 exit code: 1 时同样误判。故改为命令类白名单:
COMMAND_TOOLS = {"runcommand", "bash", "shell"}
...
is_cmd = (name or "").lower() in COMMAND_TOOLS
match = EXIT_RE.search(text)
code = int(match.group(1)) if (match and is_cmd) else None
if is_error is not None:
ok = not is_error
elif not is_cmd:
ok = None # 非命令类:正文不可当失败证据
elif code is not None:
ok = code == 0
else:
ok = None if not text.strip() else not any(h in text.lower() for h in ERR_ANYWHERE)
步骤 6:deny 工具名归一(第三批,cost_audit/wrapup.py)¶
guard 记 edit、shape 记 SearchReplace,不归一则 ③ 的剔除失效:
_TOOL_CANON = {"searchreplace": "edit", "editfile": "edit", "applypatch": "edit",
"replacestring": "edit", "createfile": "write", "writefile": "write"}
def _canon_tool(name):
key = str(name or "").lower()
return _TOOL_CANON.get(key, key)
_guard_denies 与 _near_deny 双侧都过 _canon_tool。
步骤 7:离线回归 + 重启¶
cd /home/rain/Documents/services/token_proxy
python3 -m py_compile proxy.py cost_audit/wrapup.py
python3 retry-signal-fix-20261009/offline_verify.py # 预期 13/13 通过
systemctl --user restart token-proxy.service
systemctl --user is-active token-proxy.service # 预期 active
坑:重启清空进程内存态一次;同一批多处改动攒批只重启一次。
六、验证(2026-10-09 实测)¶
| 项 | 命令 / 方式 | 结果 |
|---|---|---|
| 离线回归 | python3 retry-signal-fix-20261009/offline_verify.py |
13/13 通过(A1–A9 判定优先级 + B1–B4 工具名归一)✅ |
| 编译 | python3 -m py_compile proxy.py cost_audit/wrapup.py |
通过 |
| ① 失败退出码 | bash -c 'exit 7' / 'exit 5' 后查 req_shape.jsonl |
ok=False, code=7 / code=5 ✅ |
| ① 成功退出码 | 普通命令后查 | code=0, ok=True ✅ |
| ② 权限噪声 | 对含 600 文件的目录 Grep | ok=True(旧版 False)✅ |
| ③+⑥ deny 剔除 | 离线用例 B3(SearchReplace 命中 edit deny) | True ✅ |
| ⑤ 读类正文误判 | 读 proxy.py(正文含 exit code: 1) |
ok=None, code=None(旧版 ok=False,code=1)✅ |
| ⑤ 命令类仍判 | 同一轮内命令步 | code=0/5 正常 ✅ |
| 服务 | systemctl --user is-active |
active(三批各重启一次)✅ |
七、回滚¶
cd /home/rain/Documents/services/token_proxy
cp -a retry-signal-fix-20261009/backup/proxy.py.bak.20261009-025407 proxy.py # 退回「第四处」状态
cp -a retry-signal-fix-20261009/backup/proxy.py.bak.20261009-024505 proxy.py # 退回「改动前」状态
cp -a retry-signal-fix-20261009/backup/wrapup.py.bak.20261009-025407 cost_audit/wrapup.py
systemctl --user restart token-proxy.service
回滚后验证:bash -c 'exit 7' 的下一步 code 应回到 None。备份台账见 backup/README.md。
八、风险说明¶
| 风险 | 缓解 |
|---|---|
| 命令类白名单过窄(若换客户端、出现别家命令工具) | 未列入者行为=保守 ok=None(不误报),不会产生假失败;新工具出现时补一行即可 |
| deny 窗口 150 s 误吞真实失败 | 窗口按实测(同次拦截相差 0~40 s)取 3.7 倍余量,且要求工具名归一后相同 |
| 重启清空内存态 | 已验证重启不断流;每批攒批一次(共三次) |
九、后续观察(不做动作)¶
六处改完后 报错步 / 重试步 才第一次是可信口径(本轮实测:除我故意跑的失败命令外,0 误判)。建议观察 2~3 轮;确认有稳定「真返工」后,再决定是否落账(判据候选:同一工具连续失败 ≥2 次)。本次未落账。
附录:离线回归与红线的差异说明¶
token_proxy 红线要求改 proxy.py 前跑「/tmp 假上游 + 临时端口」离线验证。本次改动落在纯函数(_last_tool_result / tool_error_stats),故用函数级离线回归(offline_verify.py,13 用例含反例,不 import 服务、不碰线上 logs/)+ 实机受控验证(①②⑤各一条)代替;未搭假上游端到端。差异经用户知悉。
来源:
/home/rain/Documents/services/token_proxy/retry-signal-fix-20261009/SOP.md| 最后更新:2026-10-09 02:55(由build/sync.py同步,请勿在站点侧修改)