Skip to content

注释/文档声称被活代码推翻:pkg/safego「两台服务已收敛」vs 4 处手写 recover(3 处无 stack/metric) + resputil「唯一 writer」vs pkg/errcode.WriteJSON(吞编码错误) + 6 处点名不存在文件的注释 + pkg/errcode 2 个死导出 #2246

Description

@DeliciousBuding

来源: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 需要的专属计数用已有的 SetPanicObservername 分派(observer 签名已带 name string,见 safego.go:39-50),不要保留私有计数器;把 safego.go:20-21 表述改准确,或加一条测试断言白名单外不存在裸 recover()
middleware/recovery.gomiddleware/timeout.gohttpserver/server_middleware.gohandler/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.WriteJSONserver_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.godelivery_outbox_model.godelivery_outbox_facade.go 前两个不存在;第三个存在但在父包 servicehub-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:198deliveryoutbox/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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions