Skip to content

fix(multi-review): serial import + wait for serve ready line#289

Merged
Svtter merged 2 commits into
mainfrom
fix/v2-resume-migration-race
Jun 28, 2026
Merged

fix(multi-review): serial import + wait for serve ready line#289
Svtter merged 2 commits into
mainfrom
fix/v2-resume-migration-race

Conversation

@Svtter

@Svtter Svtter commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

问题

OpenCode Multi-Review 在 reviewer 数量较多的 PR 上 session-resume 失败:只有 2/7 的 bundle 被恢复,其余 5 个 reviewer 输出为空,coordinator 报「failed to complete」,最终强制 CANNOT MERGE。

线上实例:sun-praise/latex-agent#4165(10:07 那次 run,job 成功但 5 个 reviewer 全空)。

CI 日志关键行:

v2 resume: using temp XDG_DATA_HOME=.../oac-resume-4165-...
v2 resume: schema bootstrapped in 1722ms
v2 resume: failed to import bundle: opencode import .../bundle.json exited 1
[stderr] Error: Unexpected error
Failed query: INSERT INTO "migration" (id, time_completed) VALUES (?, ?)
index message_session_time_created_id_idx already exists
Failed query: ALTER TABLE `session` ADD `cost` real DEFAULT 0 NOT NULL
v2 resume: 2/7 bundles restored

