From 6318788b02ccae5dd9c20ae0025edc0c626df0f8 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:06:03 +0800 Subject: [PATCH] fix(steward): name the executor that refused the turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- loopx/extensions/lark/manager_context.py | 4 ++++ tests/test_manager_team_plan_guidance.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/loopx/extensions/lark/manager_context.py b/loopx/extensions/lark/manager_context.py index f564ac8264..b555989087 100644 --- a/loopx/extensions/lark/manager_context.py +++ b/loopx/extensions/lark/manager_context.py @@ -46,6 +46,10 @@ def manager_failure_reply(error: Exception) -> tuple[str, str]: "hard_timeout": "处理超过时间限制", "interrupted": "处理已中断", "manager_authorization_unavailable": "当前连接的授权范围不可用", + # The executor's own gate refused the call, which is what an exhausted + # credential or a revoked login looks like from the channel. Naming the + # executor keeps the owner from reading it as a manager defect. + "host_gate": "上游执行器拒绝本次调用(额度或授权),请在管家执行器一侧检查", "manager_channel_executor_rebind_required": ( "管家的执行器已由本机设置更改,需要重新应用一次管家连接" ), diff --git a/tests/test_manager_team_plan_guidance.py b/tests/test_manager_team_plan_guidance.py index 9dd6c01e3b..c12ae7b817 100644 --- a/tests/test_manager_team_plan_guidance.py +++ b/tests/test_manager_team_plan_guidance.py @@ -28,3 +28,23 @@ def test_manager_guidance_orders_one_team_preview_before_any_effect() -> None: # An unstaffable lane is named as a gap rather than invented. assert "as a gap, with the missing registration or grant" in text assert "inventing a lane, an Agent, or a capability" in text + + +def test_the_owner_visible_failure_names_the_executor_that_refused() -> None: + """A host gate is the executor's refusal, not a defect in the manager.""" + + from loopx.extensions.lark.manager_context import manager_failure_reply + + class _Refused(RuntimeError): + error_code = "host_gate" + + code, text = manager_failure_reply(_Refused("upstream refused")) + + assert code == "host_gate" + assert "上游执行器" in text + assert "管家处理失败" not in text + # An unmapped code still falls back to the bounded generic label. + class _Unknown(RuntimeError): + error_code = "some_future_code" + + assert manager_failure_reply(_Unknown("x"))[0] == "processing_failed"