Skip to content

fix(edge): 删掉 lifecycle 的第三份 safego 副本并接上 panic observer——Edge 生命周期 goroutine 的 panic 从此可观测 - #2236

Merged
DeliciousBuding merged 1 commit into
masterfrom
fix/edge-safego-single-impl
Sep 2, 2026
Merged

fix(edge): 删掉 lifecycle 的第三份 safego 副本并接上 panic observer——Edge 生命周期 goroutine 的 panic 从此可观测#2236
DeliciousBuding merged 1 commit into
masterfrom
fix/edge-safego-single-impl

Conversation

@DeliciousBuding

Copy link
Copy Markdown
Collaborator

结论先说

edge-server/internal/lifecycle/safego.go 是一份活的第三份 panic 恢复副本,守着 Edge 最关键的 13 处 goroutine 启动点;而 safego.SetPanicObserver 全仓只有 Hub 注册。结果:Edge 生命周期 goroutine 的 panic 只写日志、不进任何指标 —— 仪表盘与告警完全看不见,只能靠有人去 grep journal。同时 pkg/safego 的包注释用完成时态声称"两边已统一",这是假的。

关联:#2154(Feynman 文档/架构探索批 P1;本 PR 同时是 #2230 的后续——那个 PR 给 pkg/safego 加了 RecoverInto,而 Edge 这份副本拿不到)

核实到的事实

事实 证据
lifecycle 私有副本活着且守着关键路径 safego.gosafeGo/recoverPanickedGoroutine;13 处调用点:runhubAckhubDoneEnqueuehubFailEnqueuehubCallbackQueueresultAggregatorresultAggregatorTimeoutwatchRunProcesscancelGrace、build 阶段 5 处
同仓其它 edge 包早已用 pkg 版 adapters/orchestrator/orchestrator_dispatch_interceptor.go:184api/handlers_events.go:107store/file_store.go:92 → 一个进程两套恢复路径
Edge 无 observer SetPanicObserver 唯一注册点 = hub-server/internal/metrics/metrics.go:18(init)
pkg 文档假声明 pkg/safego/safego.go 包注释:"Both servers previously kept near-identical copies … This shared package unifies the recovery path"
无任何文档引用被删文件 全仓 .md grep lifecycle/safego = 0 命中

红证据(修复前)

panic_recovery_test.go:294: a recovered lifecycle panic never reached safego's
PanicObserver — Edge goroutine panics are invisible to metrics and alerting
--- FAIL: TestLifecycleGoroutinePanicReachesPanicObserver (3.00s)

该测试复用既有 TestFireHubCallbacksRecoverPanic 的 panicking CallbackReporter 夹具,注册 observer 后触发 fireHubAck,断言 observer 收到且 label 是 hubAck(label 是告警唯一的归因信息)。

改动

  1. 删除 lifecycle/safego.go,13 处调用点改 safego.SafeGo(行为等价:pkg 版是超集,多一条 observer 派发),同步更新引用旧标识符的注释。
  2. 新增指标 edge_goroutine_panic_recoveries_total{goroutine}:与既有 edge_http_panic_recoveries_total 合成"请求路径 / goroutine 路径"两半,也与 Hub 的 goroutine_panic_recoveries 对齐 —— 运维可用同一阈值告警两边。label 必须是静态调用点字面量(pkg/safego 对 name 的契约),空值归一到 unknown,注释里写明动态 label 会让 series 无界。
  3. (*EdgeMetrics).InstallPanicObserver():nil 安全;在 httpserver.buildEventBusAndMetrics 里于任何 run 可启动之前安装。
  4. pkg/safego 包注释改成事实:点名 lifecycle 那份副本、它守的 goroutine 清单、造成的可观测性缺口、现在两边的 observer 安装点,并写明 hook 进程级唯一、二次安装会替换。
  5. lifecycle/safego_test.gopanic_recovery_test.go(被测对象已不在本包),文件头写明分工:launcher 本身由 pkg/safego 自测;本文件钉两条属于本包的性质——① 生命周期 goroutine 确实都走它,② 恢复出来的 panic 可被观测。

覆盖是转移,不是消失(如实说明)

删掉了两个直测已删除的私有 launcher 的用例(TestSafeGoRecoversPanic / TestSafeGoRunsNormalFunc):pkg/safego 有等价测试。本包保留的 4 路 fireHub* panic 测试仍断言 4 条恢复日志(期望串随 pkg 的日志消息更新)。

一处自陈:我第一版测试有竞态