根因(两个叠加 bug,均在 multi-review/src/session-resume.ts

  1. Bootstrap 误判就绪:代码轮询 opencode.db 文件是否存在,文件一出现就 kill opencode serve。但 SQLite 在 migration 提交之前就会创建 db 文件,于是 schema 经常是半成品。
  2. 并发 import 竞争 migration DDL:7 个 bundle 通过 Promise.allSettled 并发 opencode import,共用同一个 temp DB。每个 import 检测到 schema 版本旧就重跑非幂等的 migration DDL(ALTER TABLE ADD cost / CREATE INDEX / INSERT INTO migration),互相撞「already exists」,5 个失败、2 个抢到锁的成功。

2/7 成功 = performance + regression-test,正好对应那条 PR 评论里只有这俩 reviewer 有内容。

修复

  • 等 authoritative ready 信号opencode serve 在所有 migration 提交之后才输出 opencode server listening on http://...。改为读 serve 的 stdout/stderr,匹配到这行才认为 schema 就绪(带超时 + spawn-error + premature-exit 兜底)。
  • import 改串行:彻底消除并发 migration 竞争。per-bundle try/catch 仍然隔离失败;串行多花的几秒换来确定性。

测试(新增 session-resume.test.ts

用 fake opencode binary on PATH 做确定性回归测试(无 wall-clock 时序假设):

  • imports bundles SERIALLY:每个 import 用 per-name release file 做门控,测试驱动释放顺序并断言 max in-flight == 1。临时回退到旧的并行实现时,该测试失败(max in-flight == 4)—— 证明能抓住 bug。
  • bootstrap waits for the listening line:fake serve 立即创建 opencode.db 但延迟 listening 行,断言 bootstrap 超时告警触发(证明等的是 listening 行而非 db 文件)。临时回退到 db-file 轮询时,该测试失败。
  • returns early with empty map when no bundles:空输入快速路径。

跑前两项都做了 temp-revert 验证:回退到 bug 行为 → 测试红;恢复修复 → 测试绿。

TMPDIR 沙箱化避免与 opencode-bundle 套件的「tmpdir 里无 oac-bundle-*」断言互相污染。

验证

  • bun run test151/151 pass(含新增 3 例)
  • bun run build:dist 重建成功,包含 listening-line 正则与串行 import 循环
  • 正则 /listening on https?:\/\//i 实测匹配本地 opencode serve 真实输出

附带

  • tsconfig.json"lib": ["ES2024"]Promise.withResolvers 在 ES2022 lib 里不存在;Node 22.22 runtime 原生支持,无需 polyfill)
  • bun install 顺带清掉 lockfile 里陈旧的 better-sqlite3 传递依赖(主 package.json 从未直接依赖它)

非本 PR 范围

bun run check 在 main HEAD 上已有 31 个 error(context-cache.ts null-narrowing、vitest 模块缺失、index.ts 类型收窄),按约定放下一个 PR 处理。

The v2 session-resume path restored only 2/7 bundles on PRs with 7
reviewers, producing empty reviewer output ("failed to complete")
and forcing CANNOT MERGE. Observed on sun-praise/latex-agent#4165.

Two compounding bugs in session-resume.ts:

1. Bootstrap misjudged readiness: it polled for the opencode.db FILE
   and killed `opencode serve` the instant the file appeared. SQLite
   creates the file before migrations commit, so the schema was often
   half-migrated when imports began.

2. Concurrent imports raced migration DDL: 7 bundles imported via
   Promise.allSettled against one SQLite DB. Each `opencode import`
   re-ran the non-idempotent migration DDL (ALTER TABLE ADD cost,
   CREATE INDEX message_session_time_created_id_idx, INSERT INTO
   migration). 5/7 died with "already exists"; only the 2 that won
   the lock succeeded.

Fix:
- Wait for the authoritative `listening on http://` line that
  `opencode serve` prints AFTER migrations commit (read serve
  stdout/stderr instead of polling the db file).
- Import bundles SERIALLY. Per-bundle try/catch still isolates
  failures; the few seconds of serialization is worth determinism.

Tests (session-resume.test.ts): deterministic regression tests using
a fake `opencode` binary on PATH. The serial test orchestrates each
import via a per-name release gate and asserts max-in-flight == 1
(under the old parallel code it was N). The bootstrap test delays the
listening line past the cap and asserts the timeout warning fires
(proving we waited for the real signal, not the db file). Both tests
fail when temporarily reverted to the buggy behavior.

Also: bump tsconfig lib to ES2024 (Promise.withResolvers), sandbox
TMPDIR in the suite to avoid cross-test pollution of the
opencode-bundle cleanup assertion, prune stale better-sqlite3
transitive entries from bun.lock.
@github-actions

Copy link
Copy Markdown

⚠️ 有条件合并 / CONDITIONAL MERGE

此 PR 修复了两个竞态条件 bug,设计方向正确,测试结构优秀。但有一个运行时阻塞项需修复:Promise.withResolvers() 在 Node.js 20 上不存在。


🔴 阻塞项 / Blocking Issues (1)

  • Promise.withResolvers() 需要 Node.js 22+,但未声明最低运行时版本

🟡 警告项 / Warnings (8)

  • 子进程 stdout/stderr 缺少 "error" 事件处理
  • serveOutput 字符串无界增长
  • 串行导入增加总耗时
  • 缺少异常路径的回归测试
  • opencode serve spawn 失败(bin 不存在/不可执行)
  • opencode serve 就绪前非零退出
  • 部分 bundle 导入失败时其余 bundle 仍成功
  • bootstrap 超时测试缺少关键断言

🟢 建议项 / Suggestions (7)

  • 限制 serveOutput 最大长度(如 4096 字节),防止无界增长并避免多字节 UTF-8 截断
  • 为每个 bundle 导入添加超时(如 30s),防止单个挂起导入阻塞整个串行恢复流程
  • 未来考虑有界并发导入(如同时最多 2-3 个),在 DDL 竞态修复后在串行隔离与吞吐之间取得平衡
  • 统一 tsconfig.jsontargetlib:当前 target: "ES2022" + lib: ["ES2024"] 语义不一致,建议提升到 ES2024 或添加注释说明
  • fake opencode 脚本的 setInterval 增加 unref(),避免影响测试进程退出
  • 错误消息中限制 serve 输出暴露范围(如明确标注 "for debugging only"),防止未来意外泄露环境变量等信息
  • 补全 undefined / 空数组输入覆盖路径
💰 Review Cost — $0.0000
Role Cost (USD) Input Output Reasoning Cache Read Cache Write
quality $0.0000 15,445 767 4,621 0 0
security $0.0000 15,454 169 634 0 0
performance $0.0000 15,457 842 1,304 0 0
architecture $0.0000 15,458 467 931 0 0
regression-test $0.0000 15,895 135 1,461 0 0
test-value $0.0000 25,433 461 2,543 0 0
coordinator $0.0000 11,229 1,001 1,234 0 0
Total $0.0000 114,371 3,842 12,728 0 0
📋 各 Reviewer 详细审查结果
quality

有条件合并 / CONDITIONAL MERGE

评审总结

本 PR 修复了两个关键问题:bootstrap 时等待 opencode serve 的 listening 行而非 opencode.db 文件存在(后者在迁移完成前就已创建),以及将 bundle import 从并行改为串行以避免并发迁移 DDL 竞争。设计方向正确,测试设计精良。但仍有一项阻断性问题。


阻塞项 / Blocking Issues

  1. Promise.withResolvers() 需要 Node.js 22+,但未声明最低运行时版本
    multi-review/src/session-resume.tsdist/index.cjs 直接调用了 Promise.withResolvers() 全局 API。该 API 在 Node.js 22.0.0 才正式可用。tsconfig.json 虽然设置了 "lib": ["ES2024"](让类型检查通过),但这不影响运行时行为。package.json 未设置 engines.node 字段。如果跑在 GitHub Actions 的默认 Node.js 20 runner 上,会抛出 TypeError: Promise.withResolvers is not a function,导致整个 restore session 流程崩溃。建议:声明最低 Node.js 版本为 22,或者使用手动实现(如 new Promise<void>(res => resolve = res))代替 Promise.withResolvers

警告项 / Warnings

  1. serve 输出截断可能切断多字节 UTF-8 字符
    session-resume.tsserveOutput.trim().slice(0, 500) 若截断位置落在多字节字符中间,错误信息可能产生残缺的 Unicode。建议使用 Buffer.from(serveOutput.trim()).subarray(0, 500).toString("utf8") 避免截断问题。

  2. 子进程 stdout/stderr 未处理 'error' 事件
    bsProc.stdout?.on("data", onServeData) 等未绑定 "error" 事件处理。如果流触发 error,事件静默丢失且不会 reject readyPromise。建议增加 .on("error", readyReject)


建议项 / Suggestions

  1. 测试设计优秀。用 marker/gate 文件机制确定性检测串行执行的方案,避免了基于 wall-clock 定时的脆性测试,值得推广。
  2. fallback 策略(bootstrap timeout 后仍尝试 import,依赖 per-bundle try/catch)合理,降低了该 PR 引入 regression 的风险。
  3. 建议为 fake opencode 脚本中的 setInterval(() => {}, 1 << 30) 增加 unref() 调用,避免影响测试进程退出。
security

安全无虞 / SECURE

本次 PR 主要修复 session-resume.ts 中的竞态条件(opencode.db 空洞创建 vs 等待真正 migration 完成、并行 import 冲突非幂等 DDL),不涉及安全相关的代码路径变更。

阻塞项:无

警告项:无

建议项:

  • session-resume.ts:105 处将子进程 opencode serve 的 stdout/stderr 输出截取 500 字符后传入错误消息并 console.warn,虽在 CI 环境下风险较低,但若未来 serve 输出中包含意外信息(如错误栈中的环境变量片段),建议限制其暴露范围或明确标记为仅供调试。
performance

性能有疑虑 / CONCERNS

性能分析概要

此 PR 主要修复了 restoreSessionBundles 中的两个竞态条件 bug:1) 引导阶段等待 opencode.db 文件存在而非等待迁移完成;2) 并行 bundle 导入导致 SQLite 迁移 DDL 竞争。这两个修复以正确性优先为原则,但引入了一个内存使用方面的潜在性能隐患。

