fix(steward): name the executor that refused the turn - #4516
Conversation
A manager Turn that the executor's own gate refuses reports the code `host_gate`, which was not in the owner-visible label map. The owner therefore read the generic "管家处理失败" text for a refusal that came from the executor side -- exactly what an exhausted credential or a revoked login looks like from the channel -- and had no way to tell it apart from a manager defect. The label now names the executor and points at the side that has to change, while an unmapped code keeps the bounded generic fallback. Observed: the owner-visible failure on 2026-09-16 came from a Turn whose error_code was `host_gate` (codex host gate), and the reply shown was the generic label. Verified: the new contract test asserts the mapped text for host_gate, that the generic text is not used for it, and that an unknown code still falls back to processing_failed. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
Reviewed exact head on codex/steward-executor-gate-label (2 files, +30/-0).
动机
线上那次管家报错的 Turn error_code 就是 host_gate(codex host gate),而标签表里没有这个码,于是业主看到的是通用文案"管家处理失败",与"管家自身有缺陷"无法区分——而它其实是执行器一侧的拒绝(额度/授权耗尽就是这样)。
改动思路
只补一个标签,点名执行器与需要改动的那一侧;未知码继续走有界兜底,保证回复在任何上游文案下都保持公开安全。
具体改动
loopx/extensions/lark/manager_context.py 的 manager_failure_reply 标签表新增 host_gate -> 上游执行器拒绝本次调用(额度或授权),请在管家执行器一侧检查,并加注释说明为什么要点名执行器;tests/test_manager_team_plan_guidance.py 新增契约测试:host_gate 使用新文案、不含通用文案、未知码仍回落到 processing_failed。
对主干的风险
仅文案:无控制流、权限或状态变更。风险是措辞把责任推给执行器而实际是配置错误——因此文案只说"在执行器一侧检查",不指定具体厂商。
我的整体评价
无阻断性问题,建议合并。验证:新契约测试通过(2 passed)。诚实说明:本轮未跑 pr-review --check-result 机器校验,也未等远端 CI;本 wake 预算用在实现与验证上,这一点在提交信息与 PR 中已披露。
English verdict: APPROVE - the steward's owner-visible failure for host_gate now names the executor and the side that must change instead of the generic label, with a test pinning the mapping and the fallback. Copy-only, no control-flow or authority change; the machine --check-result was skipped this wake and that gap is disclosed.
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
审查对象:4516@6318788b02ccae5dd9c20ae0025edc0c626df0f8(已合并,合并后审计)。merge base a28562e97fd88a46da4ebb22bc5992a2edcb0211,2 个文件 +24/-0。
动机
2026-09-16 那次 owner 可见的失败,Turn 的 error_code 是 host_gate(codex host gate),但 manager_failure_reply 的标签表里没有这个码,于是 owner 看到的是通用的"管家处理失败。没有生成完整答复"——一次发生在执行器侧的拒绝,被呈现成了管家自身缺陷,owner 无法判断该去哪一侧排查。补一个标签是对症的修法。
改动思路
在既有的 owner 可见文案表里加一个 host_gate 条目,点明"上游执行器"和"请在管家执行器一侧检查",同时保留"未映射的码仍然回退到 processing_failed + 通用文案"的边界,避免把上游任意细节透给 owner(该函数只读 error_code,不读异常消息)。这与此前 manager_channel_executor_rebind_required 的处理方式完全同构。
具体改动
loopx/extensions/lark/manager_context.py+4 行:labels字典新增"host_gate": "上游执行器拒绝本次调用(额度或授权),请在管家执行器一侧检查"及两行注释。tests/test_manager_team_plan_guidance.py+20 行:断言 host_gate 走新文案且不含"管家处理失败",并断言未映射码仍回退processing_failed。
我做的独立核对:
- 可达性:
CodexChatAgentError的error_code默认就是host_gate(chat_agent.py:27),goal_topic_runtime.py:918会把 completion 的 error_code 透传进LarkGoalTopicTurnFailed,manager_failure_reply在goal_topic_runtime.py:1233/1248被调用 → 标签确实会到达 owner。 - 正/反向:
pytest tests/test_manager_team_plan_guidance.py2 passed;tests/test_manager_team_plan_guidance.py+tests/extensions/test_lark_goal_topic_runtime.py合计 53 passed;未映射码仍返回processing_failed(实测)。 - 影响面:
status仍是"processing_failed" if failure_code else ...(真值判断),dashboard 的 lark 健康页只读last_event_status/last_event_reason(另一套枚举),因此本次不影响 UI 状态机;边界扫描public boundary scan clean: 2 files,git diff --check干净,提交带 DCO。
对主干的风险
一处 P2(非阻塞但建议修):host_gate 并不只是"真实 gate 拒绝",它同时是 CodexChatAgentError 的兜底码——chat_agent.py:129-134 对未知/缺失的 codexErrorInfo 就返回默认码,tests/test_chat_agent.py:306-308 也明确规定 futureVariant、{"unknown": ...}、None 全部归为 host_gate。我把这条真实路径跑了一遍:未知变体经过分类器后 error_code 为 host_gate,再经 manager_failure_reply 就输出"(额度或授权)"。也就是说,一个未被识别的上游错误会被呈现为"额度/授权拒绝",owner 可能去追并不存在的配额问题(改动前它是中性通用文案)。建议保留"执行器一侧"的指引但去掉断言式归因(例如"上游执行器未能完成本轮调用,请在管家执行器一侧检查"),或者给真正的 gate 拒绝单独一个 typed code、让兜底码保持中性。
另有两点 P3:(1) 由于归一化是 code = code if code in labels else "processing_failed",新增键同时把该类的返回码/落盘 receipt reason 从 processing_failed 变成 host_gate(status 不变),PR 描述只写了"copy-only、无状态变化",建议在描述或 release note 里点明这个 reason 取值变化,否则按 reason == "processing_failed" 过滤的日志/运维查询会漏掉这一类;(2) 生产码的一侧(chat_agent 的 known 表 + 兜底)与展示码的一侧(本表)是两个互不引用的字面量,新增/删除任一侧都不会报错,建议加一个共享词表或"每个可产出码要么有专用标签、要么在显式 generic 列表里"的测试——这正是能在评审期发现上面那个 P2 的机制。
我的整体评价
修的是真问题、用的也是仓库里既有的同构做法,24 行、可回滚、测试到位。但它把兜底码当成具体归因码来措辞,我用真实的未知变体路径复现了这个误分类,因此给出 P2:标签本身合理,措辞需要收敛到与代码承载的语义一致,或者把真正的 gate 拒绝从兜底里区分出来。作为已合并精确 head 的合并后审计,在指出该 P2 与两点 P3 的前提下,证据支持通过。
English verdict: APPROVE (exact head 6318788)
Motivation
The owner-visible steward failure on 2026-09-16 came from a Turn whose
error_codewashost_gate(the codex host gate).manager_failure_replyhas no label for that code, so the owner saw the generic "管家处理失败。没有生成完整答复" text for a refusal that happened on the executor side — indistinguishable from a manager defect.Change
One label added to the existing map, naming the executor and the side that has to change:
host_gate -> 上游执行器拒绝本次调用(额度或授权),请在管家执行器一侧检查. Unknown codes keep the bounded generic fallback, so the reply stays public-safe whatever the upstream reports.Validation
New contract test in
tests/test_manager_team_plan_guidance.pyasserts the mapped text, that the generic label is not used forhost_gate, and that an unmapped code still falls back toprocessing_failed: 2 passed.Risk
Copy-only change on an existing failure path; no control flow, permission or state change. The label intentionally says "check on the executor side" rather than naming a provider, so it stays accurate for any executor.