跳转至

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):

  1. EXIT_RE = r"exit\s*code\s*[:=]?\s*(\d+)" 匹配不到 Trae 的格式 —— 实测近 200 步 code 全为 None(连成功步也没有),说明 ① is_error / ② 退出码两条路径从未命中,ok 全部由 ③ 文本扫标记得出;
  2. ERR_ANYWHERE 里含 "permission denied" —— 只读工具扫受限文件必然回吐该串 → 误判;
  3. Hook 拦截造成的 ok=False 未在统计层剔除;
  4. 读到的文件正文被当"本工具的错误文本"(02:45:04 读 proxy.py,正文含 exit code: 1 → ok=False, code=1);
  5. 写类工具的结果回显 diff,同样误判(02:50:24 / 02:51:23 / 02:51:56 三处 Write / SearchReplace,正文正是当轮写进去的 exit code: 1 注释);
  6. 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 同步,请勿在站点侧修改)