影响性能的修改

  • session-resume.ts: 将 Promise.allSettled 并行导入改为 for...of 串行导入;引导机制从轮询文件存在改为等待 stdout/stderr 的 listening line
  • session-resume.ts: tsconfig.json 添加 "lib": ["ES2024"] 以支持 Promise.withResolvers()

阻塞项:无

无必须阻塞合并的严重性能回归。

警告项

  1. serveOutput 字符串无界增长(session-resume.tsonServeData 回调不断拼接所有 stdout/stderr 输出到 serveOutput 字符串。如果 opencode serve 在 listening line 输出之前产生了大量日志(例如调试级别输出、堆栈跟踪等),该字符串可能增长到数 MB 且无处截断。虽然在正常运作中 listening line 很快出现,但在极端场景(服务启动异常缓慢、产生冗长的告警日志)下该字符串会持续累积直到超时触发(bootstrapTimeoutMs,默认 10 秒)。exit 处理器只用前 500 字符,剩余大量字符串在函数返回前无法被 GC。

  2. 串行导入增加总耗时(session-resume.ts:虽然串行导入是必要的正确性修复(并行导入导致大部分 bundle 因 DDL 竞争而失败),但它将 bundle 导入阶段的延迟从 max(各 bundle 耗时) 变为 sum(各 bundle 耗时)。对于 bundle 数量较多的场景(例如 10+ 个 reviewer),此阶段耗时可能从 ~2-3 秒变为 10+ 秒。在处理 bundle 数量级很大的 PR 时这一延迟会线性增长。

建议项

  1. 限制 serveOutput 大小:建议在 onServeData 中设置一个上限(例如 4096 字节),达到上限后只保留末尾部分或停止拼接。仅当 listening line 出现前就需要足够的历史数据来诊断退出原因时才需保留全部输出,500 字符的诊断截断已足够:

    const MAX_SERVE_OUTPUT = 4096;
    const onServeData = (chunk: Buffer | string): void => {
      const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
      if (serveOutput.length < MAX_SERVE_OUTPUT) {
        serveOutput += text;
        if (serveOutput.length > MAX_SERVE_OUTPUT) serveOutput = serveOutput.slice(0, MAX_SERVE_OUTPUT);
      }
      if (/listening on https?:\/\//i.test(serveOutput)) {
        readyResolve();
      }
    };
  2. 考虑在 bundle 数量较多时引入有限并发:如果未来 opencode import 的 DDL 竞态被修复,可考虑使用有界并发(例如 p-limitPromise.allSettled 配合分桶)在串行隔离和并行吞吐之间取得平衡,例如每次最多并发 2-3 个导入。

  3. 单个 bundle 导入超时:当前每个 importBundle 调用没有超时保护,一个挂起的导入会阻塞后续所有导入的串行进度。考虑为每个导入调用添加超时(例如 30 秒),以便在某个 bundle 异常挂起时不会完全阻塞恢复流程。

architecture

架构合理 / SOUND

该 PR 修复了 session-resume.ts 中两个可观测的竞态条件,改动集中且模块边界清晰。

分析总结:

  • 耦合度session-resume 模块只依赖 types.ts 的类型定义和同模块的 importBundle 工具函数,无跨模块耦合问题。
  • 模块放置session-resume.ts / .test.ts 位于 multi-review/src/,路径和命名均符合现有模块约定。
  • 分层:会话恢复逻辑(schema 引导 + bundle 导入)封装在单一模块中,与 orchestrator 之间的接口 restoreSessionBundles(reviewBundles, options) → ResumeContext 清晰且职责单一。
  • 接口设计ResumeOptions 配置项(baseTempDir, prLabel, opencodeBin, bootstrapTimeoutMs)抽象层次合理;返回值 ResumeContext 包含 existingSessionstempDataHome,调用方无需关心内部实现细节。
  • 散射程度:除构建产物外,逻辑改动全部集中在 session-resume.ts 内;tsconfig.jsonpackage.json 的变更是必要的配套改动,不属于散射。
  • 架构一致性console.warn/log 的日志风格、Promise.withResolvers + AbortController 的资源管理方式、类型定义风格均与现有代码一致。

阻塞项:无

警告项:无

建议项:

  • tsconfig.jsontarget: "ES2022" 配合 lib: ["ES2024"] 略有语义不一致。Promise.withResolvers() 运行时需要 V8 支持,Node 22+ 无问题,但从配置角度建议将 target 也提升到 ES2024(如果构建链支持),或保持 target: "ES2022" 但添加注释说明为何 lib 指定为 ES2024。当前不会引起实际故障,可做可选的清理。
regression-test

缺少回归测试

PR类型:BUGFIX

该 PR 修复了两个关键竞态条件导致的 v2 resume: 2/7 bundles restored 问题:1) bootstrap 从轮询 db 文件改为等待 listening line;2) bundle 导入从并行改为串行。已包含测试文件 session-resume.test.ts,覆盖了串行导入验证和 listening line 等待验证,但仍缺少以下回归测试:

  • opencode serve spawn 失败bsProc.on("error") 触发时,readyReject(err) 的行为没有任何测试。如果 opencode 二进制不存在或无法启动,当前测试全部跳过此路径。

    • 严重性:CRITICAL
    • 建议:增加一个测试,使用不存在的 bin 路径或不可执行的 fake binary,验证 restoreSessionBundles 不会抛出未捕获异常,而是正确 fallback 并输出警告。
  • opencode serve 提前退出(非0退出码)bsProc.on("exit") 中非 null code 的 reject 逻辑未被覆盖。例如 serve 进程因配置错误立即 crash。

    • 严重性:MEDIUM
    • 建议:增加测试,让 fake opencode serve 在启动后立即 process.exit(1),验证输出正确的退出警告且不影响后续 import。
  • 部分 bundle 导入失败:串行循环中有 try/catch 隔离失败,允许后续 bundle 继续导入。当前测试只覆盖了全部成功和全部为空的场景,未验证单个 bundle 失败时其他 bundle 仍能正确恢复。

    • 严重性:MEDIUM
    • 建议:增加测试,让 fake opencode import 对其中一个特定 id 的 bundle 抛出错误,验证成功导入的 bundle 数量正确且失败 bundle 不影响其余 bundle。

