来源:round-64 lane D 只读探索(#2241),锚点在 42ba064c 逐行核实。主机侧已抽验复现并修正了 1 处原报告证据不准(见切片 3 注)。
主题:注释/文档做出的收敛声明被活代码推翻。这类注释读者信任度最高,成本不是「读不懂」而是「照着做无处可放」或「以为还有一份旧实现需要对照」。
切片 1:pkg/safego 声称两台服务已全部收敛,实际仍有 4 处手写 recover(3 处无 stack、无 metric)
pkg/safego/safego.go:18-21
// ... so those panics were invisible to every dashboard while Hub's
// equivalent ones were counted. That copy is gone (#2154); both servers
// now launch through here and both install an observer
被这 4 处推翻:
| 位置 |
形态 |
edge-server/internal/events/bus.go:441-447 |
手写 recover,slog.Error("event bus observer panic", "panic", recovered),无 stack、无 metric、无 observer |
hub-server/internal/ws/fanout.go:226-231 |
同上(ws PushToSession panic recovered) |
edge-server/internal/adapters/parser_ndjson.go:56-63 |
同上(ndjson: panic in parseLine) |
hub-server/internal/bus/bus.go:138-145 |
第 4 种写法:自己 metrics.EventBusPanics.Inc() + 自己记 stack,不走 safego observer |
复现的正是文档自己描述的那个故障模式(edge 侧 panic 无 stack、无计数 → 对 dashboard 不可见)。4 处都能用现成的 defer safego.Recover("name")(该 API 就是为「非 go 语句、直接在闭包里 defer」设计,仓库内已有 30+ 处这么用)⇒ 不是「做不到」,是「约定没被门禁保住」。
修法:4 处替换为 defer safego.Recover("<stable.name>");bus.go 需要的专属计数用已有的 SetPanicObserver 按 name 分派(observer 签名已带 name string,见 safego.go:39-50),不要保留私有计数器;把 safego.go:20-21 表述改准确,或加一条测试断言白名单外不存在裸 recover()。
(middleware/recovery.go、middleware/timeout.go、httpserver/server_middleware.go、handler/ws.go 的 recover 是 HTTP 请求面兜底,属正当,不计入。)写集 ≈5 文件 / 30 行。
切片 2:resputil 自称 edge「唯一 JSON response writer」,但 pkg/errcode.WriteJSON 是第三个 writer,且它吞掉编码错误
edge-server/internal/resputil/resputil.go:1-8
// Package resputil holds the single JSON response writer for the Edge server's
// inbound transports ... This is the converged implementation (#1675) ... so the
// wire behavior cannot diverge again.
edge-server/internal/resputil/resputil.go:28-30
if err := json.NewEncoder(w).Encode(v); err != nil { slog.Error("write json response failed", "error", err) }
pkg/errcode/error.go:141-143
if v != nil { json.NewEncoder(w).Encode(v) } // 返回值被丢弃
edge-server/internal/httpserver/server_middleware.go:24,84
sharederr.WriteJSON(w, http.StatusForbidden/Unauthorized, ...) // edge 实际调用 pkg 那份
声明对 internal/api + internal/mcp 成立(那两处的 writeJSON 确实是 1 行 shim),但对 edge 中间件层不成立——它在任何 transport handler 之前就用另一份实现写 401/403。漂移维度正是声明里点名的 log text:resputil 记编码失败,pkg 版静默丢弃 ⇒ 同一台服务上「响应体写坏了」这件事一半可观测一半不可观测。
修法:二选一——(a) 删 pkg/errcode.WriteJSON,server_middleware.go 两处改用 resputil.WriteJSON(pkg 不该有 HTTP writer);或 (b) 保留但补错误日志并让 resputil.WriteJSON 转发它。同时把 resputil.go:1-8 的 "single" 改成准确范围。写集 ≈2-3 文件 / 15 行。
与 #(edge status 手抄)那条 issue 同属 edge-server 响应面,建议同一个 wave 内串行做,不要并行(会撞 resputil/errcode)。
切片 3:6 处注释点名仓库中不存在的 .go 文件,其中 1 处是「代码该放这里」的指令性注释
| 注释位置 |
点名的文件 |
实测 |
hub-server/internal/service/agentteam/route_helpers.go:14 // Keep orchestration (DB, dispatch, events) in agent_team_routing.go. |
agent_team_routing.go |
不存在(find 命中 0);实际是 route_decision.go / agent_team_run.go |
hub-server/internal/service/agentteam/agent_team.go:14-17 文件清单 |
agent_team_routing.go |
不存在;且清单漏了 agent_team_compete.go/_replay.go/_review.go/assignment_lifecycle.go/audit.go/authz.go |
hub-server/internal/service/deliveryoutbox/doc.go:9-11 |
delivery_outbox.go、delivery_outbox_model.go、delivery_outbox_facade.go |
前两个不存在;第三个存在但在父包 service(hub-server/internal/service/delivery_outbox_facade.go:1),不在本包。本包实际 8 文件:outbox.go entry.go retry.go status.go eligibility.go store.go string.go orchestration.go |
hub-server/internal/service/dispatchsvc/agent_dispatch_ports.go:40 |
delivery_outbox.go / delivery_outbox_model.go |
均不存在 |
edge-server/internal/runnerctx/context_budget.go:5-7 // moved ... into context_budget_message.go and context_budget_compact.go. Zero behavior change — pure move only. |
context_budget_compact.go |
不存在(只有 context_budget_message.go)⇒ 声称的「两文件拆分」实为一文件 |
edge-server/internal/adapters/opencode/opencode_acp.go:4-7 // The existing OpenCodeAdapter (opencode.go, root package) is Phase 1/2 batch mode: ... a 500+ line custom parser ... This adapter replaces that hop |
opencode.go + type OpenCodeAdapter |
文件不存在;该包实际只有 fixture_provider.go aliases.go opencode_acp.go opencode_acp_test.go doc.go |
主机侧修正:原报告称「全仓 grep -rn OpenCodeAdapter 只命中这条注释本身」,实测 4 处命中——除 opencode_acp.go:4 外还有 opencode_acp_test.go:173(注释)、:176(测试函数名 TestOpenCodeAdapterMetadataIsNotEmpty)、:198(注释 "the legacy opencode run --format json OpenCodeAdapter has none")。核心结论不变:没有任何 OpenCodeAdapter 类型/符号定义、没有 opencode.go;但那 3 处测试侧引用说明「legacy 适配器已删」这件事在测试注释里也留了同样的过期指代,修的时候要一并处理(否则改完源码注释、测试注释仍在指向不存在的东西)。
修法:逐条改成当前真实文件名,或直接删掉易腐的文件清单(改为一句职责描述);opencode_acp.go:4-7 把 "existing/replaces" 改成过去式并注明旧 batch 适配器已删除、同步 opencode_acp_test.go:198;deliveryoutbox/doc.go:9-11 换成真实 8 文件的职责表。写集 ≈6-7 文件 / 30 行纯注释,零行为变更、零测试影响。
切片 4:pkg/errcode 两个导出符号是死代码,其中一个是另一函数的 17 行近似副本
pkg/errcode/error.go:119-135 WriteErrorWithTrace — 全仓 0 个调用方(含测试),是 WriteError(:95-117)的逐行复制
pkg/errcode/error.go:152-161 EnvelopeForGin — 唯一调用方是它自己的测试 pkg/errcode/errcode_test.go:168-170;活的那份是 EnvelopeForGinWithTrace(:163-175,被 hub-server/internal/handler/response.go:39 使用)
EnvelopeForGin(无 traceId 字段)留在 pkg/ 导出面上=持续邀请新调用方选那个「丢掉 trace 关联」的劣化版本,而 error.go:99-100 的注释恰恰把 trace 关联当卖点
修法:删 EnvelopeForGin 及其测试;WriteErrorWithTrace 二选一——删掉,或把 WriteError 重写为 WriteErrorWithTrace(w, err, NewTraceID()) 让两者共用一份实现(后者更省事且保住 API)。写集 ≈2 文件 / 25 行删除,风险极低。
验收
- 切片 1:必须有断言/门禁证明「白名单外不存在裸
recover()」,否则这次收敛下次还会漂;edge 侧 panic 必须能记到 stack 与计数
- 切片 2:必须有断言「编码失败会被记录」(两份 writer 行为一致)
- 切片 3:纯注释,
git diff --check 干净 + make test 不受影响即可;改完必须再跑一次 find 证明注释里点名的每个文件都真实存在
- 切片 4:删导出符号前必须再跑一次全仓 grep(含
_test.go)确认 0 调用方
负向约束
- 切片 1 不许把 HTTP 请求面兜底的 recover 也「收敛」掉(那是正当的)
- 切片 3 不许顺手改代码逻辑(纯注释批)
- 切片 4 不许因为「导出即承诺」就保留死代码;
pkg/ 是三 module 共享层,删之前确认无外部 consumer
来源:round-64 lane D 只读探索(#2241),锚点在
42ba064c逐行核实。主机侧已抽验复现并修正了 1 处原报告证据不准(见切片 3 注)。主题:注释/文档做出的收敛声明被活代码推翻。这类注释读者信任度最高,成本不是「读不懂」而是「照着做无处可放」或「以为还有一份旧实现需要对照」。
切片 1:
pkg/safego声称两台服务已全部收敛,实际仍有 4 处手写 recover(3 处无 stack、无 metric)被这 4 处推翻:
edge-server/internal/events/bus.go:441-447slog.Error("event bus observer panic", "panic", recovered),无 stack、无 metric、无 observerhub-server/internal/ws/fanout.go:226-231ws PushToSession panic recovered)edge-server/internal/adapters/parser_ndjson.go:56-63ndjson: panic in parseLine)hub-server/internal/bus/bus.go:138-145metrics.EventBusPanics.Inc()+ 自己记 stack,不走 safego observer复现的正是文档自己描述的那个故障模式(edge 侧 panic 无 stack、无计数 → 对 dashboard 不可见)。4 处都能用现成的
defer safego.Recover("name")(该 API 就是为「非 go 语句、直接在闭包里 defer」设计,仓库内已有 30+ 处这么用)⇒ 不是「做不到」,是「约定没被门禁保住」。修法:4 处替换为
defer safego.Recover("<stable.name>");bus.go需要的专属计数用已有的SetPanicObserver按name分派(observer 签名已带name string,见safego.go:39-50),不要保留私有计数器;把safego.go:20-21表述改准确,或加一条测试断言白名单外不存在裸recover()。(
middleware/recovery.go、middleware/timeout.go、httpserver/server_middleware.go、handler/ws.go的 recover 是 HTTP 请求面兜底,属正当,不计入。)写集 ≈5 文件 / 30 行。切片 2:
resputil自称 edge「唯一 JSON response writer」,但pkg/errcode.WriteJSON是第三个 writer,且它吞掉编码错误声明对
internal/api+internal/mcp成立(那两处的writeJSON确实是 1 行 shim),但对 edge 中间件层不成立——它在任何 transport handler 之前就用另一份实现写 401/403。漂移维度正是声明里点名的 log text:resputil 记编码失败,pkg 版静默丢弃 ⇒ 同一台服务上「响应体写坏了」这件事一半可观测一半不可观测。修法:二选一——(a) 删
pkg/errcode.WriteJSON,server_middleware.go两处改用resputil.WriteJSON(pkg 不该有 HTTP writer);或 (b) 保留但补错误日志并让resputil.WriteJSON转发它。同时把resputil.go:1-8的 "single" 改成准确范围。写集 ≈2-3 文件 / 15 行。切片 3:6 处注释点名仓库中不存在的
.go文件,其中 1 处是「代码该放这里」的指令性注释hub-server/internal/service/agentteam/route_helpers.go:14// Keep orchestration (DB, dispatch, events) in agent_team_routing.go.agent_team_routing.gofind命中 0);实际是route_decision.go/agent_team_run.gohub-server/internal/service/agentteam/agent_team.go:14-17文件清单agent_team_routing.goagent_team_compete.go/_replay.go/_review.go/assignment_lifecycle.go/audit.go/authz.gohub-server/internal/service/deliveryoutbox/doc.go:9-11delivery_outbox.go、delivery_outbox_model.go、delivery_outbox_facade.goservice(hub-server/internal/service/delivery_outbox_facade.go:1),不在本包。本包实际 8 文件:outbox.go entry.go retry.go status.go eligibility.go store.go string.go orchestration.gohub-server/internal/service/dispatchsvc/agent_dispatch_ports.go:40delivery_outbox.go/delivery_outbox_model.goedge-server/internal/runnerctx/context_budget.go:5-7// moved ... into context_budget_message.go and context_budget_compact.go. Zero behavior change — pure move only.context_budget_compact.gocontext_budget_message.go)⇒ 声称的「两文件拆分」实为一文件edge-server/internal/adapters/opencode/opencode_acp.go:4-7// The existing OpenCodeAdapter (opencode.go, root package) is Phase 1/2 batch mode: ... a 500+ line custom parser ... This adapter replaces that hopopencode.go+ typeOpenCodeAdapterfixture_provider.go aliases.go opencode_acp.go opencode_acp_test.go doc.go修法:逐条改成当前真实文件名,或直接删掉易腐的文件清单(改为一句职责描述);
opencode_acp.go:4-7把 "existing/replaces" 改成过去式并注明旧 batch 适配器已删除、同步opencode_acp_test.go:198;deliveryoutbox/doc.go:9-11换成真实 8 文件的职责表。写集 ≈6-7 文件 / 30 行纯注释,零行为变更、零测试影响。切片 4:
pkg/errcode两个导出符号是死代码,其中一个是另一函数的 17 行近似副本pkg/errcode/error.go:119-135WriteErrorWithTrace— 全仓 0 个调用方(含测试),是WriteError(:95-117)的逐行复制pkg/errcode/error.go:152-161EnvelopeForGin— 唯一调用方是它自己的测试pkg/errcode/errcode_test.go:168-170;活的那份是EnvelopeForGinWithTrace(:163-175,被hub-server/internal/handler/response.go:39使用)EnvelopeForGin(无 traceId 字段)留在pkg/导出面上=持续邀请新调用方选那个「丢掉 trace 关联」的劣化版本,而error.go:99-100的注释恰恰把 trace 关联当卖点修法:删
EnvelopeForGin及其测试;WriteErrorWithTrace二选一——删掉,或把WriteError重写为WriteErrorWithTrace(w, err, NewTraceID())让两者共用一份实现(后者更省事且保住 API)。写集 ≈2 文件 / 25 行删除,风险极低。验收
recover()」,否则这次收敛下次还会漂;edge 侧 panic 必须能记到 stack 与计数git diff --check干净 +make test不受影响即可;改完必须再跑一次find证明注释里点名的每个文件都真实存在_test.go)确认 0 调用方负向约束
pkg/是三 module 共享层,删之前确认无外部 consumer