fix(issue-fix): re-propose the typed public read failure row (from #4784) - #4928
Conversation
…der text A failed public GitHub read placed the provider's own message, truncated to 180 characters, into the packet's read_error field, so a steward answer could quote that text instead of naming what was unread. Replace it 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, and the dead _compact_error helper goes with it. Scope note: this covers the public GitHub read path. An unread failure from a different source stays open on the same goal row and is not part of this change. Evidence: value-connectors-github-public-probe-smoke ok, with new assertions that the row is typed, that the coverage effect says unread, and that the provider's own message cannot appear in the row; the focused issue-fix suites passed (49 cases); ruff clean; the branch is rebased on latest origin/main. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
b9bdeee to
db11094
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
动机
这条 PR 是 #4784 的原样重新提出(#4784 于 2026-09-20 被关闭且未合并,之后
git grep github_public_read_failure origin/main 仍为空)。
被修的真实缺口在 problem_context 里是可核验的:main 上
loopx/capabilities/issue_fix/github_public.py 的 _compact_error(exc) 只做
" ".join(str(exc).split())[:180],两个 packet builder 把它塞进 read_error,两个 markdown
渲染器再输出 ## Read Error。也就是说,一次失败的公开读取最终以 provider 自己的字符串
(例如 Cache miss)出现在读者面前,读者看不到「哪个源没读到 / 影响什么 / 怎么修」。
交付判定:这是该验收行(todo_f417292acebb:re-propose or retire)要求的「重新提出」这一半,
而不是整条证据源健康工作的完成 —— 同一行里另一个源(Lark im 的 invalid_arguments)不在
本 PR 范围,PR 内已显式声明边界。
改动思路
入口是 loopx/capabilities/value_connectors/cli.py 调用的两个 packet builder
(build_github_public_channel_probe_packet / build_github_public_reply_monitor_packet),
它们各自 try 一次公开读取,失败时原先记录 provider 文本。
决策边界放在新函数 github_public_read_failure_row(exc):把「失败」从一段不可引用的
provider 文本,变成一条 typed row —— source_id + 6 个 reason code 之一 +
coverage_effect(本次调用未读到该目标,而不是「该目标没有活动」)+ next_action
(用 gh/已认证工具重试或显式传入元数据)+ provider_message_digest。
与既有实现的关系是复用而非另起一层:packet 的 read_error 字段原位改名为 read_failure,
两个渲染器原位把 ## Read Error 换成 ## Unread Source,_compact_error 唯一的两处调用
被替换后直接删除。仓库内没有第二个消费 read_error 的读者(manager_context 里的同名变量
是它自己的 receipt 读取结果,与 packet 字段无关),渲染器也只有
value_connectors/cli.py 的四个调用点,都在新构建的 packet 上。
具体改动
关键代码讲解
github_public_read_failure_row(exc)(新增)—— 这是本 PR 的行为核心。分类顺序先
subprocess.TimeoutExpired/TimeoutError→provider_timeout,再HTTPError→
provider_response_rejected(并把exc.code作为provider_status带上),
再URLError→provider_network_unavailable。这里是正确的:HTTPError是URLError
的子类,如果顺序反过来,401/404 这类「provider 明确拒绝」会被误报成网络不可用。
之后JSONDecodeError→provider_result_unreadable、RuntimeError→
provider_tool_unavailable,其余落到provider_read_failed,所以没有任何异常类型会
落回「把原文讲出去」。provider_message_digest = "sha256:" + sha256(compact)—— 保留与 provider 日志的相关性,
同时让 answer 无法引用原文。这是把「可相关」与「可引用」分开的关键点。build_github_public_channel_probe_packet/build_github_public_reply_monitor_packet——
read_error: str | None→read_failure: dict | None,validation_errors的触发条件
跟着改名,语义不变(仍然是「metadata read failed; retry...」)。render_github_public_reply_monitor_markdown/render_github_public_channel_probe_markdown
—— 只在read_failure是Mapping时输出## Unread Source区块,列出
source_id / code / coverage_effect / next_action / provider_message_digest;
非 Mapping 的脏值不会让渲染炸掉。_compact_error删除 —— 没有残留调用点,避免了「新 typed 行与旧原文路径并存」的半迁移状态。
对主干的风险
- 主要风险是 packet 字段改名(
read_error→read_failure),而两个 packet 的
schema_version字符串仍是..._v0:仓库内没有第二个读者,但如果仓库外有人解析
这两个 packet 或保存了旧 packet 再交给新渲染器,## Read Error区块会静默消失。这是
低影响、可接受的(渲染器只在新构建的 packet 上被调用),但属于一次未在版本号上明示的
字段级变更,建议维护者知悉;最小修补是后续把 packet schema version 提升到v1。 - 失败路径的负向走查:
fetch_metadata=False(no-fetch 模式)时不会触发这条路径;
触发时read_failure一定非空且ok=False,packet 仍然返回,调用方可继续用
--metadata-json,行为与改动前一致。 - 该改动让回答不再泄露 provider 原文,属于证据表达边界的收紧,不改变任何权限、评分或
scheduler 行为。
验证
in steward-typed-read-failure-0923(基线 origin/main e57b49e,head
db11094cca40567b6d328759874852256f14361e):
uv run --extra test python examples/value-connectors-github-public-probe-smoke.py→ ok,
新增断言覆盖「row 是 typed 的」「coverage_effect 说明 unread」「provider 原文不可能出现在 row 里」。tests/capabilities/test_issue_fix_github_encoding.py、
test_issue_fix_monitor_execution.py、test_issue_fix_candidate_evidence.py→ 49 passed。ruff check loopx/capabilities/issue_fix/github_public.py examples/value-connectors-github-public-probe-smoke.py→ clean。- 边界扫描:
rg 'read_error|read_failure' docs/无命中;read_error在 main 上的其余命中
都属于manager_context/TS 的无关同名变量。
结论
APPROVE —— 重新提出的范围与原验收行的要求一致,typed row 取代 provider 原文的核心行为
有 smoke 与正向/负向断言支撑,仓库内无未迁移的读者;唯一需要维护者知悉的是 packet 字段
改名未伴随 schema_version 提升(已作为风险写明,不影响本 PR 合并)。
English verdict: APPROVE - Re-proposes #4784 unchanged and bounded: a failed public GitHub read is now a typed github_public_read_failure_row (source, reason code, coverage effect, repair action, message digest) in both packets and both renderers, with the raw provider text path deleted and no in-repo reader left on the old field; only the un-bumped packet schema_version for that field rename is disclosed as a residual risk.
动机 / Motivation
管家回答里会直接出现 provider 自己的失败原文。GitHub 公开读取失败时,
loopx/capabilities/issue_fix/github_public.py把str(exc)截断到 180 字符放进 packet 的read_error字段(channel probe 与 reply monitor 两处),再由 markdown 渲染成## Read Error。读者看到的是Cache miss这类 provider 文本,而不是「哪个源没读到、覆盖受到什么影响、下一步怎么修」。
这条修复原本在 #4784 提出,于 2026-09-20 被关闭且未合并,之后 main 上
git grep github_public_read_failure仍为空。因此这里按原样重新提出,不夹带新范围。改动 / What Changes
github_public_read_failure_row(exc):一条 typed row,带schema_version、source_id、六个 reason code 之一(按异常类型分类:provider_timeout、provider_network_unavailable、provider_response_rejected(含 provider status)、provider_result_unreadable、provider_tool_unavailable、provider_read_failed兜底)、coverage_effect(目标在这次调用里是 未读到,而不是「没有活动」)、next_action(用 gh/已认证工具重试,或显式传入元数据),以及provider_message_digest(保留与 provider 日志的相关性,但回答无法引用原文)。read_error字段改为read_failure;两个 markdown 渲染器改为输出
## Unread Source区块。_compact_error。证据 / Evidence
examples/value-connectors-github-public-probe-smoke.py通过,并新增断言:该行是 typed 的、coverage_effect说明 unread、Cache miss不可能出现在该行里。tests/capabilities/test_issue_fix_github_encoding.py、test_issue_fix_monitor_execution.py、test_issue_fix_candidate_evidence.py:49 passed。
ruff check干净;git merge-base --is-ancestor origin/main HEAD成立(基线 e57b49e)。边界 / Boundary
control-plane(
loopx/capabilities/issue_fix/**),只改公开 GitHub 读取路径的失败表达;另一位源(Lark im 的
invalid_arguments)仍是同一行里的独立缺口,不在此 PR 范围。合并决定留给维护者。