阻塞项:

  1. 缺少 serve spawn 失败的回归测试(CRITICAL)

警告项:

  1. 缺少 serve 提前退出的回归测试(MEDIUM)
  2. 缺少部分 bundle 导入失败的回归测试(MEDIUM)

建议项:无

test-value

发现低价值测试 / LOW-VALUE TESTS FOUND

CRITICAL: 无

MEDIUM: 无

LOW:

  1. session-resume.test.ts:231-263 — 缺少对 opencode serve 进程 spawn 错误(bsProc.on("error"))的测试,也缺少对 serve 在就绪前意外退出(bsProc.on("exit"))的测试
  2. session-resume.test.ts:231-263 — "bootstrap 超时后 bundles 仍被导入"的场景只验证了超时警告和 tempDataHome,未验证 ctx.existingSessions.size 是否为 1,无法确认导入确实成功
  3. session-resume.test.ts:265-269 — 仅测试了 null 输入,未覆盖 undefined 和空数组 {bundles: []} 路径
  4. session-resume.test.ts:161-229 — 未覆盖部分 bundle 导入失败时其余 bundle 仍恢复成功的场景(源文件每轮有 try/catch,但测试只有全成功或全失败路径)
  5. session-resume.test.tsopencodeBin 选项从未被测试,该选项专为测试场景设计但未被使用