defer close(done)fn 内层先执行,而 observer 在 SafeGo 自己的 defer 里才跑 → <-done 后立刻读计数器恒为 0pkg/safego 自己的测试注释也警告过这点)。改为带 2s 截止时间的轮询,并把原因写进注释。

门禁(全部跑在本 HEAD)

门禁 结果
edge go build ./... / go vet ./... 干净
go test ./internal/lifecycle ./internal/metrics ./internal/httpserver ./internal/api ./internal/store ./internal/adapters/orchestrator -count=1 6 包 ok
go test ./internal/lifecycle -race -count=1 ok 34.5s,无 DATA RACE
golangci-lint run(lifecycle/metrics/httpserver) 仅 1 条既有 gocyclo(process_executor_build.go:37 复杂度 22;本 PR 只替换了其中调用名,复杂度未变)
verify-edge-lint-ratchet.py PASS(9 findings,全在基线)
gosec -fmt=json ./... | verify-gosec-gates.sh CLEAN
verify-doc-ssot / verify-conventions rc=0
pkg go test ./... ok
git diff --check 干净

证据等级:L0(单测 + -race + 静态门禁)。

未做活体 panic 注入:新指标是 CounterVec,首次恢复 panic 前不产生 series,所以活体只能验证 Edge 重启健康 + /v1/metrics 可服务;接线正确性由上述两个 metrics 测试与 lifecycle observer 测试证明。合并后我会重建 bin/edge 并做这两项活体检查。

