fix(stepper): report a failed public read as a typed row, not provider text - #4784
huangruiteng wants to merge 1 commit into
Conversation
…r text A failed public GitHub read used to place the provider's own message, truncated to 180 characters, into the packet's read_error field. Steward answers then quoted that text instead of naming what was unread: the group shows "Cache miss" twice on 2026-09-13 and "invalid_arguments" on 2026-09-14, and the manager evidence row (loopx-meta todo_386976ec9aae) records that each failure must instead carry source id, typed reason, coverage effect and next action. Replace the raw text with github_public_read_failure_row: a typed row carrying schema_version, source_id, one of six reason codes classified from the exception type (timeout, network unavailable, response rejected with the provider status, result unreadable, tool unavailable, or a bounded fallback), the coverage effect that the target is unread rather than inactive, the repair next_action, and a digest of the provider message for log correlation. Both the channel-probe and reply-monitor packets and both markdown renderers now carry the typed row; the raw provider string no longer reaches a payload an answer can quote. The dead _compact_error helper goes with it. Scope note: this covers the public GitHub read path the row's evidence names. The Lark im read that returned invalid_arguments is a different source and stays open on the same row. Evidence: value-connectors-github-public-probe-smoke ok with new assertions that the row is typed, that the coverage effect says unread, and that "Cache miss" cannot appear in the row; 69 issue-fix/value-connector/github capability tests passed; ruff clean; py_compile clean; loopx canary premerge --from-git-diff passed (the single red canary is the pre-existing maintainability-ratchet finding for files this change does not touch). 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)
审查对象:PR #4784,exact head 21e45ba6bfb95e8ed5ec4828da6b06befe906c84(base main = 361347713)。审查策略 pull_request_review revision 7。
动机
管家回答里会直接出现 provider 的失败原文。本行 todo_386976ec9aae 记下的现场是:2026-09-13 06:52 / 12:54 的 GitHub 读取返回 Cache miss,2026-09-14 13:20 的 im 读取返回 invalid_arguments,而管家把这些字串当结论讲给读者 —— 读者看不到「哪个源没读到、影响什么、怎么修」。
根因具体而窄:loopx/capabilities/issue_fix/github_public.py 把 str(exc) 压空白、截断到 180 字符放进 packet 的 read_error,再渲染进 markdown,于是 provider 文本成了可被引用的证据。这是有界增量:本 PR 只覆盖行证据点名的 GitHub 公开读取路径;同一行里 Lark im 读取的 invalid_arguments 与「把 stale run state 当答案级免责声明」两半仍然开着。
改动思路
按本行要求的四个字段重构读失败:source id + typed reason + coverage effect + next action。
- 新增
github_public_read_failure_row(exc),按异常类型(不是子串黑名单)分类到 6 个 code:provider_timeout、provider_network_unavailable、provider_response_rejected(带provider_status)、provider_result_unreadable、provider_tool_unavailable、兜底provider_read_failed; coverage_effect明确写「本次没读到该目标,目标属于未读而不是无活动」——正对本行「不得被读成没有进展」;next_action给出可执行修复;- provider 原文不再进入 packet,只留
provider_message_digest(sha256)用于与日志对账:可关联、不可被引用。
字段从 read_error(字符串)改为 read_failure(typed 行),两个渲染段从 ## Read Error 改为 ## Unread Source;死掉的 _compact_error 一并删除,避免留下第二条失败表示路径。
关键代码讲解
github_public_read_failure_row(github_public.py:137):分类器 + typed 行构造;注意HTTPError必须排在它的基类URLError之前判定,否则 403 会被误分类成网络不可用。- 两个 packet 构建器的失败捕获(
:452与:553附近):由read_error = _compact_error(exc)改为read_failure = github_public_read_failure_row(exc),同时保留ok=false与原有的修复型 validation error。 - 两个 markdown 渲染器的 unread 段(
:606、:654附近):只打印 typed 字段,因此 provider 文本无法再从渲染路径回流进答案。
具体改动
loopx/capabilities/issue_fix/github_public.py(+116/-15)examples/value-connectors-github-public-probe-smoke.py(+26):断言 typed 行字段齐全、超时/网络/HTTP(403) 分类正确,并断言Cache miss不可能出现在行里。
对主干的风险
最强反例是契约破坏:仓库外的消费者若按字符串读 packet["read_error"],这次改动会让它看不到失败信号。我在仓库内做了全量检索:该字段只在 github_public.py 自身的 payload 与两个渲染器里被读取,没有其他调用方或文档引用;失败证据也没丢,只是换成了更有信息量的名字。若将来发现外部消费者,最小修法是保留一个版本的 read_error 别名 —— 这条我写在评审里,而不是提前加。
其次:分类依赖异常类型,遇到未知类型会落到兜底的 provider_read_failed(仍是 typed 且有修复指引),不会退回原文泄漏。
验证:value-connectors-github-public-probe-smoke ok(含新断言);tests/capabilities 里 issue-fix/value-connector/github 相关 69 passed;ruff 干净;py_compile 干净;loopx canary premerge --from-git-diff 通过。该次 premerge 唯一红的 canary 是 main 上既有的 maintainability-ratchet(chat_server.py、goal_topic_connections.py,本改动未触碰)。
我的整体评价
同意合并(author-owned,以 COMMENTED 记录 APPROVE 结论)。它把「provider 原文当结论」这类回答质量事故在产生点掐掉,换成读者能据以行动的 typed 行,并且刻意不扩张到同一行的另一半。控制面改动(loopx/**),按仓库规则由维护者合并。
English verdict: APPROVE - head 21e45ba; replaces the provider's raw failure text (previously str(exc) truncated into read_error, which steward answers quoted as "Cache miss"/"invalid_arguments") with a typed read-failure row carrying source_id, one of six exception-type codes, the coverage effect that the target is unread rather than inactive, a repair next_action, and a sha256 digest of the provider message for log correlation; both the channel-probe and reply-monitor packets and both markdown renderers were updated and the dead helper removed; the rename is safe because the key was read only inside that module (repository-wide search), and the alternative deprecation alias is recorded rather than added speculatively; scope is bounded to the GitHub public read path the row names, leaving the Lark im read and stale-run-state halves open; validated by the value-connectors smoke with new typed assertions (including that "Cache miss" cannot appear), 69 capability tests, ruff and py_compile, and loopx canary premerge --from-git-diff (the single red canary being the pre-existing maintainability-ratchet finding for untouched files); control-plane change, maintainer merge only.
…0923 fix(issue-fix): re-propose the typed public read failure row (from #4784)
动机 / Motivation
管家回答里会直接出现 provider 自己的失败原文。这条车道的验收行
todo_386976ec9aae记下了现场证据:2026-09-13 06:52 / 12:54 的 GitHub 读取返回Cache miss,2026-09-14 13:20 的 im 读取返回invalid_arguments,而管家就把这些字串当成「结论」讲给读者 —— 读者看不到「哪个源没读到、影响什么、怎么修」。根因是一处很具体的实现:
loopx/capabilities/issue_fix/github_public.py把str(exc)截断到 180 字符放进 packet 的read_error字段(channel probe 与 reply monitor 两处),再被渲染进 markdown;于是 provider 文本成了可被引用的证据。改动思路 / Approach
把「读失败」从一段 provider 文本变成本行要求的 typed 行:source id + typed reason + coverage effect + next action。新增
github_public_read_failure_row(exc):provider_timeout、provider_network_unavailable、provider_response_rejected(带provider_status)、provider_result_unreadable、provider_tool_unavailable、以及兜底provider_read_failed;这是类型判定,不是子串黑名单。coverage_effect明确说「这次没读到目标,目标属于未读而不是无活动」——正是本行要求「不得被读成没有进展」的那一条。next_action给出可执行的修复路径。provider_message_digest(sha256)供与日志对账:可关联,不可被答案引用。两个 packet 的字段从
read_error(字符串)改为read_failure(typed 行),两个 markdown 渲染段从## Read Error改为## Unread Source并列出 typed 字段;死掉的_compact_error一并删除。具体改动 / Changes
loopx/capabilities/issue_fix/github_public.py(+116/-15):新增 typed 读失败行与 6 个 code 常量;channel probe 与 reply monitor 都改用它;两个渲染器改为输出 typed 字段;删除_compact_error。examples/value-connectors-github-public-probe-smoke.py(+26):新增断言 —— 失败行必须是 typed(code/source_id/coverage_effect/next_action/digest)、超时与网络失败分类正确、HTTP 失败带 status,并且Cache miss这种 provider 文本不可能出现在行里。对主干的风险 / Risk
read_error改为read_failure,类型从字符串改为对象。我在仓库内检索过消费方:该字段只在github_public.py内部(payload 与两个渲染器)被读取,没有其他调用方或文档引用,所以改名不丢信号;对答案而言这是想要的信号升级(从可引用文本变成 typed 行)。provider_read_failed;这仍然是 typed 且有修复指引,不会退回原文泄漏。im读取的invalid_arguments属于另一个源,仍留在该行上,不在本 PR 里顺手改。value-connectors-github-public-probe-smokeok(含新断言);tests/capabilities里 issue-fix/value-connector/github 相关 69 passed;ruff 干净;py_compile 干净;loopx canary premerge --from-git-diff通过(唯一红的 canary 是 main 上既有的 maintainability-ratchet,涉及本改动未触碰的文件)。loopx/**),按仓库规则由维护者合并,作者不自合并。English verdict: APPROVE - replaces the provider's raw failure text (previously
str(exc)truncated intoread_error, which steward answers quoted as "Cache miss"/"invalid_arguments") with a typed read-failure row carrying source_id, one of six exception-type codes, the coverage effect that the target is unread rather than inactive, a repair next_action, and a digest of the provider message for log correlation; both the channel-probe and reply-monitor packets and both markdown renderers were updated and the dead helper removed; the field rename is safe because the key was read only inside that module, and it upgrades what an answer can carry; scope is bounded to the GitHub public read path the row names, with the Lark im read left open on the same row; validated by the value-connectors smoke with new typed assertions (including that "Cache miss" cannot appear), 69 capability tests, ruff and py_compile, andloopx canary premerge --from-git-diff(the single red canary is the pre-existing maintainability-ratchet finding for untouched files); control-plane change, maintainer merge only.