阻塞项:无

警告项:无

建议项

  1. 缺少 spawn 错误和 serve 提前退出的异常路径覆盖
  2. "bootstrap 超时"测试应补充断言 ctx.existingSessions.size 以确认 fallback 导入实际执行
  3. 补全 undefined / 空数组输入覆盖
  4. 补全部分 bundle 导入失败的恢复路径覆盖

@Svitter Svitter added fix Bug fix review:p1 Major review findings tool Internal tooling and utilities triaged Issue has been triaged labels Jun 28, 2026

@Svitter Svitter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 — clean fix, one convention touch-up.

The root-cause analysis is tight and the two fixes are both minimal and correct: the listening-line bootstrap sidesteps the SQLite-pre-migration race, and the serial import loop kills the migration-DDL collision outright. The new session-resume.test.ts is the strongest part of the change — the per-name release-file handshake gives you a deterministic serial-vs-parallel assertion (no wall-clock assumption), and the fake serve that creates opencode.db instantly but delays the listening line correctly distinguishes the two bootstrap strategies. Temp-revert validation is exactly the right move.

Findings (inline):

  • The listening on https?:// regex is the bootstrap's only contract and a loose substring match; the defensive fallback at line 140 covers the missing-line case but not a misordered one. Worth pinning the phrasing or adding a secondary post-migration sentinel before the next opencode bump.
  • serveOutput is unbounded for the lifetime of the serve process. Minor; cap after the regex check.

One thing for the maintainer to decide:

  • This PR does not add a ## [Unreleased] entry to CHANGELOG.md. PR #286 did (the prior fix that introduced the [Unreleased] section), and the current top of CHANGELOG.md is back to dated-only after 4.2.2. Per the review conventions a fix touching a package should land a [Unreleased] bullet; you can either ask the contributor to add one or fold it into the next release commit yourself. Not blocking the code itself.