…oroutine 的 panic 从此可观测 (#2154)

现状(逐条源码核实):
- `edge-server/internal/lifecycle/safego.go` 是一份**活的**私有副本
  (safeGo + recoverPanickedGoroutine),守着 edge 最关键的 13 处 goroutine
  启动点:run / hubAck / hubStream(经 hubCallbackQueue) / hubDoneEnqueue /
  hubFailEnqueue / hubCallbackQueue / resultAggregator /
  resultAggregatorTimeout / watchRunProcess / cancelGrace / build 阶段 5 处。
- 同仓其它 edge 包(adapters/orchestrator、api/handlers_events、
  store/file_store)**早已**用 pkg/safego → 一个进程里两套恢复路径。
- `safego.SetPanicObserver` 全仓**只有 hub 注册**
  (hub-server/internal/metrics/metrics.go:18 的 init);edge 没有任何 observer,
  而私有副本也从不派发 → edge 生命周期 goroutine 的 panic **只写日志、不进任何
  指标**:仪表盘与告警完全看不见,只能靠有人去 grep journal。
- `pkg/safego` 的包注释用完成时态声称「两边曾各有一份、现已统一」——**当时是假
  的**(lifecycle 这份没被收编),本 PR 一并把注释改成事实并记下收编过程。

红证据(修复前,新增测试直接失败):
  panic_recovery_test.go:294: a recovered lifecycle panic never reached
  safego's PanicObserver — Edge goroutine panics are invisible to metrics and
  alerting
  --- FAIL: TestLifecycleGoroutinePanicReachesPanicObserver (3.00s)
该测试复用既有 TestFireHubCallbacksRecoverPanic 的 panicking CallbackReporter
夹具,注册 observer 后触发 fireHubAck,断言 observer 收到且 label 是 "hubAck"
(label 是告警唯一的归因信息)。

改动:
- 删除 lifecycle/safego.go,13 处调用点改为 safego.SafeGo(行为等价:pkg 版是
  超集,多一条 observer 派发);同步更新引用旧标识符的注释。
- edge metrics 新增 `edge_goroutine_panic_recoveries_total{goroutine}`
  (CounterVec,与既有 edge_http_panic_recoveries_total 形成"请求路径 /
  goroutine 路径"两半,也与 hub 的 goroutine_panic_recoveries 对齐,运维可用
  同一阈值告警两边)+ `(*EdgeMetrics).InstallPanicObserver()`(nil 安全,
  label 空值归一到 "unknown"),在 httpserver.buildEventBusAndMetrics 里于任何
  run 可启动之前安装。
- pkg/safego 包注释改为事实:点名 lifecycle 那份副本、它守的 goroutine 清单、
  它造成的可观测性缺口,以及现在两边的 observer 安装点;并写明 hook 是进程级
  唯一、二次安装会替换。
- lifecycle 的 safego_test.go 更名 panic_recovery_test.go(被测对象已不在本
  包),文件头写明分工:launcher 本身由 pkg/safego 自测,本文件钉两条属于本包
  的性质(① 生命周期 goroutine 确实都走它;② 恢复出来的 panic 可被观测)。
  其中两个直测已删 launcher 的用例(TestSafeGoRecoversPanic /
  TestSafeGoRunsNormalFunc)删除——pkg/safego 有等价测试,覆盖是**转移**不是
  消失;本包保留的 4 路 fireHub* panic 测试仍断言 4 条恢复日志。

一处自陈:我第一版 metrics 测试有竞态——`defer close(done)` 在 fn 内层先执行,
而 observer 在 SafeGo 自己的 defer 里才跑,于是 `<-done` 后立刻读计数器恒为 0
(pkg/safego 自己的测试注释也警告过这一点)。改为带 2s 截止时间的轮询,并把
原因写进注释。

门禁(全部跑在本 HEAD):
- edge go build ./... / go vet ./... 干净
- go test ./internal/lifecycle ./internal/metrics ./internal/httpserver
  ./internal/api ./internal/store ./internal/adapters/orchestrator -count=1 → 6 包 ok
- go test ./internal/lifecycle -race -count=1 → ok 34.5s,无 DATA RACE
- golangci-lint(lifecycle/metrics/httpserver 三包)→ 仅 1 条既有 gocyclo
  (process_executor_build.go:37 buildAndStartProcess 复杂度 22,本 PR 只替换
  了其中的调用名,复杂度未变);verify-edge-lint-ratchet.py → **PASS(9
  findings,全在基线)**
- gosec -fmt=json ./... | verify-gosec-gates.sh → CLEAN
- verify-doc-ssot / verify-conventions rc=0;pkg go test ./... ok
- git diff --check 干净;全仓 grep 无任何 .md 引用被删文件

证据等级:L0(单测 + -race + 静态门禁)。未做活体 panic 注入:新指标
edge_goroutine_panic_recoveries_total 属 CounterVec,首次恢复 panic 前不会
产生 series,故活体只能验证 edge 重启健康 + /metrics 可服务,接线正确性由
上述两个 metrics 测试与 lifecycle observer 测试证明。

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: caaa14f5-6895-4037-bfa5-16e07c3e4262

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DeliciousBuding
DeliciousBuding merged commit dc7df53 into master Sep 2, 2026
39 checks passed
@DeliciousBuding
DeliciousBuding deleted the fix/edge-safego-single-impl branch September 2, 2026 16:48
DeliciousBuding added a commit that referenced this pull request Sep 2, 2026
…2154)(#2237)

## 这条 lane 是什么

实施 lane E(**纯文档**)= #2154 Feynman 文档探索批的切片 C「对外文档诚实批」。
唯一非 `.md` 改动是第 6 条的 `scripts/verify/quality-debt-baseline.json`(任务书明示的唯一例外)。
**未改任何 `.go` / `.ts` / `.tsx` / `.yaml` 契约文件。**

分支已 `git fetch origin && git rebase origin/master`;上游新增的 `dc7df53d`(纯 Go:edge lifecycle safego 去重 + panic observer)与本分支 9 个文件**零重叠,rebase 无冲突**,因此没有触发「两边都保留 + 行数预算重新核算」的解冲突流程。

合并时 HEAD = `b017f0f4`,base = `94aef98a`(origin/master);lane push 时的 HEAD 为 `8f2ade25`(base `dc7df53d`)。7 个 commit,每个 commit 一次门禁。

**订正(主机侧合并前补记,原文此处写的是「故意不再 rebase」,与最终实况不符)**:push 后 `origin/master` 由 `dc7df53d` 连续前进到 `94aef98a`(#2234/#2235/#2236/#2239/#2240),本仓 `required_status_checks.strict: true` 使 PR 转 `BEHIND`,故本分支**已按纪律 rebase 到 `94aef98a`**,HEAD 由 `8f2ade25` 变为 `b017f0f4`。rebase 前后**写集逐字节零变化**:`git diff --stat 8f2ade2 b017f0f -- AGENTS.md CHANGELOG.md CONTRIBUTING.md README.md README_EN.md SECURITY.md docs/ scripts/verify/quality-debt-baseline.json` **输出为空**;两者全量 diff 只含 master 自身的 `app/pnpm-lock.yaml`+`app/pnpm-workspace.yaml`(#2240)与 `edge-server/internal/events/**`(#2234/#2239),与本 lane 9 个文件零重叠;7 个 commit 逐条 subject 一一对应。因此正文里所有本地实测结论原样成立。rebase 后按原文要求**重跑并复现**:`scripts/verify/verify-doc-ssot.py` → `doc SSOT ok`(verifier-map 66 script paths / 58 CI files、AGENTS.md 96 paths)、`wc -l AGENTS.md` → **284**(≤300 预算)、`git diff --check` 干净;CI run `33666303378` 在 `b017f0f4` 上**全绿**(22 successful / 17 skipped / 0 failing / 0 pending,含 go-hub、go-edge、windows-go、backend-required、frontend-required、ui-required、validate、CodeRabbit),`mergeStateStatus: CLEAN`。

---


## 逐条三段式证据(文档原话 → 代码/CI 事实 → 改后原话)

> 7 条逐字引证与改后原话全文见本 PR 正文(GitHub 侧永久保留),squash commit 只收判定结论以免历史膨胀。

| # | 条目 | 复核判定 |
|---|---|---|
| 1 | `CONTRIBUTING.md:36` 称 `make test` 跑前端 vitest | ✅ 成立(四处口径改指明 Makefile 目标名) |
| 2 | `CHANGELOG.md:5-7`「暂无未发布变更」 | ✅ 成立(改为声明 SSOT 与生成方式) |
| 3 | `SECURITY.md:23` 安全门禁工具名指错 | ✅ 成立,且原文另有一处更严重的不诚实 |
| 4 | `README.md` / `README_EN.md` | ⚠️ 部分成立(禁用词那条报告说法不准确,但假声明本身成立) |
| 5 | `verifier-map.md` + `docs/architecture/README.md` 双向差集 | ✅ 成立(补 12 行 + 修 2 处不诚实 + 补 1 行索引) |
| 6 | `quality-debt-baseline.json` 的 `issue` 归属 | ⚠️ 报告部分成立:6 条里只敢改 2 条 |
| 7 | `AGENTS.md` §12 加「CHANGELOG owner」 | ✅ 净减 2 行守住行数预算(284≤300) |

## 复核后判定「不成立 / 已过期」而跳过或改判的条目

| # | 报告说法 | 实测 | 处置 |
|---|---|---|---|
| 4b | 「路线图/roadmap 被门禁主动禁止却仍存在」 | `verify-doc-ssot.py` 只禁**根级文件 `ROADMAP.md`**(`:100`)、**路径 `docs/roadmap`**(`:109`)、**正则 `ROADMAP\.md`**(`:200`);没有任何规则禁「路线图」这个词,门禁本来就跑得绿 | 报告说法**不成立**,已在 PR 正文写明。但底下的**假声明成立**(`docs/` 里确实没有路线图),故仍按事实改,改法换成门禁理由本身陈述的真事实(roadmap 在 GitHub issues) |
| 6 | web lint 债的真实归属是 1575 | `gh issue view 1581` 正文原话「**#1575 只负责 Desktop ESLint,不覆盖 Web**」 | 报告此点**不成立**,改判为 **#1581** |
| 6 | i18n callsite 债的真实归属是 1612 | `#1612` 是 PR「docs(progress): MASTER 同步」,`files` 只有 `docs/progress/MASTER.md`;而仓内三处(checks.yml:2126 / verifier-map:27 / CHANGELOG:44)一致引用 #1612 | 报告此点**不成立**(且暴露更大问题:全仓的 #1612 引用可疑)。**不改**,登记 #2154 待裁决 |
| 6 | 5/6 条都该改 | 只有 2 条能拿到「该 issue 明确以这笔债为标的」的正文证据 | **只改 2 条**,另 3 条按任务书要求不猜号 |
| 1 | 四处口径自相矛盾 | `docs/developer-quickstart.md:122-123` 其实是**正确**的那一处 | quickstart **未改**,只改 CONTRIBUTING(错的那处)+ AGENTS(歧义的那处) |
| 2 | 若不成立才补真实条目 | SSOT 判断**成立** | 按要求**没有**手写 Unreleased 列表 |

---

## 门禁表(原跑于 HEAD `8f2ade25` / base `dc7df53d`;rebase 到 `b017f0f4` / base `94aef98a` 后写集零变化,doc 门禁已重跑复现、CI 已全绿重证)

| 门禁 | 命令 | 结果 |
|---|---|---|
| 文档 SSOT(主门禁) | `python3 scripts/verify/verify-doc-ssot.py` | ✅ `doc SSOT ok`;verifier-map **66** 脚本路径 / **58** CI 文件全部存在;AGENTS.md **96** 个反引号路径全部存在;`DOC-README-PARITY` PASS |
| CI 结构合同 | `python3 scripts/verify/verify-ci-gates.py` | ✅ `ci gate policy ok` |
| 质量债棘轮(动了 baseline) | `python3 scripts/verify/verify-quality-debt-ratchet.py` | ✅ **9 pass / 0 fail** |
| 质量债棘轮负向自测 | `python3 scripts/verify/tests/verify-quality-debt-ratchet.Tests.py` | ✅ **15 tests OK** |
| skill 白名单 | `python3 scripts/verify/verify-project-skills.py` | ✅ rc=0(`skills root absent (.agents removed) — whitelist gate trivially passes`) |
| conventions 方法 SSOT | `python3 scripts/verify/verify-conventions.py` | ✅ `Passed: 1 \| Failed: 0` |
| doc-ssot 负向自测 | `python3 scripts/verify/tests/verify-doc-entrypoints.Tests.py` | ✅ `Ran 1 test … OK`(证明主门禁没被我的改动弄钝) |
| 空白/冲突标记 | `git diff --check origin/master..HEAD` | ✅ clean |
| AGENTS.md 行数 | `wc -l AGENTS.md` | ✅ **284** ≤ 300 |
| **GitHub Actions(本 PR 真实 run)** | run [33658111957](https://github.com/TokenDanceLab/AgentHub/actions/runs/33658111957) | ✅ **22 SUCCESS / 17 SKIPPED / 0 非绿**;7 个 required 聚合全绿:`validate` `go-edge` `go-hub` `windows-go` `windows-frontend` `backend-required` `frontend-required` |
| 其他行数预算 | `wc -l` | ✅ CHANGELOG.md 80/90、CONTRIBUTING.md 58/90、verifier-map.md 87/120、docs/architecture/README.md 28/40 |

**按纪律未跑**:`go test`、`go build`、vitest、coverage、全量 golangci-lint、docker、`make *`(4 核机 + 并行 lane)。
**golangci-lint 幽灵**:本 lane 未跑 golangci-lint,未遇到指向已删除 worktree 路径的缓存幽灵 issue。

---

## 未验证项(诚实声明)

1. ~~**没有跑任何 CI**~~ → **已验证(本条从「未验证」升级为「已验证」,PR 开出后回写)**:GitHub Actions run [33658111957](https://github.com/TokenDanceLab/AgentHub/actions/runs/33658111957) 结果 **22 SUCCESS / 17 SKIPPED / 0 非绿**,7 个 required 聚合(`validate`/`go-edge`/`go-hub`/`windows-go`/`windows-frontend`/`backend-required`/`frontend-required`)全部 SUCCESS。`validate` 是承载 `verify-doc-ssot.py` + `verify-ci-gates.py` + `verify-quality-debt-ratchet.py` + `verify-conventions.py` + `verify-project-skills.py` 的 job,它 SUCCESS ⇒ 本 PR 全部 9 个文件的改动在 CI 上被同一套门禁判绿,不只是我本地判绿。`go-*` 侧也跑了(因为 `scripts/verify/**` 在 `changes` job 的 `go` 路径过滤里,baseline JSON 改动触发了 Go lane),`go-hub` 的 golangci-lint + 覆盖率门禁 SUCCESS ⇒ 未出现缓存幽灵。
   **顺带活体印证第 3 条的改法**:`Vuln scan (pnpm audit prod+full)`、`Vuln scan (govulncheck)`、`Vuln scan (cargo audit)`、全部 `frontend-*`、`Visual QA *`、`Design CSS syntax` 在本 PR 均为 **SKIPPED** —— 正是我写进 `SECURITY.md` 的「三个 vuln-scan job 都经 `changes` job 路径过滤触发,不是每次 push 全量扫描」的实时证据(本 PR 不含 `app/**` 改动)。
2. **markdown 渲染只在本地按 CommonMark 规则推断**,没有在 GitHub 上肉眼看过渲染结果。两处需要 review 时确认:`docs/architecture/README.md` 新增行、`verifier-map.md` 宏观四行并入主表后是否真的渲染成表格。
3. **`gh issue view` 读到的是 issue/PR 的当前标题与正文**,不能证明「该 issue 在软门禁被引入的那一刻就是 owner」。desktop→#1575 / web→#1581 的判定依据是两个 issue 正文**逐字点名了对应的 baseline 条目与 step 名**,这是我能拿到的最强证据,但仍属文档考古而非当事人确认。
4. **tag `v0.6.1` 与 master 历史脱钩**这件事我只做了 `git merge-base --is-ancestor` / `git merge-base` 两个命令的验证,**没有**去查 release.yml 的历史 run 是否真的因此失败过,也没有验证 git-cliff 在无前序 tag 时的实际输出长度。它超出纯文档 lane 范围,只登记不动手。
5. **未改任何产品代码**,因此第 3/5 条里所有关于「门禁 fail-closed」的描述都是**读脚本源码 + workflow YAML 得出**,不是我实跑这些门禁观察到的红/绿。唯一实跑过的是 `verify-doc-ssot.py` / `verify-ci-gates.py` / `verify-quality-debt-ratchet.py` / `verify-conventions.py` / `verify-project-skills.py` 及两个负向自测。

---

## 需要人工裁决 / 后续 issue(已同步登记 #2154)

1. quality-debt baseline 3 条 `issue` 归属待确认:`frontend-mobile: Lint (mobile rules)`、`validate: Verify i18n callsites ratchet`、`vuln-scan-rust: cargo clippy (advisory)`(现值均为可证伪的 1573)。
2. 全仓 `#1612` 引用可疑(checks.yml:2126 / verifier-map:27 / CHANGELOG:44 三处),需定位 i18n callsite ratchet 的真实接线 issue/PR。
3. tag `v0.6.1` 不在 master 祖先链上 ⇒ `release.yml:42` tag-guard 与 git-cliff `--latest` 的前序 tag 解析都受影响,下一次打 tag 前需裁决(重打 tag / 调整 cliff 调用 / 接受全量分组)。
4. `cliff.toml` 的 `^security` commit parser 是**死分支**(提交类型白名单不含 `security`):要么给白名单加 `security`,要么删掉这个 parser 并改用 label/其它机制披露安全修复。本 PR 只把 SECURITY.md 的承诺改成与现状一致,没动 cliff.toml(属产品配置,非纯文档 lane 范围)。
5. `scripts/verify/tests/merge-coverprofiles.Tests.py` 与 `scripts/verify/tests/verify-real-e2e-artifacts.Tests.py` **存在于磁盘但没有任何 workflow 调用**(`grep .github/workflows/` 零命中)⇒ 两个负向自测是死的。我在 verifier-map 里因此**没有**把它们写成「负向自测」(只登记了脚本本体),避免制造新的假绿声明;是否接线请裁决。
6. #1575 / #1581 均已 CLOSED,但对应的两条 `continue-on-error` 软门禁**仍在 checks.yml 里活着**(`verify-quality-debt-ratchet.py` 的 zombie 检查 PASS 即证明这点),且两条的 `review_by` 都是 `2026-10-01`。即「偿还 ESLint 债并移除软门禁」的 issue 关了、软门禁没移除。属治理不一致,非本 lane 范围。

## 流程事故记录(不影响代码,但影响交付物可信度,故如实记)

开出本 PR 后、往 #2154 贴登记评论时,**另一条并行 lane(Lane A,#2154 评论 `5513247382`)在同一分钟覆写了 `/tmp/pr-body.md`** —— 两条 lane 用了同一个临时文件名。后果与处置:

- **PR #2237 正文未受影响**:`gh pr create` 在覆写发生前已执行完毕。事后用 `gh pr view 2237 --json body` 回读实测 21035 字符,首句「## 这条 lane 是什么」、末句「Closes 无(本 PR 是 #2154 的切片 C…)」,且 `grep -c "toast.actionUnavailable"`(对方正文特征串)= 0 ⇒ 内容是我的、完整的。
- **#2154 的首版评论被污染**:拼评论时读到的是对方正文,等于把我的抬头 + Lane A 的正文贴了上去。已从 PR 正文回读重建、用 `gh api -X PATCH .../issues/comments/5513238457` **原地编辑**修正(不新贴第二条制造噪声),并复核修正后正文里对方 lane 的 5 个特征串(`§6.4`/`§6.5`/`desktop forward`/`regenerate 是否另开 lane`/`i18n 资源面 lane`)全部 0 命中、我的 4 个结构节各 1 次。
- **教训(供主机侧收进并行 lane 纪律)**:多 lane 并行时临时文件必须用 lane 唯一路径。本 lane 后续已改用 `/tmp/laneE-doc-honesty-2237/`。这与 `AGENTS.md`「一个 worktree 同时只放一个写 agent」是同一类风险,但发生在 worktree 之外的共享 `/tmp`,现有规则没覆盖到。

Closes 无(本 PR 是 #2154 的切片 C,#2154 由主机侧统一收口,不在此自动关单)。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant