Repository navigation
CI infra: @objectstack/spec 的 DTS 构建贴着 runner 内存天花板 —— 每个 spec PR 首跑都被 OOM 杀掉一次(--max-old-space-size=12288 on a 16GB runner) #4845
Description
Activity
第三次复现:PR #4846(
ef8c17e3),同一步、同一签名job 91652570174 ——
Test Core (1/2)。三个内容毫不相干的 spec PR 连续命中,进一步坐实这不是任一 PR 的内容问题:
PR 内容性质 结果 #4831 纯删除(退役两个键 + 一个 def) 首跑 OOM,重跑转绿 #4841 wire 形状迁移(实现改形) 首跑 OOM #4846 纯文案纠错(patch,只改散文与注释) 首跑 OOM 第三例尤其说明问题:#4846 一行运行时代码都没改,只改墓碑文案、注释与再生成的文档,
packages/spec的类型图与 v17 之前逐字节等价。它同样在 DTS 那一步被杀,说明触发条件与改动内容无关 —— 只要 turbo 缓存失效、真正跑一次@objectstack/spec的 DTS 构建,就有相当概率越界。即:命中率取决于「是否跑这一趟」,不取决于「改了什么」。这与正文分析(12 GB 堆上限 vs 16 GB runner,V8 被告知可涨到 12 GB 就不积极 GC)一致。
三例首跑全中,重跑可过 —— 边际性、高频。协议线还有 #4001 / #4391 两个 spec 单在飞,预计同样命中。
Generated by Claude Code
⬆️ 升级:第四次复现,且发生在合并队列里 —— 从「多烧一轮 CI」变成「PR 反复被踢出队列」
merge-queue run 30803140986(
gh-readonly-queue/main/pr-4831-…),12 个 job 里只有Test Core (1/2)失败,其余全绿(Test Core 2/2、Build Core、Build Docs、Console Pin Gate、Dogfood ×2 + gate、Temporal Conformance、filter)。失败 job 91652260560 日志尾部,签名与前三次逐字一致:
CJS ⚡️ Build success in 18085ms CLI Building entry: src/index.ts, …(16 entries) DTS Build start ELIFECYCLE Command failed. ##[error]command (…/packages/spec) …/pnpm run test exited (1) Tasks: 3 successful, 5 total Time: 1m42.997s … check-test-completeness: OK (3 package(s), 7382 test(s) declared and all 7382 accounted for).耗时 1m43s,与前三次(1m39s / 1m40s / 1m40s)一致;测试阶段完整跑完、零遗漏;
DTS Build start后零 tsc 诊断。为什么这次要升级严重度
前三次都是 PR 上的 CI,重跑就能过(#4831 attempt 2 已实证)。但合并队列会对合并结果重新跑一遍,这次就栽在这里 —— 结果是 PR 被
CI_FAILURE踢出队列。重跑 PR 的 CI 帮不上忙:队列每次消化都是一次新的抽签,而每次没中就再踢出一次。四次命中记录:
# 场景 PR / run 内容性质 结果 1 PR CI #4831 纯删除 OOM → 重跑转绿 2 PR CI #4841 wire 形状迁移 OOM 3 PR CI #4846 patch,零运行时代码改动 OOM 4 merge queue #4831 同 #1 OOM → 踢出队列 第 3 例说明命中与改动内容无关;第 4 例说明它能实际阻止合并。
现状与影响
已重新入队 #4831。但 v17 窗口内协议线还有 #4846、#4841、#4391 三个 spec PR 要过同一道队列,加上 #4001 战役后续多批 —— 按目前命中率,每单都可能反复被踢。
建议把本单的处置提前:正文给的三个方向里,最小改动是把
packages/spec/package.json里 DTS 那趟的--max-old-space-size=12288降到与 16 GB runner 相称的数值(V8 会因此更早开始积极 GC,而不是一路涨到被内核杀掉)。仍建议先量一次真实峰值 RSS 再定数字,但如果要快,8192 是个保守且大概率有效的起点(留 8 GB 给堆外与系统)。本 PM 会话未自行改该配置 —— 它影响全仓所有 PR,且同时段另一个 PM 会话也在推 PR 过同一道队列,已上报维护者裁决。
Generated by Claude Code
⛔ 更正:本单的 OOM 诊断是错的,已被自己的修复 PR 证伪
PR #4853 把 DTS 堆上限降到 8192 后,它自己的 CI 以完全相同的方式失败。修复无效 ⇒ 前提不成立。已把 #4853 退回 draft 并关掉 auto-merge。
以下是重查后确证与推翻的部分,写清楚免得下一个人照着我的错结论再走一遍。
被推翻的两条
1. 「
DTS Build start紧接ELIFECYCLE⇒ DTS 被 OOM 杀掉」—— 这是误读因果。turbo.json里test的依赖是dependsOn: ["^build"](上游依赖的 build),packages/spec没有pretest,其test就是vitest run。所以日志里 spec 自己的gen:schema/tsup/DTS是另一个并发任务@objectstack/spec#build(由 shard 上依赖 spec 的包触发)的输出。CI 用--concurrency=4,turbo 把多个任务的 stdout 交织在一起,而 GitHub 又把整个 group 用同一个时间戳一次性 flush —— 相邻不等于因果。真正失败的是@objectstack/spec#test,日志自己写得很清楚:command (…/packages/spec) …/pnpm run test exited (1)。2. 「测试全绿,只是构建挂了」—— 我把 completeness 的语义读反了。
scripts/check-test-completeness.mjs的注释原文:它解析 vitest 的汇总行,防的是「worker 进程级死掉导致部分用例从未运行、而汇总仍以 passed 开头」。all N accounted for只表示没有用例被静默吞掉,完全不表示全部通过。工作流自己的注释更直接:Runs even when the suite failed — that is when it earns its keep. A red suite plus a GREEN completeness check means real test failures
我此前把这行读成「测试全绿」,并据此断定失败在构建 —— 这是这次误诊的起点。
确证的两条
3. 本地同分支、用 CI 的同一条命令,测试全过。
$ pnpm turbo run test --filter=@objectstack/spec --concurrency=4 # 在 #4853 分支上 Test Files 294 passed (294) Tests 7362 passed (7362) Tasks: 1 successful, 1 total turbo rc=04. DTS 构建本身在 8192 下健康,峰值 RSS 实测 6.29 GB(15 GB 机器,与 runner 同量级)。所以 12288 确实高于真实需求 —— 但这不是这些 PR 变红的原因。
现在的结论
CI 里
@objectstack/spec有真实的测试失败,且本地复现不出来 —— 是环境相关的。 具体是哪个用例,我拿不到:GitHub 的 job log API 只回约 10 KB 的尾部,而尾部被gen:schema的 1675 行 schema 清单占满,vitest 的失败明细在更靠前的位置被截断了。下一步该做的是拿到完整日志(下载 run 的日志归档,或在 workflow 里让失败摘要落到尾部/单独 artifact),而不是继续对着截断的尾部猜。
一个未经证实的怀疑方向,供接手的人参考、不要当结论:
scripts/build-schemas-check-mode.test.ts(本地 12 个用例跑 77 秒)里有多条显式依赖 git 状态的断言 —— 「the anchor is the merge base with origin/main, not HEAD」「fails LOUDLY when origin/main cannot be resolved」。CI 的 merge-base 与本地 worktree 不同。但本地在 main 已领先分支的状态下这些用例是过的,所以这条线索本身也有反证,不能直接采信。对 #4853 的处置
改动本身(12288 → 8192)仍有独立依据 —— 真实需求 6.29 GB,上限高于物理内存确实会把可诊断的 V8 heap OOM 变成不可诊断的内核 SIGKILL。但它不修本单,所以已退回 draft、摘掉
Fixes #4845,是否单独作为一个小改进合入由维护者定。对之前四条「命中记录」的更正
那张表里的四例仍然都是真实的红,但归因是错的 —— 它们不是同一个 OOM,而是同一个尚未定位的测试失败。特此更正,以免这张表被当成 OOM 的证据链继续引用。
Generated by Claude Code
PR #4853 已关闭 —— 把测量留在这里,免得连同错误结论一起丢掉
维护者关掉了 #4853(把 DTS 堆上限 12288 → 8192)。那个 PR 的结论是错的、且已被它自己的 CI 推翻,但它做的那次测量是对的,值得单独留档 —— 下次真有人撞到
packages/spec的构建内存问题时,不必从头再量一遍。实测数据(与 CI runner 同量级的机器:15 GB 物理 / 可用 13 GB)
项 读数 packages/specDTS 那一趟的峰值 RSS6.29 GB 当时的堆上限 --max-old-space-size=8192(实验值)结果 构建成功完成 main 上现行上限 12288(packages/spec/package.json的build脚本)CI runner ubuntu-latest,16 GB仍然成立的那部分推理
--max-old-space-size是允许 V8 涨到多少的上限,不是预留。12288 相对 6.29 GB 的真实需求高出近一倍,意味着 V8 在逼近 12 GB 前都不会积极 GC;Test Core又是turbo run test --concurrency=4,并发下总量理论上可以逼近物理内存。所以「把上限降到 8192」作为预防性卫生是有依据的,8 GB 相对实测需求留约 27% 余量。⛔ 已被推翻、不要再引用的那部分
我曾据此断定今晨四次 CI 红是内核 OOM-killer 杀掉 DTS 进程。这是错的。 #4853 自己挂着 8192 跑,红得一模一样 —— 同一个位置、同一个签名。
真因是 #4796 那一族:13 个 compiler-API 导出面 pin 贴着 vitest 默认 5000ms,CI 上 turbo 把
spec#build与spec#test并发到同 4 vCPU,把它们推过 5.0s。修复是 #4856 的testTimeout: 60_000(现在在packages/spec/vitest.config.ts:15)。我当时叠加犯了三个错,记在这里供后来者对照:
- 把
check-test-completeness.mjs的 “all N accounted for” 读成「测试通过」。它只断言没有 worker 静默死掉,workflow 自己的注释写得很清楚:“A red suite plus a GREEN completeness check means real test failures”。 - 把 turbo 并发输出的相邻当成因果。
test的dependsOn只有["^build"](上游),packages/spec没有pretest,所以spec#build(DTS)和spec#test在--concurrency=4下同时跑,GitHub 又给整组打同一个时间戳 ——DTS Build start紧接着ELIFECYCLE完全可能来自两个无关的进程。 - 从日志尾巴下结论。那 ~10 KB 的 tail 被
gen:schema的 1675 行清单吃光了,真正的失败行根本不在里面。下完整日志归档才看见。
后续
如果哪天真要降这个上限:改动本身是一个 token(
packages/spec/package.json的build脚本),依据就是上面那张表。但要写实话 —— 它不修任何已观测到的故障,是预防性的。别再把它和这个 issue 里的 OOM 叙事绑在一起。
Generated by Claude Code
- 把
重新定性:本单的可派发余量只剩「预防性堆上限」,不是 OOM 修复 —— 建议改为决策位
分诊(会话
session_01FTszibd6C8sUCCZnM4VcrL,v17 D 组排查)。本单被 D 组列为「spec DTS 构建贴 OOM 天花板,v17 链每个 spec PR 首跑被杀一次」,但该定性来自已被本单自己推翻的诊断,不能照原样派发。核验结果:1. 真因已在别处修复,今天仍在 main 上。 本单 10:25Z / 13:30Z 两条更正确认真因是 #4796 一族的 5000ms 超时,修复是
testTimeout: 60_000。实测origin/main:packages/spec/vitest.config.ts:15: testTimeout: 60_000,修复健在。「每个 spec PR 首跑被杀一次」这个 D 组描述不再有观测支撑 —— 它继承自被推翻的 OOM 叙事。
2. 堆上限确实还是 12288,但它不修任何已观测故障。 实测
origin/main:packages/spec/package.json:185: "build": "... NODE_OPTIONS=\"--max-old-space-size=12288\" BUILD_DTS=true tsup; fi"而 13:30Z 评论已明确要求接手者「写实话」:降它是预防性的,别再和 OOM 叙事绑定。同时 10:25Z 记录了实测数据 —— 8192 下 DTS 峰值 RSS 6.29GB,构建健康。也就是说 12288 这个数字在 16GB runner 上确实没有余量意义(峰值离 8192 都还有 1.7GB),但把它降下来修的是风险,不是故障。
3. 为什么不由 PM 直接拍板派发。 按 skill 的升级标准,这本该是 PM 的判断(hygiene 类改动),但有一条硬性反例:#4853 就是这个改动,它被维护者关掉了 —— 关闭理由是「结论错误且被自己的 CI 推翻」。在同一文件、同一数值上重开一个 PR,而唯一的新论据是「这次我们承认它不修故障」,需要维护者知情。这是「重开一个被维护者关闭过的改动」,不是常规 hygiene。
给维护者的两个选项(两轴分析)
A. 关闭本单(not planned) —— 根因已在 #4796/#4856 修复,预防性调参无观测需求。
- 项目长远合理性:诚实的账 —— 一个标题与正文均已自证伪的 issue 留在 v17 工作集里,会让下一个读 D 组清单的人继续相信「spec PR 首跑被杀」。关掉它,把预防性调参需要时另开干净的单,比留一个带错误叙事的壳更符合台账纪律。
- 防 AI 犯错:留着它的最大风险恰恰是 AI 复现路径 —— 下一个 agent 读标题「贴 OOM 天花板」就会去改 12288,重走 chore(spec): DTS 堆上限 12288 → 8192(实测峰值 6.29 GB)—— ⚠️ 原「修 #4845」的前提已被本 PR 自己证伪 #4853 被推翻的路。本轮排查中确实差点这么派。
B. 改标题与正文后派发一行改动(12288 → 8192) —— 明确定性为预防性。
- 项目长远合理性:12288 在 16GB runner 上是把上限设到物理内存的 75%,实测峰值只有 6.29GB;留着它意味着未来 spec 体积增长时,第一个撞上的不是清晰的 heap OOM 而是内核 OOM-killer(无诊断信息)。降到 8192 让失败模式从「静默被杀」变成「V8 报 heap out of memory」——可读的失败优于沉默的失败,这条本身站得住。
- 防 AI 犯错:需要在 PR 与 changeset 里写死「本改动不修任何已观测故障」,否则这个数值变更会被后来者当作某次事故的修复来引用,错误叙事就借尸还魂了。
我的建议:B,但要求把 issue 标题与正文一并改写(标题去掉 OOM 定性,正文首段就写清「原诊断已证伪,本单只剩预防性调参」)。理由:8192 的可读失败论据独立于被推翻的叙事,自身成立;而只要标题还写着 OOM,选 A 关掉也不能阻止下一个人从 D 组清单里再把它翻出来 —— 改写正文才是真正拆掉复现路径的动作。若维护者认为发版期不该动全仓构建参数,A 同样干净,本会话即刻执行。
已挂
needs-user-decision,退出本轮派发队列。⛔ 本会话不会在无裁决的情况下派任何人改这个数值。
Generated by Claude Code
处置(2026-08-06):关闭(not planned)——前提已死。
原「spec DTS 构建贴 12GB 堆上限被 OOM 杀」的诊断已被本单评论链完整证伪(三层错误:completeness check 绿≠测试过、turbo 并发输出相邻≠因果、日志 tail 被 gen:schema 清单吃掉);真因是 #4796 族 5000ms 超时,已由 #4856 的
testTimeout: 60_000修复并在 main(packages/spec/vitest.config.ts:15)。维护者已关闭对应 PR #4853。留此单的最大风险是下一个 agent 读标题重走 #4853 老路(上一轮已差点发生)。预防性堆上限调整(12288→8192)若将来需要,请另开干净单,并引用本单 2026-08-04 评论中的 6.29GB 峰值 RSS 实测数据——测量保留,错误叙事拆除。
经办:PM 会话
session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。
Generated by Claude Code
【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:
裁 B——改写标题与正文后派一行预防性改动:改写必须写明「原 OOM 叙事已被 PR #4853 证伪、真因(#4796 族测试超时)已由 #4856 修复且健在于 main,本单仅剩预防性降堆
--max-old-space-size12288→8192(实测 DTS 峰值 RSS 6.29GB),不修任何已观测故障」——防止后续 agent 从 D 组清单翻出重走 #4853 老路。同时摘
target:v17(名不副实:实际故障已修,不挡发布)。流转:摘
needs-user-decision、target:v17→pm:queue(改写正文 + 一行改动,归 devx/ci 面承接)。评估与落地会话:
session_01N3uGFF8teXbpgtbEJ1aYXu
Generated by Claude Code
协议线 PM 会话在 v17 窗口连推 spec PR 时观测到的高频基建 flake,不是任何一个 PR 的内容问题。只记录证据与分析,不自行改 CI 配置(影响面是全仓所有 PR,且同时段有另一个 PM 会话在跑)。
现象:两个内容完全无关的 PR,同一步、同一签名
4f136c55)Build success in 17882msDTS Build start→ELIFECYCLE Command failed.7358 test(s) declared and all 7358 accounted forf3e2f404)Build success in 18043msDTS Build start→ELIFECYCLE Command failed.7362 test(s) declared and all 7362 accounted for两者都是
Test Core (1/2),耗时同为 ~1m40s。关键特征:DTS Build start的下一行直接是ELIFECYCLE。这是进程被 SIGKILL 的签名(编译错误会打印诊断,内存耗尽的 V8 会打印 heap OOM 堆栈;两者都没有 = 被内核 OOM-killer 杀掉)。check-test-completeness在两个 job 里都报 OK,说明测试阶段完整跑完,失败纯在构建。rerun_failed_jobs后转绿并进入合并队列。故是边际性问题,不是确定性失败。分析:堆上限设得比 runner 物理内存还接近
packages/spec/package.json:tsup.config.ts里的注释写着// Generate DTS separately to avoid memory issues—— 拆开单跑本身是对的处置,但那一趟给了 12288 MB(12 GB) 的老生代上限。.github/workflows/ci.yml的Test Core是runs-on: ubuntu-latest,公开仓标准 runner 为 4 vCPU / 16 GB。--max-old-space-size是告诉 V8 可以涨到多少,不是预留。设成 12 GB 意味着 V8 在逼近 12 GB 之前不会激进 GC;叠加 tsup/tsc 自身的堆外内存、pnpm/turbo 进程与系统占用,总量越过 16 GB 物理内存时,内核先于 V8 的 OOM 处理把进程杀掉 —— 于是没有任何 Node 侧的错误输出。也就是说:这个设置在 16 GB runner 上不是防 OOM,而是 OOM 的成因之一。随着 spec 类型图增长,越界从偶发变成了几乎每次首跑必现。
为什么现在才密集出现
turbo对@objectstack/spec#build有缓存,只有改动 spec 的 PR 才真正跑这一趟。v17 窗口内 spec PR 密集,于是暴露频率陡增。非 spec PR(如 #4823)命中缓存,完全看不到这个问题。可能的处置方向(供维护者裁决,未实施)
--max-old-space-size=8192甚至 6144),让 V8 在越过物理内存前先积极 GC —— 反直觉但通常正是修法;runs-on: ubuntu-latest-8-cores之类,若组织有配额);任一方向都建议先复现再改:在 CI 上跑一次带
/usr/bin/time -v或--max-old-space-size探测的 job,拿到真实峰值 RSS,再定数字 —— 否则又是一次凭猜调参。影响
不阻塞合并(重跑可过),但让每个 spec PR 多花一轮 CI,在发布窗口期是实打实的节流。协议线目前还有 #4756 / #4709 / #4001 / #4391 四个 spec 单在飞,预计都会命中。
关联:#4831、#4841、#4796(另一个已知 flaky,
spec/src/cloud/tenant.test.ts5s 超时,与本条无关)。Generated by Claude Code — session
session_0176qgxgCXTJCUv4YFLtusP9