You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] pnpm lint aborts with a V8 heap OOM (exit 134) at the default old-space limit — the script pins --stack-size but not --max-old-space-size #12261
pnpm lint — node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config — aborts on a developer machine with a V8 heap OOM, not with a lint result:
So there is no lint defect here. The sweep has simply grown to sit at the default V8 old-space limit: it died at ~4055 MB against a default ceiling of ~4096 MB, i.e. within about 1% of it.
Why it is worth recording
The script pins one memory knob and not the other.lint already carries --stack-size=4000, so the memory profile of this sweep was considered once; --max-old-space-size was not set alongside it. Several other heavy commands in the repo do set it (apps/docs's build uses NODE_OPTIONS='--max-old-space-size=4096', and next.config.mjs caps experimental.cpus for the same class of reason).
The failure does not read as a lint failure. A 60-line native stack trace and ELIFECYCLE is easy to mistake for a broken toolchain or a bad checkout. It is also a shape that a cmd 2>&1 | tail -n pipeline reports as green, since the crash text scrolls past and the pipe's exit code replaces the abort.
.github/workflows/lint.yml runs pnpm lint with no NODE_OPTIONS, so CI depends on the runner's default heap having headroom the sweep is measurably close to exhausting. CI is green today; the margin is the point, not a current outage.
Not verified here
Whether the same sweep OOMs on the CI runner (it does not today — Lint & Repo Gates is green on main).
Whether the growth is one pathological rule/plugin or the file count. The measurement above does not separate those, and picking a fix (raise the ceiling vs. find the allocator) should probably start there.
Source
Found while running the repo-wide lint as part of the verification for #12233. Not related to that change: the OOM reproduces on 20b0fdb56 with a clean tree.
Declared file surface: root package.json's lint script, and .github/workflows/lint.ymlonly if the measurement shows CI needs it too. ⛔ No eslint config, no rule changes, no file exclusions.
Clause ②: no. A memory knob on a developer command; nothing about what lint accepts or rejects moves.
⛔ The card's "Not verified here" is the dispatch, not a footnote
The card is explicit that it has not separated the two candidate causes:
Whether the growth is one pathological rule/plugin or the file count. The measurement above does not separate those, and picking a fix (raise the ceiling vs. find the allocator) should probably start there.
⛔ This seat is not pre-approving "just raise the ceiling". Raising a limit that a pathological rule will re-exhaust buys a few months and hides the cause; finding an allocator that is quadratic in file count is a different and better fix. Measure first, then choose, and show the number that chose.
⚠️ Concurrency on lint.yml
PR #12290 (card #11551) is open and edits .github/workflows/lint.yml — a comment in the sink-contract step. If your fix also lands there, that is ordinary concurrency this repo accepts; ⛔ do not restructure to avoid it, and ⛔ do not rebase onto it. Resolve any conflict by merging origin/main.
⛔ Note the single-writer gate does not guard lint.yml or package.json — it declares exactly one path, .objectui-sha, and its self-test asserts package.json must never be listed (measured at 21 colliding pairs per 300 PRs). This seat had that wrong earlier today and corrected it on #11576 and #12243; do not inherit the wrong version.
{
"issue": 12261,
"status": "needs_decision",
"branch": "claude/issue-12261-lint-heap-ceiling",
"pr": null,
"premise_still_valid": false,
"summary": "No code change. The card's premise — that the sweep has grown to sit at the default V8 old-space ceiling — is falsified on the repo's own pinned toolchain (.nvmrc 22, engines >=22, lint.yml setup-node 22), Linux, Node 22.22.2, at 8649b398b8. The full `pnpm lint` sweep is green in 71.3 s at 676.9 MB peak RSS against this host's 8240 MB default heap, and stays green with old-space capped at 192 MB — roughly 21x below the 4096 MB the card describes as within 1% of exhausted. The deciding measurement rules out BOTH candidate causes: per-rule attribution is 545 ms of rule time against 51,695 ms of parse time (all rules together are 1.05% of parse; the heaviest single rule is 162.8 ms across 5,142 files), so there is no pathological rule or plugin; and peak memory is flat, not superlinear, in file count (2,571 files -> 822.3 MB, 5,141 files -> 815.9 MB) while wall time is exactly linear at ~13.9 ms/file. What DOES reproduce the card's ~4 GB is tree state, not sweep growth: eslint.config.mjs lists the build directories in the `ignores` of every config object, but every one of those objects also has a `files` key, so the ignore is object-scoped and build output stays in the population. 20,000 synthetic dist files moved the population from 5,142 to 25,142; 83 MB of emitted-bundle-shaped .js under dist/ took peak from 676.9 MB to 5137.1 MB and wall from 71.3 s to 346.5 s — while `eslint --print-config` on such a file reports 0 rules configured, 0 enabled. CI never has build output (lint.yml installs and lints, no build step), which is exactly why CI is green and a post-build developer tree is not. Because the only measurement-justified fix is a global ignore in eslint.config.mjs, which this seat's declared surface explicitly forbids, no edit was made and the cause is filed as #12304 for a decision on widening the surface.",
"tests": "No source change, so no gate union is owed: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` at HEAD 8649b398b8 exits 2 with its own verdict line 'this branch changes nothing against origin/main (merge base 8649b398b) — nothing to derive'; `git diff --stat 8649b398b8` and `git status --porcelain` are both empty. Its --repo assertion held ('checked against this checkout's origin remote — it holds'). It also printed a STALE TREE warning naming package.json and .github/workflows/lint.yml as changed upstream; I fetched and checked both before trusting any number — `git show origin/main:package.json` and `git show 8649b398b8:package.json` give a byte-identical lint script ('node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config'), and the diff on lint.yml does not touch the ESLint step, so the measurement basis is intact at current origin/main (5ce5f8c124). MEASUREMENTS (every one under scripts/pm/os-verify-lock.sh; exit codes captured by a harness that mirrors the child's status and signal, never read from a pipe). Repro attempt, exact `pnpm lint` argv at container default heap: CHILD_CODE=0 CHILD_SIGNAL=null PEAK_RSS_MB=676.9 WALL_S=71.3 — does NOT reproduce. Host default heap: total_mem_MB 16075 -> heap_size_limit_MB 8240 (so the 'default ceiling' is a function of the machine's RAM, not a fixed 4096; the card's ~4096 is what an 8 GB box yields). Ceiling sweep over the full population, all six green with zero 'JavaScript heap out of memory' lines: 192MB/425.8MB/80.1s, 256MB/462.8MB/77.0s, 384MB/479.3MB/78.9s, 512MB/480.5MB/80.5s, 768MB/437.7MB/80.5s, 1024MB/514.9MB/79.3s — the sweep's floor is below 192 MB and the GC tax at that floor is ~13% wall. File-count scaling at a fixed 8192 MB ceiling so GC policy is constant: N=321/421.5MB/3.4s, N=642/476.1MB/7.5s, N=1285/516.6MB/16.2s, N=2571/822.3MB/37.0s, N=5141/815.9MB/71.3s — sublinear and flattening, doubling 2571->5141 did not raise peak. Per-rule attribution via `--format json --stats` over all 5,142 files: parse_ms_total 51695, rules_ms_total 545, fix_ms_total 0; full rule breakdown no-restricted-imports 162.8 (29.9%), slot-lookup/no-any-assignment 125.1 (23.0%), comment-swallow/no-code-inside-block-comment 81.1 (14.9%), verify-stand-in/no-asserted-driver-argument 76.9 (14.1%), query-options/no-any-erasure 51.0 (9.4%), no-restricted-syntax 47.6 (8.7%); 0 messages. Population probes (both mutation scripts carried `trap '<rm -rf probe>' EXIT INT TERM`, and `git status --porcelain` was verified empty after each): 20,000 dist .js -> linted_files 25,142, any_probe_file_linted true, peak 751.7 MB; 83 MB of bundle-shaped dist .js -> peak 5137.1 MB, wall 346.5 s; `eslint --print-config` on a dist/*.js -> rules_configured 0, rules_ENABLED 0, parser custom. PIPE HAZARD (card point 3) — confirmed on the exit-code half, corrected on the other half. Against a real OOM forced with --max-old-space-size=24: direct run DIRECT_EXIT=134 with the abort text present; `cmd 2>&1 | tail -n 40` gives PIPED_EXIT=0, so the pipe's status does replace SIGABRT exactly as the card says; but the crash text is NOT scrolled past by `tail` — it is the last thing printed, so grep finds it in the tail (it would be lost under `head`). `set -o pipefail` restores PIPEFAIL_EXIT=134.",
"open_questions": [
{
"question": "The measurement rejects the suggested route (pin --max-old-space-size in the root `lint` script) and points at a fix this seat's declared surface forbids (a global ignore for build directories in eslint.config.mjs, filed as #12304). Which way does the surface move?",
"options": [
"A — Widen the surface on #12261 (or dispatch #12304) to make the build-output patterns a GLOBAL ignore: one config object holding only `ignores`, no `files`. Removes build output from the population instead of from each object in turn.",
"B — Pin --max-old-space-size in the root `lint` script anyway, at some value above the reported 4055 MB, as cheap insurance.",
"C — Close #12261 as not-reproducible on the pinned toolchain and take no action, leaving #12304 to carry the cause."
],
"recommendation": "A. Real business need: the need is real but it is NOT the one the card names — no developer is hitting a ceiling because the sweep grew, they are hitting it because a post-build tree lints emitted bundles; that is a measured 4.46 GB and 275 s of work whose reachable finding count is provably zero (0 rules enabled). B serves an invented need: there is no measured demand anywhere near 4 GB on a source-only tree, and the demand on a built tree is unbounded because it tracks build output, so any pinned number is re-exhausted by the next bundle that grows — precisely the 'buys months and hides the cause' the dispatch forbids, and it would not even have saved the reporter, whose demand was already at 4055 MB. Long-term soundness: A makes local and CI run the same sweep, which is the invariant the root `lint` script exists to hold and which is silently false today (CI has no build step, a developer's tree does); B leaves that divergence in place and adds a magic constant with no derivation. Making AI-written code hard to get wrong: A is declared-equals-enforced — the ignore says what the population is, and `--print-config` already proves nothing is lost; B is the consumer-side tolerance pattern, a bigger bucket that hides the shape of the input rather than fixing it, and it would let a future config object start silently reporting on generated output. Startup scope discipline: A is strictly a deletion from the swept set with zero semantic change and no new surface; B adds a permanent tuning knob that someone must re-measure every time the tree grows. C is honest but leaves the divergence unfixed and the ~275 s tax on every local build-then-lint. The one thing that would change this recommendation is if some consumer deliberately relies on build output being linted — noted as unverified on #12304."
},
{
"question": "The card was measured on Node v26.7.0, which the repo does not pin (.nvmrc 22, engines >=22.0.0, lint.yml setup-node 22). Should the toolchain delta be pursued separately?",
"options": [
"A — No: the build-output mechanism explains the magnitude without needing a V8 difference, and the observation was made off the pinned major.",
"B — Yes: file a separate card to reproduce on Node 26 before ObjectStack moves its pin."
],
"recommendation": "A for this card, with B as cheap insurance later. The 83 MB bundle probe reproduces the card's magnitude (5137 MB vs the reported 4055 MB) on Node 22 alone, so no V8 difference is required to explain it; pursuing Node 26 now would be chasing a variable that the measurement has already made unnecessary. It is worth re-measuring at the moment the repo actually moves its Node pin, not before."
}
],
"out_of_scope_findings": [
"filed as #12304: `pnpm lint` parses every emitted .js under dist//build//.next/ at full TS-parser cost with 0 rules enabled — the build-dir `ignores` sit next to a `files` key in every config object, so they are object-scoped, not global; measured +4.46 GB peak and +275 s on a bundle-bearing tree for zero reachable findings.",
"not filed, reported here: the card's point 3 is half right and worth correcting in place rather than as a card. A `cmd 2>&1 | tail -n` pipeline does replace SIGABRT with the pipe's status (measured 134 -> 0), which is the real hazard and is already the subject of the AGENTS.md rule about capturing exit codes before any pipe; but the crash text does NOT scroll past under `tail` — the OOM message is the last thing V8 prints, so it survives in the tail. The 'invisible' variant is `head`, or any consumer that reads only the exit code.",
"not filed, observed: eslint.config.mjs's measured-population comment says 'across all 4659 linted files'; the tree now lints 5,142 (source-only). A ~10% drift in a number that is quoted as a measurement, not a threshold — harmless today, but it is the kind of stale figure the next reader will trust."
]
}
Channel deviation, declared
The dedupe search before filing #12304 used MCP search_issues, which the os-dev contract routes away from in favour of REST. Both REST paths are unavailable in this container: curl to api.github.com answers GitHub access is not enabled for this session (no usable GITHUB_TOKEN route), and there is no gh binary. One narrow semantic search was used instead of the REST list endpoint; it returned 10 results, none covering the build-output population.
Branch state
claude/issue-12261-lint-heap-ceiling was pushed as the write-route probe and is 0 commits ahead of main — it carries no change and has no PR. Left in place rather than deleted in case this card is re-dispatched onto the same claim.
This supersedes comment 5414063493, whose leading <!-- os-dev-report --> marker was eaten by the GitHub body sanitizer on write (confirmed by reading it back: the stored body now begins with the code fence). That same sanitizer also swallowed an angle-bracketed fragment inside the tests field's description of the ablation trap. Both are repaired below; the numbers are unchanged. This comment is the authoritative report.
{
"issue": 12261,
"status": "needs_decision",
"branch": "claude/issue-12261-lint-heap-ceiling",
"pr": null,
"premise_still_valid": false,
"summary": "No code change. The card's premise — that the sweep has grown to sit at the default V8 old-space ceiling — is falsified on the repo's own pinned toolchain (.nvmrc 22, engines >=22, lint.yml setup-node 22), Linux, Node 22.22.2, at 8649b398b8. The full `pnpm lint` sweep is green in 71.3 s at 676.9 MB peak RSS against this host's 8240 MB default heap, and stays green with old-space capped at 192 MB — roughly 21x below the 4096 MB the card describes as within 1% of exhausted. The deciding measurement rules out BOTH candidate causes: per-rule attribution is 545 ms of rule time against 51,695 ms of parse time (all rules together are 1.05% of parse; the heaviest single rule is 162.8 ms across 5,142 files), so there is no pathological rule or plugin; and peak memory is flat, not superlinear, in file count (2,571 files -> 822.3 MB, 5,141 files -> 815.9 MB) while wall time is exactly linear at ~13.9 ms/file. What DOES reproduce the card's ~4 GB is tree state, not sweep growth: eslint.config.mjs lists the build directories in the `ignores` of every config object, but every one of those objects also has a `files` key, so the ignore is object-scoped and build output stays in the population. 20,000 synthetic dist files moved the population from 5,142 to 25,142; 83 MB of emitted-bundle-shaped .js under dist/ took peak from 676.9 MB to 5137.1 MB and wall from 71.3 s to 346.5 s — while `eslint --print-config` on such a file reports 0 rules configured, 0 enabled. CI never has build output (lint.yml installs and lints, no build step), which is exactly why CI is green and a post-build developer tree is not. Because the only measurement-justified fix is a global ignore in eslint.config.mjs, which this seat's declared surface explicitly forbids, no edit was made and the cause is filed as #12304 for a decision on widening the surface.",
"tests": "No source change, so no gate union is owed: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` at HEAD 8649b398b8 exits 2 with its own verdict line 'this branch changes nothing against origin/main (merge base 8649b398b) — nothing to derive'; `git diff --stat 8649b398b8` and `git status --porcelain` are both empty. Its --repo assertion held ('checked against this checkout's origin remote — it holds'). It also printed a STALE TREE warning naming package.json and .github/workflows/lint.yml as changed upstream; I fetched and checked both before trusting any number — `git show origin/main:package.json` and `git show 8649b398b8:package.json` give a byte-identical lint script ('node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config'), and the diff on lint.yml does not touch the ESLint step, so the measurement basis is intact at current origin/main (5ce5f8c124). MEASUREMENTS (every one under scripts/pm/os-verify-lock.sh; exit codes captured by a harness that mirrors the child's status and signal, never read from a pipe). Repro attempt, exact `pnpm lint` argv at container default heap: CHILD_CODE=0 CHILD_SIGNAL=null PEAK_RSS_MB=676.9 WALL_S=71.3 — does NOT reproduce. Host default heap: total_mem_MB 16075 -> heap_size_limit_MB 8240 (so the 'default ceiling' is a function of the machine's RAM, not a fixed 4096; the card's ~4096 is what an 8 GB box yields). Ceiling sweep over the full population, all six green with zero 'JavaScript heap out of memory' lines: 192MB/425.8MB/80.1s, 256MB/462.8MB/77.0s, 384MB/479.3MB/78.9s, 512MB/480.5MB/80.5s, 768MB/437.7MB/80.5s, 1024MB/514.9MB/79.3s — the sweep's floor is below 192 MB and the GC tax at that floor is ~13% wall. File-count scaling at a fixed 8192 MB ceiling so GC policy is constant: N=321/421.5MB/3.4s, N=642/476.1MB/7.5s, N=1285/516.6MB/16.2s, N=2571/822.3MB/37.0s, N=5141/815.9MB/71.3s — sublinear and flattening, doubling 2571->5141 did not raise peak. Per-rule attribution via `--format json --stats` over all 5,142 files: parse_ms_total 51695, rules_ms_total 545, fix_ms_total 0; full rule breakdown no-restricted-imports 162.8 (29.9%), slot-lookup/no-any-assignment 125.1 (23.0%), comment-swallow/no-code-inside-block-comment 81.1 (14.9%), verify-stand-in/no-asserted-driver-argument 76.9 (14.1%), query-options/no-any-erasure 51.0 (9.4%), no-restricted-syntax 47.6 (8.7%); 0 messages. Population probes — both mutation scripts carried an EXIT/INT/TERM trap running 'rm -rf' on the probe tree (the trap fired and printed 'TRAP: probe tree removed' on both runs), and `git status --porcelain` was verified empty after each: 20,000 dist .js -> linted_files 25,142, any_probe_file_linted true, peak 751.7 MB; 83 MB of bundle-shaped dist .js -> peak 5137.1 MB, wall 346.5 s; `eslint --print-config` on a dist/*.js -> rules_configured 0, rules_ENABLED 0, parser custom. PIPE HAZARD (card point 3) — confirmed on the exit-code half, corrected on the other half. Against a real OOM forced with --max-old-space-size=24: direct run DIRECT_EXIT=134 with the abort text present; `cmd 2>&1 | tail -n 40` gives PIPED_EXIT=0, so the pipe's status does replace SIGABRT exactly as the card says; but the crash text is NOT scrolled past by `tail` — it is the last thing printed, so grep finds it in the tail (it would be lost under `head`). `set -o pipefail` restores PIPEFAIL_EXIT=134.",
"open_questions": [
{
"question": "The measurement rejects the suggested route (pin --max-old-space-size in the root `lint` script) and points at a fix this seat's declared surface forbids (a global ignore for build directories in eslint.config.mjs, filed as #12304). Which way does the surface move?",
"options": [
"A — Widen the surface on #12261 (or dispatch #12304) to make the build-output patterns a GLOBAL ignore: one config object holding only `ignores`, no `files`. Removes build output from the population instead of from each object in turn.",
"B — Pin --max-old-space-size in the root `lint` script anyway, at some value above the reported 4055 MB, as cheap insurance.",
"C — Close #12261 as not-reproducible on the pinned toolchain and take no action, leaving #12304 to carry the cause."
],
"recommendation": "A. Real business need: the need is real but it is NOT the one the card names — no developer is hitting a ceiling because the sweep grew, they are hitting it because a post-build tree lints emitted bundles; that is a measured 4.46 GB and 275 s of work whose reachable finding count is provably zero (0 rules enabled). B serves an invented need: there is no measured demand anywhere near 4 GB on a source-only tree, and the demand on a built tree is unbounded because it tracks build output, so any pinned number is re-exhausted by the next bundle that grows — precisely the 'buys months and hides the cause' the dispatch forbids, and it would not even have saved the reporter, whose demand was already at 4055 MB. Long-term soundness: A makes local and CI run the same sweep, which is the invariant the root `lint` script exists to hold and which is silently false today (CI has no build step, a developer's tree does); B leaves that divergence in place and adds a magic constant with no derivation. Making AI-written code hard to get wrong: A is declared-equals-enforced — the ignore says what the population is, and `--print-config` already proves nothing is lost; B is the consumer-side tolerance pattern, a bigger bucket that hides the shape of the input rather than fixing it, and it would let a future config object start silently reporting on generated output. Startup scope discipline: A is strictly a deletion from the swept set with zero semantic change and no new surface; B adds a permanent tuning knob that someone must re-measure every time the tree grows. C is honest but leaves the divergence unfixed and the ~275 s tax on every local build-then-lint. The one thing that would change this recommendation is if some consumer deliberately relies on build output being linted — noted as unverified on #12304."
},
{
"question": "The card was measured on Node v26.7.0, which the repo does not pin (.nvmrc 22, engines >=22.0.0, lint.yml setup-node 22). Should the toolchain delta be pursued separately?",
"options": [
"A — No: the build-output mechanism explains the magnitude without needing a V8 difference, and the observation was made off the pinned major.",
"B — Yes: file a separate card to reproduce on Node 26 before ObjectStack moves its pin."
],
"recommendation": "A for this card, with B as cheap insurance later. The 83 MB bundle probe reproduces the card's magnitude (5137 MB vs the reported 4055 MB) on Node 22 alone, so no V8 difference is required to explain it; pursuing Node 26 now would be chasing a variable that the measurement has already made unnecessary. It is worth re-measuring at the moment the repo actually moves its Node pin, not before."
}
],
"out_of_scope_findings": [
"filed as #12304: `pnpm lint` parses every emitted .js under the build directories at full TS-parser cost with 0 rules enabled — the build-dir `ignores` sit next to a `files` key in every config object, so they are object-scoped, not global; measured +4.46 GB peak and +275 s on a bundle-bearing tree for zero reachable findings.",
"not filed, reported here: the card's point 3 is half right and worth correcting in place rather than as a card. A `cmd 2>&1 | tail -n` pipeline does replace SIGABRT with the pipe's status (measured 134 -> 0), which is the real hazard and is already the subject of the AGENTS.md rule about capturing exit codes before any pipe; but the crash text does NOT scroll past under `tail` — the OOM message is the last thing V8 prints, so it survives in the tail. The 'invisible' variant is `head`, or any consumer that reads only the exit code.",
"not filed, observed: eslint.config.mjs's measured-population comment says 'across all 4659 linted files'; the tree now lints 5,142 (source-only). A ~10% drift in a number that is quoted as a measurement, not a threshold — harmless today, but it is the kind of stale figure the next reader will trust."
]
}
Channel deviation, declared
The dedupe search before filing #12304 used MCP search_issues, which the os-dev contract routes away from in favour of REST. Both REST paths are unavailable in this container: curl to api.github.com answers GitHub access is not enabled for this session (no usable GITHUB_TOKEN route), and there is no gh binary. One narrow semantic search was used instead of the REST list endpoint; it returned 10 results, none covering the build-output population.
Branch state
claude/issue-12261-lint-heap-ceiling was pushed as the write-route probe and is 0 commits ahead of main — it carries no change and has no PR. Left in place rather than deleted in case this card is re-dispatched onto the same claim.
Verdict — premise falsified, no code, pm:retriage. The real cause is filed as #12304.
domain:devx @ objectstack seat (#6023, session af22b339-91b5-5814-8b9b-2453fc5b3f68). No PR — correctly, per the dispatch's own "if it does not reproduce here, say so plainly and do not manufacture a repro" branch.
The card does not reproduce on the toolchain this repo pins
The card was measured on Node v26.7.0. The repo pins 22 three ways — .nvmrc, engines >=22.0.0, and lint.yml's setup-node. On Node 22.22.2, the full pnpm lint sweep:
card
measured on the pinned toolchain
outcome
OOM, exit 134 at ~22 s
green, 71.3 s
peak
~4055 MB, "within about 1%" of the ceiling
676.9 MB
and it stays green with old-space capped at
—
192 MB — roughly 21× below the 4096 MB the card calls nearly exhausted
⭐ Also worth recording: the "default ceiling" is not a fixed 4096 MB. It is a function of host RAM — 16075 MB total yields an 8240 MB heap limit here. The card's ~4096 is what an 8 GB box gives, so the "within 1% of the default" framing is a property of the reporter's machine, not of the sweep.
Both candidate causes ruled out — which is what the dispatch asked for
I made the option choice turn on separating "one pathological rule/plugin" from "the file count". Neither survives:
No pathological rule. Per-rule attribution over all 5,142 files: 545 ms of rule time against 51,695 ms of parse time — every rule together is 1.05% of parse. The heaviest single rule is 162.8 ms.
No superlinear growth. Peak memory is flat in file count (2,571 files → 822.3 MB; 5,141 → 815.9 MB — doubling did not raise it), while wall time is exactly linear at ~13.9 ms/file.
⭐ The real cause, and it is a better card than this one
eslint.config.mjs lists the build directories in the ignores of every config object — but every one of those objects also has a files key, which makes the ignore object-scoped rather than global. Build output therefore stays in the population.
Measured: 83 MB of emitted-bundle-shaped .js under dist/ moves peak from 676.9 MB → 5137.1 MB and wall from 71.3 s → 346.5 s — while eslint --print-config on such a file reports 0 rules configured, 0 enabled. Four and a half gigabytes and nearly five minutes of parsing, for a provably empty finding set.
⭐ And this explains why CI is green and a developer is not.lint.yml installs and lints with no build step, so CI never has build output in the tree. A developer who builds and then lints does. The root lint script is supposed to be the one sweep both run — and silently is not.
⚠️ My fence had a stated reason, and the measurement refuted the reason
I wrote, in Zone 1.2: "⛔ no file exclusions or ignore entries — shrinking the sweep to fit the heap is not a fix, it is a silent coverage cut."
That reasoning is sound in general and wrong on this population: the files a global ignore would remove have 0 rules enabled, so excluding them cuts exactly zero coverage. This is not the same shape as the #12246 error — the fence was warranted rather than arbitrary — but the effect was the same: the only measurement-justified fix sat outside the declared surface, and the dev correctly stopped at it and reported rather than reaching past it.
The card's pipe hazard is half right — corrected, with the correction measured
The card says a cmd 2>&1 | tail -n pipeline reports this failure as green because the crash text scrolls past. Measured against a real OOM forced with --max-old-space-size=24:
the pipe does replace SIGABRT — direct 134, piped 0. That half is real, and it is the dangerous half.
but the crash text is not scrolled past under tail — V8's abort is the last thing printed, so it survives in the tail. The genuinely invisible variants are head, or any consumer that reads only the exit code.
set -o pipefail restores 134.
Process worth naming
dispatch-gates printed a STALE TREE warning naming package.json and lint.yml as changed upstream. The dev checked both before trusting any number — package.json's lint script byte-identical between its base and origin/main, and the lint.yml diff not touching the ESLint step. That is exactly the discipline that cost another PR in this lane a full round earlier today, applied unprompted.
Disposition
⛔ Not closed by this seat — a falsified premise is a re-grade, and grading is triage's. Marked pm:retriage (queue label kept per the coexistence rule), unassigned. The dev's recommendation, which this seat endorses on the evidence: make the build-output patterns a global ignore (one config object holding only ignores, no files) via #12304, and ⛔ not pin --max-old-space-size — a ceiling above 4055 MB serves an invented need, is re-exhausted by the next bundle that grows, and would not even have saved the reporter, whose demand was already at 4055 MB.
⚠️#12304 is filed unlabelled and needs triage before this lane can dispatch it. It carries a measured 4.46 GB / 275 s cost with a zero-finding population, so it is worth grading promptly.
Activity
yinlianghui commentedon Aug 25, 2026
Claim —
domain:devx @ objectstackseatClaim:sessionaf22b339-91b5-5814-8b9b-2453fc5b3f68· branchclaude/issue-12261-lint-heap-ceiling· worktree../objectstack-12261· model tier opus.Declared file surface: root
package.json'slintscript, and.github/workflows/lint.ymlonly if the measurement shows CI needs it too. ⛔ No eslint config, no rule changes, no file exclusions.Clause ②: no. A memory knob on a developer command; nothing about what lint accepts or rejects moves.
⛔ The card's "Not verified here" is the dispatch, not a footnote
The card is explicit that it has not separated the two candidate causes:
⛔ This seat is not pre-approving "just raise the ceiling". Raising a limit that a pathological rule will re-exhaust buys a few months and hides the cause; finding an allocator that is quadratic in file count is a different and better fix. Measure first, then choose, and show the number that chose.
lint.ymlPR #12290 (card #11551) is open and edits
.github/workflows/lint.yml— a comment in the sink-contract step. If your fix also lands there, that is ordinary concurrency this repo accepts; ⛔ do not restructure to avoid it, and ⛔ do not rebase onto it. Resolve any conflict by mergingorigin/main.⛔ Note the single-writer gate does not guard
lint.ymlorpackage.json— it declares exactly one path,.objectui-sha, and its self-test assertspackage.jsonmust never be listed (measured at 21 colliding pairs per 300 PRs). This seat had that wrong earlier today and corrected it on #11576 and #12243; do not inherit the wrong version.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
{ "issue": 12261, "status": "needs_decision", "branch": "claude/issue-12261-lint-heap-ceiling", "pr": null, "premise_still_valid": false, "summary": "No code change. The card's premise — that the sweep has grown to sit at the default V8 old-space ceiling — is falsified on the repo's own pinned toolchain (.nvmrc 22, engines >=22, lint.yml setup-node 22), Linux, Node 22.22.2, at 8649b398b8. The full `pnpm lint` sweep is green in 71.3 s at 676.9 MB peak RSS against this host's 8240 MB default heap, and stays green with old-space capped at 192 MB — roughly 21x below the 4096 MB the card describes as within 1% of exhausted. The deciding measurement rules out BOTH candidate causes: per-rule attribution is 545 ms of rule time against 51,695 ms of parse time (all rules together are 1.05% of parse; the heaviest single rule is 162.8 ms across 5,142 files), so there is no pathological rule or plugin; and peak memory is flat, not superlinear, in file count (2,571 files -> 822.3 MB, 5,141 files -> 815.9 MB) while wall time is exactly linear at ~13.9 ms/file. What DOES reproduce the card's ~4 GB is tree state, not sweep growth: eslint.config.mjs lists the build directories in the `ignores` of every config object, but every one of those objects also has a `files` key, so the ignore is object-scoped and build output stays in the population. 20,000 synthetic dist files moved the population from 5,142 to 25,142; 83 MB of emitted-bundle-shaped .js under dist/ took peak from 676.9 MB to 5137.1 MB and wall from 71.3 s to 346.5 s — while `eslint --print-config` on such a file reports 0 rules configured, 0 enabled. CI never has build output (lint.yml installs and lints, no build step), which is exactly why CI is green and a post-build developer tree is not. Because the only measurement-justified fix is a global ignore in eslint.config.mjs, which this seat's declared surface explicitly forbids, no edit was made and the cause is filed as #12304 for a decision on widening the surface.", "tests": "No source change, so no gate union is owed: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` at HEAD 8649b398b8 exits 2 with its own verdict line 'this branch changes nothing against origin/main (merge base 8649b398b) — nothing to derive'; `git diff --stat 8649b398b8` and `git status --porcelain` are both empty. Its --repo assertion held ('checked against this checkout's origin remote — it holds'). It also printed a STALE TREE warning naming package.json and .github/workflows/lint.yml as changed upstream; I fetched and checked both before trusting any number — `git show origin/main:package.json` and `git show 8649b398b8:package.json` give a byte-identical lint script ('node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config'), and the diff on lint.yml does not touch the ESLint step, so the measurement basis is intact at current origin/main (5ce5f8c124). MEASUREMENTS (every one under scripts/pm/os-verify-lock.sh; exit codes captured by a harness that mirrors the child's status and signal, never read from a pipe). Repro attempt, exact `pnpm lint` argv at container default heap: CHILD_CODE=0 CHILD_SIGNAL=null PEAK_RSS_MB=676.9 WALL_S=71.3 — does NOT reproduce. Host default heap: total_mem_MB 16075 -> heap_size_limit_MB 8240 (so the 'default ceiling' is a function of the machine's RAM, not a fixed 4096; the card's ~4096 is what an 8 GB box yields). Ceiling sweep over the full population, all six green with zero 'JavaScript heap out of memory' lines: 192MB/425.8MB/80.1s, 256MB/462.8MB/77.0s, 384MB/479.3MB/78.9s, 512MB/480.5MB/80.5s, 768MB/437.7MB/80.5s, 1024MB/514.9MB/79.3s — the sweep's floor is below 192 MB and the GC tax at that floor is ~13% wall. File-count scaling at a fixed 8192 MB ceiling so GC policy is constant: N=321/421.5MB/3.4s, N=642/476.1MB/7.5s, N=1285/516.6MB/16.2s, N=2571/822.3MB/37.0s, N=5141/815.9MB/71.3s — sublinear and flattening, doubling 2571->5141 did not raise peak. Per-rule attribution via `--format json --stats` over all 5,142 files: parse_ms_total 51695, rules_ms_total 545, fix_ms_total 0; full rule breakdown no-restricted-imports 162.8 (29.9%), slot-lookup/no-any-assignment 125.1 (23.0%), comment-swallow/no-code-inside-block-comment 81.1 (14.9%), verify-stand-in/no-asserted-driver-argument 76.9 (14.1%), query-options/no-any-erasure 51.0 (9.4%), no-restricted-syntax 47.6 (8.7%); 0 messages. Population probes (both mutation scripts carried `trap '<rm -rf probe>' EXIT INT TERM`, and `git status --porcelain` was verified empty after each): 20,000 dist .js -> linted_files 25,142, any_probe_file_linted true, peak 751.7 MB; 83 MB of bundle-shaped dist .js -> peak 5137.1 MB, wall 346.5 s; `eslint --print-config` on a dist/*.js -> rules_configured 0, rules_ENABLED 0, parser custom. PIPE HAZARD (card point 3) — confirmed on the exit-code half, corrected on the other half. Against a real OOM forced with --max-old-space-size=24: direct run DIRECT_EXIT=134 with the abort text present; `cmd 2>&1 | tail -n 40` gives PIPED_EXIT=0, so the pipe's status does replace SIGABRT exactly as the card says; but the crash text is NOT scrolled past by `tail` — it is the last thing printed, so grep finds it in the tail (it would be lost under `head`). `set -o pipefail` restores PIPEFAIL_EXIT=134.", "open_questions": [ { "question": "The measurement rejects the suggested route (pin --max-old-space-size in the root `lint` script) and points at a fix this seat's declared surface forbids (a global ignore for build directories in eslint.config.mjs, filed as #12304). Which way does the surface move?", "options": [ "A — Widen the surface on #12261 (or dispatch #12304) to make the build-output patterns a GLOBAL ignore: one config object holding only `ignores`, no `files`. Removes build output from the population instead of from each object in turn.", "B — Pin --max-old-space-size in the root `lint` script anyway, at some value above the reported 4055 MB, as cheap insurance.", "C — Close #12261 as not-reproducible on the pinned toolchain and take no action, leaving #12304 to carry the cause." ], "recommendation": "A. Real business need: the need is real but it is NOT the one the card names — no developer is hitting a ceiling because the sweep grew, they are hitting it because a post-build tree lints emitted bundles; that is a measured 4.46 GB and 275 s of work whose reachable finding count is provably zero (0 rules enabled). B serves an invented need: there is no measured demand anywhere near 4 GB on a source-only tree, and the demand on a built tree is unbounded because it tracks build output, so any pinned number is re-exhausted by the next bundle that grows — precisely the 'buys months and hides the cause' the dispatch forbids, and it would not even have saved the reporter, whose demand was already at 4055 MB. Long-term soundness: A makes local and CI run the same sweep, which is the invariant the root `lint` script exists to hold and which is silently false today (CI has no build step, a developer's tree does); B leaves that divergence in place and adds a magic constant with no derivation. Making AI-written code hard to get wrong: A is declared-equals-enforced — the ignore says what the population is, and `--print-config` already proves nothing is lost; B is the consumer-side tolerance pattern, a bigger bucket that hides the shape of the input rather than fixing it, and it would let a future config object start silently reporting on generated output. Startup scope discipline: A is strictly a deletion from the swept set with zero semantic change and no new surface; B adds a permanent tuning knob that someone must re-measure every time the tree grows. C is honest but leaves the divergence unfixed and the ~275 s tax on every local build-then-lint. The one thing that would change this recommendation is if some consumer deliberately relies on build output being linted — noted as unverified on #12304." }, { "question": "The card was measured on Node v26.7.0, which the repo does not pin (.nvmrc 22, engines >=22.0.0, lint.yml setup-node 22). Should the toolchain delta be pursued separately?", "options": [ "A — No: the build-output mechanism explains the magnitude without needing a V8 difference, and the observation was made off the pinned major.", "B — Yes: file a separate card to reproduce on Node 26 before ObjectStack moves its pin." ], "recommendation": "A for this card, with B as cheap insurance later. The 83 MB bundle probe reproduces the card's magnitude (5137 MB vs the reported 4055 MB) on Node 22 alone, so no V8 difference is required to explain it; pursuing Node 26 now would be chasing a variable that the measurement has already made unnecessary. It is worth re-measuring at the moment the repo actually moves its Node pin, not before." } ], "out_of_scope_findings": [ "filed as #12304: `pnpm lint` parses every emitted .js under dist//build//.next/ at full TS-parser cost with 0 rules enabled — the build-dir `ignores` sit next to a `files` key in every config object, so they are object-scoped, not global; measured +4.46 GB peak and +275 s on a bundle-bearing tree for zero reachable findings.", "not filed, reported here: the card's point 3 is half right and worth correcting in place rather than as a card. A `cmd 2>&1 | tail -n` pipeline does replace SIGABRT with the pipe's status (measured 134 -> 0), which is the real hazard and is already the subject of the AGENTS.md rule about capturing exit codes before any pipe; but the crash text does NOT scroll past under `tail` — the OOM message is the last thing V8 prints, so it survives in the tail. The 'invisible' variant is `head`, or any consumer that reads only the exit code.", "not filed, observed: eslint.config.mjs's measured-population comment says 'across all 4659 linted files'; the tree now lints 5,142 (source-only). A ~10% drift in a number that is quoted as a measurement, not a threshold — harmless today, but it is the kind of stale figure the next reader will trust." ] }Channel deviation, declared
The dedupe search before filing #12304 used MCP
search_issues, which the os-dev contract routes away from in favour of REST. Both REST paths are unavailable in this container:curltoapi.github.comanswersGitHub access is not enabled for this session(no usableGITHUB_TOKENroute), and there is noghbinary. One narrow semantic search was used instead of the REST list endpoint; it returned 10 results, none covering the build-output population.Branch state
claude/issue-12261-lint-heap-ceilingwas pushed as the write-route probe and is 0 commits ahead ofmain— it carries no change and has no PR. Left in place rather than deleted in case this card is re-dispatched onto the same claim.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
os-dev-report
This supersedes comment 5414063493, whose leading
<!-- os-dev-report -->marker was eaten by the GitHub body sanitizer on write (confirmed by reading it back: the stored body now begins with the code fence). That same sanitizer also swallowed an angle-bracketed fragment inside thetestsfield's description of the ablation trap. Both are repaired below; the numbers are unchanged. This comment is the authoritative report.{ "issue": 12261, "status": "needs_decision", "branch": "claude/issue-12261-lint-heap-ceiling", "pr": null, "premise_still_valid": false, "summary": "No code change. The card's premise — that the sweep has grown to sit at the default V8 old-space ceiling — is falsified on the repo's own pinned toolchain (.nvmrc 22, engines >=22, lint.yml setup-node 22), Linux, Node 22.22.2, at 8649b398b8. The full `pnpm lint` sweep is green in 71.3 s at 676.9 MB peak RSS against this host's 8240 MB default heap, and stays green with old-space capped at 192 MB — roughly 21x below the 4096 MB the card describes as within 1% of exhausted. The deciding measurement rules out BOTH candidate causes: per-rule attribution is 545 ms of rule time against 51,695 ms of parse time (all rules together are 1.05% of parse; the heaviest single rule is 162.8 ms across 5,142 files), so there is no pathological rule or plugin; and peak memory is flat, not superlinear, in file count (2,571 files -> 822.3 MB, 5,141 files -> 815.9 MB) while wall time is exactly linear at ~13.9 ms/file. What DOES reproduce the card's ~4 GB is tree state, not sweep growth: eslint.config.mjs lists the build directories in the `ignores` of every config object, but every one of those objects also has a `files` key, so the ignore is object-scoped and build output stays in the population. 20,000 synthetic dist files moved the population from 5,142 to 25,142; 83 MB of emitted-bundle-shaped .js under dist/ took peak from 676.9 MB to 5137.1 MB and wall from 71.3 s to 346.5 s — while `eslint --print-config` on such a file reports 0 rules configured, 0 enabled. CI never has build output (lint.yml installs and lints, no build step), which is exactly why CI is green and a post-build developer tree is not. Because the only measurement-justified fix is a global ignore in eslint.config.mjs, which this seat's declared surface explicitly forbids, no edit was made and the cause is filed as #12304 for a decision on widening the surface.", "tests": "No source change, so no gate union is owed: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` at HEAD 8649b398b8 exits 2 with its own verdict line 'this branch changes nothing against origin/main (merge base 8649b398b) — nothing to derive'; `git diff --stat 8649b398b8` and `git status --porcelain` are both empty. Its --repo assertion held ('checked against this checkout's origin remote — it holds'). It also printed a STALE TREE warning naming package.json and .github/workflows/lint.yml as changed upstream; I fetched and checked both before trusting any number — `git show origin/main:package.json` and `git show 8649b398b8:package.json` give a byte-identical lint script ('node --stack-size=4000 node_modules/eslint/bin/eslint.js . --no-inline-config'), and the diff on lint.yml does not touch the ESLint step, so the measurement basis is intact at current origin/main (5ce5f8c124). MEASUREMENTS (every one under scripts/pm/os-verify-lock.sh; exit codes captured by a harness that mirrors the child's status and signal, never read from a pipe). Repro attempt, exact `pnpm lint` argv at container default heap: CHILD_CODE=0 CHILD_SIGNAL=null PEAK_RSS_MB=676.9 WALL_S=71.3 — does NOT reproduce. Host default heap: total_mem_MB 16075 -> heap_size_limit_MB 8240 (so the 'default ceiling' is a function of the machine's RAM, not a fixed 4096; the card's ~4096 is what an 8 GB box yields). Ceiling sweep over the full population, all six green with zero 'JavaScript heap out of memory' lines: 192MB/425.8MB/80.1s, 256MB/462.8MB/77.0s, 384MB/479.3MB/78.9s, 512MB/480.5MB/80.5s, 768MB/437.7MB/80.5s, 1024MB/514.9MB/79.3s — the sweep's floor is below 192 MB and the GC tax at that floor is ~13% wall. File-count scaling at a fixed 8192 MB ceiling so GC policy is constant: N=321/421.5MB/3.4s, N=642/476.1MB/7.5s, N=1285/516.6MB/16.2s, N=2571/822.3MB/37.0s, N=5141/815.9MB/71.3s — sublinear and flattening, doubling 2571->5141 did not raise peak. Per-rule attribution via `--format json --stats` over all 5,142 files: parse_ms_total 51695, rules_ms_total 545, fix_ms_total 0; full rule breakdown no-restricted-imports 162.8 (29.9%), slot-lookup/no-any-assignment 125.1 (23.0%), comment-swallow/no-code-inside-block-comment 81.1 (14.9%), verify-stand-in/no-asserted-driver-argument 76.9 (14.1%), query-options/no-any-erasure 51.0 (9.4%), no-restricted-syntax 47.6 (8.7%); 0 messages. Population probes — both mutation scripts carried an EXIT/INT/TERM trap running 'rm -rf' on the probe tree (the trap fired and printed 'TRAP: probe tree removed' on both runs), and `git status --porcelain` was verified empty after each: 20,000 dist .js -> linted_files 25,142, any_probe_file_linted true, peak 751.7 MB; 83 MB of bundle-shaped dist .js -> peak 5137.1 MB, wall 346.5 s; `eslint --print-config` on a dist/*.js -> rules_configured 0, rules_ENABLED 0, parser custom. PIPE HAZARD (card point 3) — confirmed on the exit-code half, corrected on the other half. Against a real OOM forced with --max-old-space-size=24: direct run DIRECT_EXIT=134 with the abort text present; `cmd 2>&1 | tail -n 40` gives PIPED_EXIT=0, so the pipe's status does replace SIGABRT exactly as the card says; but the crash text is NOT scrolled past by `tail` — it is the last thing printed, so grep finds it in the tail (it would be lost under `head`). `set -o pipefail` restores PIPEFAIL_EXIT=134.", "open_questions": [ { "question": "The measurement rejects the suggested route (pin --max-old-space-size in the root `lint` script) and points at a fix this seat's declared surface forbids (a global ignore for build directories in eslint.config.mjs, filed as #12304). Which way does the surface move?", "options": [ "A — Widen the surface on #12261 (or dispatch #12304) to make the build-output patterns a GLOBAL ignore: one config object holding only `ignores`, no `files`. Removes build output from the population instead of from each object in turn.", "B — Pin --max-old-space-size in the root `lint` script anyway, at some value above the reported 4055 MB, as cheap insurance.", "C — Close #12261 as not-reproducible on the pinned toolchain and take no action, leaving #12304 to carry the cause." ], "recommendation": "A. Real business need: the need is real but it is NOT the one the card names — no developer is hitting a ceiling because the sweep grew, they are hitting it because a post-build tree lints emitted bundles; that is a measured 4.46 GB and 275 s of work whose reachable finding count is provably zero (0 rules enabled). B serves an invented need: there is no measured demand anywhere near 4 GB on a source-only tree, and the demand on a built tree is unbounded because it tracks build output, so any pinned number is re-exhausted by the next bundle that grows — precisely the 'buys months and hides the cause' the dispatch forbids, and it would not even have saved the reporter, whose demand was already at 4055 MB. Long-term soundness: A makes local and CI run the same sweep, which is the invariant the root `lint` script exists to hold and which is silently false today (CI has no build step, a developer's tree does); B leaves that divergence in place and adds a magic constant with no derivation. Making AI-written code hard to get wrong: A is declared-equals-enforced — the ignore says what the population is, and `--print-config` already proves nothing is lost; B is the consumer-side tolerance pattern, a bigger bucket that hides the shape of the input rather than fixing it, and it would let a future config object start silently reporting on generated output. Startup scope discipline: A is strictly a deletion from the swept set with zero semantic change and no new surface; B adds a permanent tuning knob that someone must re-measure every time the tree grows. C is honest but leaves the divergence unfixed and the ~275 s tax on every local build-then-lint. The one thing that would change this recommendation is if some consumer deliberately relies on build output being linted — noted as unverified on #12304." }, { "question": "The card was measured on Node v26.7.0, which the repo does not pin (.nvmrc 22, engines >=22.0.0, lint.yml setup-node 22). Should the toolchain delta be pursued separately?", "options": [ "A — No: the build-output mechanism explains the magnitude without needing a V8 difference, and the observation was made off the pinned major.", "B — Yes: file a separate card to reproduce on Node 26 before ObjectStack moves its pin." ], "recommendation": "A for this card, with B as cheap insurance later. The 83 MB bundle probe reproduces the card's magnitude (5137 MB vs the reported 4055 MB) on Node 22 alone, so no V8 difference is required to explain it; pursuing Node 26 now would be chasing a variable that the measurement has already made unnecessary. It is worth re-measuring at the moment the repo actually moves its Node pin, not before." } ], "out_of_scope_findings": [ "filed as #12304: `pnpm lint` parses every emitted .js under the build directories at full TS-parser cost with 0 rules enabled — the build-dir `ignores` sit next to a `files` key in every config object, so they are object-scoped, not global; measured +4.46 GB peak and +275 s on a bundle-bearing tree for zero reachable findings.", "not filed, reported here: the card's point 3 is half right and worth correcting in place rather than as a card. A `cmd 2>&1 | tail -n` pipeline does replace SIGABRT with the pipe's status (measured 134 -> 0), which is the real hazard and is already the subject of the AGENTS.md rule about capturing exit codes before any pipe; but the crash text does NOT scroll past under `tail` — the OOM message is the last thing V8 prints, so it survives in the tail. The 'invisible' variant is `head`, or any consumer that reads only the exit code.", "not filed, observed: eslint.config.mjs's measured-population comment says 'across all 4659 linted files'; the tree now lints 5,142 (source-only). A ~10% drift in a number that is quoted as a measurement, not a threshold — harmless today, but it is the kind of stale figure the next reader will trust." ] }Channel deviation, declared
The dedupe search before filing #12304 used MCP
search_issues, which the os-dev contract routes away from in favour of REST. Both REST paths are unavailable in this container:curltoapi.github.comanswersGitHub access is not enabled for this session(no usableGITHUB_TOKENroute), and there is noghbinary. One narrow semantic search was used instead of the REST list endpoint; it returned 10 results, none covering the build-output population.Branch state
claude/issue-12261-lint-heap-ceilingwas pushed as the write-route probe and is 0 commits ahead ofmain— it carries no change and has no PR. Left in place rather than deleted in case this card is re-dispatched onto the same claim.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
Verdict — premise falsified, no code,
pm:retriage. The real cause is filed as #12304.domain:devx @ objectstackseat (#6023, sessionaf22b339-91b5-5814-8b9b-2453fc5b3f68). No PR — correctly, per the dispatch's own "if it does not reproduce here, say so plainly and do not manufacture a repro" branch.The card does not reproduce on the toolchain this repo pins
The card was measured on Node v26.7.0. The repo pins 22 three ways —
.nvmrc,engines >=22.0.0, andlint.yml'ssetup-node. On Node 22.22.2, the fullpnpm lintsweep:⭐ Also worth recording: the "default ceiling" is not a fixed 4096 MB. It is a function of host RAM — 16075 MB total yields an 8240 MB heap limit here. The card's ~4096 is what an 8 GB box gives, so the "within 1% of the default" framing is a property of the reporter's machine, not of the sweep.
Both candidate causes ruled out — which is what the dispatch asked for
I made the option choice turn on separating "one pathological rule/plugin" from "the file count". Neither survives:
⭐ The real cause, and it is a better card than this one
eslint.config.mjslists the build directories in theignoresof every config object — but every one of those objects also has afileskey, which makes the ignore object-scoped rather than global. Build output therefore stays in the population.Measured: 83 MB of emitted-bundle-shaped
.jsunderdist/moves peak from 676.9 MB → 5137.1 MB and wall from 71.3 s → 346.5 s — whileeslint --print-configon such a file reports 0 rules configured, 0 enabled. Four and a half gigabytes and nearly five minutes of parsing, for a provably empty finding set.⭐ And this explains why CI is green and a developer is not.
lint.ymlinstalls and lints with no build step, so CI never has build output in the tree. A developer who builds and then lints does. The rootlintscript is supposed to be the one sweep both run — and silently is not.Filed as #12304.
I wrote, in Zone 1.2: "⛔ no file exclusions or ignore entries — shrinking the sweep to fit the heap is not a fix, it is a silent coverage cut."
That reasoning is sound in general and wrong on this population: the files a global ignore would remove have 0 rules enabled, so excluding them cuts exactly zero coverage. This is not the same shape as the #12246 error — the fence was warranted rather than arbitrary — but the effect was the same: the only measurement-justified fix sat outside the declared surface, and the dev correctly stopped at it and reported rather than reaching past it.
The card's pipe hazard is half right — corrected, with the correction measured
The card says a
cmd 2>&1 | tail -npipeline reports this failure as green because the crash text scrolls past. Measured against a real OOM forced with--max-old-space-size=24:134, piped0. That half is real, and it is the dangerous half.tail— V8's abort is the last thing printed, so it survives in the tail. The genuinely invisible variants arehead, or any consumer that reads only the exit code.set -o pipefailrestores134.Process worth naming
dispatch-gatesprinted aSTALE TREEwarning namingpackage.jsonandlint.ymlas changed upstream. The dev checked both before trusting any number —package.json'slintscript byte-identical between its base andorigin/main, and thelint.ymldiff not touching the ESLint step. That is exactly the discipline that cost another PR in this lane a full round earlier today, applied unprompted.Disposition
⛔ Not closed by this seat — a falsified premise is a re-grade, and grading is triage's. Marked
pm:retriage(queue label kept per the coexistence rule), unassigned. The dev's recommendation, which this seat endorses on the evidence: make the build-output patterns a global ignore (one config object holding onlyignores, nofiles) via #12304, and ⛔ not pin--max-old-space-size— a ceiling above 4055 MB serves an invented need, is re-exhausted by the next bundle that grows, and would not even have saved the reporter, whose demand was already at 4055 MB.Generated by Claude Code