Verification already done by the contributor: 151/151 tests pass, dist rebuilt, regex validated against real opencode serve output. The 31 pre-existing bun run check errors on main are out of scope and acceptable to defer.

Thanks for the thorough writeup — the 2/7 repro line and the temp-revert claim in the body are the kind of evidence that makes this a one-glance review.

let serveOutput = "";
const onServeData = (chunk: Buffer | string): void => {
serveOutput += typeof chunk === "string" ? chunk : chunk.toString("utf8");
if (/listening on https?:\/\//i.test(serveOutput)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should-fix / worth a maintainer eye: the /listening on https?:\/\//i regex is the only contract the bootstrap depends on, and it's a free-form substring match. If a future opencode release prints any other "listening on" line (a progress message, a deprecation notice, etc.) before migrations commit, the bootstrap will fire early and the serial-import safeguard is the only thing standing between the user and the old 2/7 failure. The defensive fallback at multi-review/src/session-resume.ts:140 catches the missing-line case but not the misordered-line case. Worth pinning either to a more specific phrasing (e.g. anchor to the opencode version string you tested against) or wiring in a post-migration sentinel as a secondary confirmation. Not blocking, but flag for the next time opencode is bumped.

// no-ops, so every handler can call them unconditionally.
const { promise: readyPromise, resolve: readyResolve, reject: readyReject } =
Promise.withResolvers<void>();
let serveOutput = "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: serveOutput accumulates every byte that opencode serve writes (stdout + stderr) for the lifetime of the process. Once readyResolve fires the regex test is still a no-op cost, but the string keeps growing until the SIGTERM in the finally. On a long-lived or chatty serve this is unbounded. The exit-message truncation (.slice(0, 500)) only matters on the error path. A cap like serveOutput.length > MAX && (serveOutput = serveOutput.slice(-MAX)) after the regex check would bound it without losing the substring property the regex needs.

…output, more tests

Review feedback on #289 addressed:

BLOCKING
- Replace Promise.withResolvers (Node 22+) with a Node 20-compatible
  local helper. The action's engines.node is >=20 and consumer runners
  may still be on Node 20, where Promise.withResolvers throws TypeError.
  Revert the tsconfig lib bump (no longer needed).

WARNINGS
- Add 'error' handlers on serve stdout/stderr streams so a broken pipe
  rejects the ready promise instead of hanging forever.
- Bound captured serve output to the last 4KB (tail is what matters for
  listening-line detection and crash diagnostics; unbounded growth was
  possible if serve logged verbosely before the ready line).

TESTS (regression-test CRITICAL/MEDIUM + test-value gaps)
- serve spawn failure (missing binary) → no throw, warning, empty map.
- serve crash before listening line (exit 1) → warning, fallback path.
- partial bundle failure → per-bundle try/catch isolates; rest succeed.
- bootstrap-timeout test now asserts existingSessions.size === 1
  (proves the fallback import actually ran, not just that we warned).
- empty-input test covers null, undefined, and empty bundles array.
- fake serve's keepalive setInterval now calls unref() so it can't pin
  the test process exit.

154/154 tests pass; session-resume files typecheck clean.
@github-actions

Copy link
Copy Markdown

✅ 可合并 / CAN MERGE

所有 6 位 reviewer 均同意合并,上一轮指出的所有问题已在本轮修复(Promise.withResolvers() 兼容性、serveOutput 上限截断、流错误处理、异常路径测试覆盖等)。

🟢 建议项 / Suggestions (1)

  • bundle 导入添加超时保护 — performance reviewer 指出每个 bundle 的 opencode import 缺少超时保护(如 30s),一个挂起的导入会阻塞后续所有串行导入的进度,建议后续添加。
💰 Review Cost — $0.0000
Role Cost (USD) Input Output Reasoning Cache Read Cache Write
quality $0.0000 71,370 157 632 0 0
security $0.0000 71,379 114 734 0 0
performance $0.0000 71,382 159 1,572 0 0
architecture $0.0000 71,383 286 645 0 0
regression-test $0.0000 71,820 176 707 0 0
test-value $0.0000 71,510 60 499 0 0
coordinator $0.0000 9,207 159 246 0 0
Total $0.0000 438,051 1,111 5,035 0 0
📋 各 Reviewer 详细审查结果
quality

可合并 / CAN MERGE

本次重审确认前一版所有问题均已修复:

  • Promise.withResolvers() 替换为自定义 withResolvers() 函数,兼容 Node 20
  • serveOutput 添加了 4096 字节上限截断
  • 子进程 stdout/stderr 添加了 "error" 事件处理
  • 新增了 spawn 失败、serve 崩溃、部分 bundle 导入失败等异常路径测试
  • tsconfig.json"lib": ["ES2024"] 已移除(不再需要)

阻塞项:无

警告项:无

建议项:无

security

安全无虞 / SECURE

本次 re-review 的改动(自定义 withResolvers() 替代 Promise.withResolvers()MAX_SERVE_OUTPUT 上限、流 error 处理器、测试补充)不涉及安全相关的代码路径变更,未引入新的安全风险。

阻塞项:无

警告项:无

建议项:无

performance

性能良好 / GOOD

本版本修复了上一轮指出的所有性能问题:serveOutput 已设置 4096 字节上限(appendServe + 滑动截断),流错误处理已添加,手工 withResolvers 替代了 Promise.withResolvers。串行导入是正确性必需的折衷,bundle 数量(通常 < 10)下总耗时 < 20s,在可接受范围内。

阻塞项:无

警告项:无

建议项:

  1. 每个 bundle 导入缺少超时保护(如 30s),一个挂起的 opencode import 会阻塞后续所有串行导入的进度,建议后续添加。
architecture

架构合理 / SOUND

本次 re-review 的 diff 相比上一轮有显著改进:之前的架构阻断项(Promise.withResolvers 不兼容 Node 20)已被消除,改用模块级局部函数 withResolvers() 替代。改动集中、边界清晰、设计一致。

  • session-resume.ts:新增的 withResolvers<T>() 工具函数是模块私有实现(不导出),不增加外部耦合。serveOutput 通过 MAX_SERVE_OUTPUT = 4096 边界控制防止无界增长,属于合理的内部资源管理。stdout/stderr 的 stream error 处理补充完整。bootstrap/import 串行化逻辑保持清晰分层,与 orchestrator 的接口未变。

  • 增加的测试覆盖了之前缺失的异常路径(spawn 失败、serve 崩溃、部分 bundle 导入失败、empty/null/undefined 输入),且测试代码使用 marker/gate 文件机制实现确定性断言,架构上优秀。

  • dist/index.cjs 的变更同步了上述修复。package.json 仅追加了测试文件路径。

阻塞项:无

警告项:无

建议项:无

regression-test

回归测试完整 / REGRESSION TESTS COMPLETE

PR类型:BUGFIX

上一轮评审指出的回归测试缺口已全部补充:

  • opencode serve spawn 失败(不存在的 binary)的测试已添加
  • opencode serve 就绪前退出的测试已添加
  • ✅ 部分 bundle 导入失败时其余 bundle 仍恢复成功的测试已添加
  • ✅ bootstrap 超时测试补充了 existingSessions.size 断言
  • ✅ null / undefined / 空数组的边界输入已覆盖
  • opencodeBin 选项已在 spawn 失败测试中使用
  • ✅ fake script 中 setInterval 已添加 unref()

阻塞项:无

警告项:无

建议项:无

test-value

测试全部有价值 / ALL TESTS HAVE VALUE

CRITICAL: 无

MEDIUM: 无

LOW: 无

阻塞项:无

警告项:无

建议项:无

@Svtter
Svtter merged commit 469dd93 into main Jun 28, 2026
3 checks passed
@Svtter Svtter mentioned this pull request Jun 28, 2026
Svtter added a commit that referenced this pull request Jun 28, 2026
### Fixed
- multi-review: fix v2 session-resume restoring only a fraction of
  bundles on PRs with many reviewers. Two compounding bugs in
  session-resume.ts caused 2/7 bundles restored on 7-reviewer PRs,
  empty reviewer output, and forced CANNOT MERGE. PR #289.
@Svtter
Svtter deleted the fix/v2-resume-migration-race branch June 28, 2026 03:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix review:p1 Major review findings tool Internal tooling and utilities triaged Issue has been triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants