Skip to content

docs(check-dts-emitted): name the silent worker-death class, not OOM - #13898

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-13509-dts-emitted-oom-header
Aug 31, 2026
Merged

docs(check-dts-emitted): name the silent worker-death class, not OOM#13898
os-project-manager merged 2 commits into
mainfrom
claude/issue-13509-dts-emitted-oom-header

Conversation

@claude

@claude claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #13509

Text-only rewrite of scripts/check-dts-emitted.mjs: its header comment and the one
message it prints to the engineer who trips it. The predicate, the --self-test and the
rollout are untouched — see the mechanical zero-logic-change proof below.

Note on placeholders: the guard's real message spells the package placeholder with angle
brackets. It is written PKG throughout this body so the body sanitizer cannot eat it.

The defect

The guard named OOM as the shape that leaves a build silently declaration-less, in two
places — the THE DEFECT IT EXISTS TO CLOSE header block, and the printed remedy, which
told the engineer to retry with NODE_OPTIONS=--max-old-space-size=8192.

Measured, that is backwards, and the printed half is the expensive one: heap advice for a
failure that was never about heap cannot converge, so it sends the reader into a retry loop
while the genuinely useful step in the same message (turbo --force) sits below it.

A2.1 — the loud/silent claim, established here rather than taken on faith

The card's table is the whole basis of the rewrite, so I rebuilt it rather than citing it.
Harness mirroring tsup's promise structure exactly (a Worker, postMessage, and only
worker.on('message') registered — no error, no exit handler), Node v22.22.2,
linux x64 — the same platform the card reports:

worker death mode event that fires process outcome output
process.exit() in worker, CJS caller exit only exit 0 0 bytes
process.exit() in worker, ESM caller w/ top-level await exit only exit 13 Warning: Detected unsettled top-level await
heap OOM, per-worker resourceLimits: { maxOldGenerationSizeMb: 16 } error exit 1 Error [ERR_WORKER_OUT_OF_MEMORY] + Unhandled 'error' event
heap OOM, process-wide --max-old-space-size=64 error exit 1 same

All four rows reproduce. The mechanism is EventEmitter: an error event with no listener
is rethrown, so an OOM'd DTS worker fails loudly and non-zero — and a non-zero build is
never cached by turbo, which is exactly the property the header claimed OOM lacked.

In run 1 the harness's Promise.all(...).then(() => console.log('BUILD REPORTED SUCCESS'))
never fired and the process still exited 0 with zero bytes of output — the silent
false-success reproduced directly.

Nothing in Zone 2 was falsified. The card's measurement stands as written.

A2.2 — what the silent shape actually is

Not OOM. It is a worker that ends without posting a message and without emitting an
error event
— a hard process.exit inside the worker, a terminated thread, any other
message-less end. The caller's module system is load-bearing: the same message-less death
exits 13 with a warning under an ESM top-level await and 0 in silence under CJS, and
tsup's CLI is CJS, which is why the observed shape was silent.

Per the triage boundary, OOM is kept, not deleted — a reader who has just seen an OOM is
told explicitly that it is not this — and the rewrite does not assign a cause to the
original plugin-auth observation. It establishes only that whatever ended that thread did so
without an error event, because the run exited 0.

A2.3 — the new remedy, and why it discriminates

The heap-headroom retry is gone. Two discriminators replace it, both derivable from things
the engineer can already see:

1. Reaching the message is itself evidence. Every one of the 67 packages wires the guard
as tsup && node ../../scripts/check-dts-emitted.mjs. The && means the guard runs only
if tsup exited 0 — and an OOM'd worker exits non-zero, so the build stops at tsup and this
guard never runs. The message now says so: "Reaching this text is itself evidence the DTS
pass exited 0."
That is what makes "more heap will not help" checkable rather than asserted.

2. One ls splits the two real causes. The guard can fire for two quite different
reasons, and they need opposite responses:

  WHICH SHAPE ARE YOU IN? Count the declarations actually on disk:
    ls dist/*.d.ts dist/*.d.mts dist/*.d.cts 2>/dev/null | wc -l

  0 - the DTS pass produced nothing, so read what it printed:
        pnpm --filter PKG build 2>&1 | grep -i dts
    A "DTS Build start" with no success and no error line after it is the
    message-less death above.

  1 or more - nothing died. package.json promises a declaration path that this
    package's tsup entries never emit, so every rebuild fails here the same way;
    line the `types` conditions up with the entry points that are really built.

The second branch is the one the old text could never reach: a manifest promising a path the
package's tsup entries never emit fails identically on every rebuild, so any retry-shaped
advice is wrong for it. The turbo --force cache-clearing step is kept, unchanged.

A2.4 — census: is the wrong fact repeated?

Swept 942 files under scripts/, content/docs/ and docs/ by mechanism — comment markers
stripped, newlines flattened (the sentence wraps), case-sensitive word-bounded OOM, plus a
co-occurrence pattern for OOM within 200 chars of a silence / exit-0 / never-settles claim.
Counter-checked against two phrases known to be present (THE DEFECT IT EXISTS TO CLOSE,
11907) — both fire, so the sweep is reading these files.

Inside the repo tree, scripts/check-dts-emitted.mjs is the only carrier. The other
OOM hits are all a different subject:

  • docs/launch-readiness.md — unbounded in-memory adapter growth and retention caps. Unrelated.
  • scripts/check-test-completeness.mjs — a vitest pool worker dying with "no JS error to
    catch"; a different runtime and a narrower proposition (it does not claim process exit 0).
    Its own defect (feat(spec): one canonical conformance table for the filter logical combinators #3812) has its own gate. Related in spirit, distinct in fact.
  • scripts/check-agent-test-spelling.mjs — a fixture string quoting the line above.
  • check-type-check-coverage.mjs / check-test-typecheck.mtsmax-old-space-size for
    tsc, backed by a real captured FATAL ERROR ... heap out of memory trace. A genuinely
    loud OOM; legitimate.

One repeat exists outside the tree: the body of #12167 (closed) carries the same
attribution verbatim — "OOM under memory pressure is the observed shape". It is an issue
body, not a repo file, and out of this card's fence. Reporting, not widening — say the
word and I will file it or correct it.

Verification

Final commit 5d42a06 (branch merged with origin/main before the sweep, so the gate
derivation is not answering about a stale tree).

  • Zero logic change, mechanically: every changed line on both sides of the diff is
    either a // comment line or lies inside the single console.error(...) argument —
    12 old-side lines (7 comment, 5 in the message), 53 new-side (35 comment, 18 in the
    message), 0 outside both. No executable statement moved.
  • node scripts/check-dts-emitted.mjs --self-test → exit 0, "all assertions passed."
  • pnpm check:ratchet-remedy-authority → exit 0. "OK check-ratchet-remedy-authority:
    179 scripts swept ... Control corpus: 26 hand-classified scripts, set-equality audited both
    ways."
    This file's verdict stays excluded before and after.
    • Positive control, because a green here could otherwise just mean the sweep is blind to
      this file:
      injected an offer-shaped remedy ("add an entry to the shrink-only baseline
      in scripts/dts-waiver-baseline.json") into the printed message, proved it on disk, and
      the gate flipped to exit 1 / UNMARKED: scripts/check-dts-emitted.mjs. Restored with
      git checkout HEAD -- ABSOLUTE_PATH and verified byte-identical (blob
      d4a18319ebf8a0d3f553c48e8e45338060443787 both sides), 0 residual occurrences.
  • pnpm lint (eslint . --no-inline-config, full repo-wide, under the shared verify
    lock) → exit 0, 112s. No narrowing, so no narrowing argument is owed.
  • pnpm check:nul-bytes → exit 0, 7614 files, no raw control bytes. Edited file is 0 bytes
    non-ASCII.
  • All 12 path-derived gate families (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands) → 11 exit 0. The 12th,
    check-test-completeness.mjs, exits 3 = PREREQUISITE NOT MET: it grades a saved
    turbo run test log and the family list invokes it with no argument. Its own text calls
    this branch NOT MEASURED, "not a red, and there is nothing here to fix." Recorded as
    not measured, not as green.
  • The derived family list does not name check:ratchet-remedy-authority — reproducing
    dispatch-gates places check:ratchet-remedy-authority in "undetermined" — a scripts/** sweep that no derived family names, so a full local family run misses it #13813 (its population is built at runtime from readdirSync, so no path derivation
    reaches it). Run explicitly.

No changeset

Nothing publishes. The root package is private: true, and scripts/ sits outside every
glob in pnpm-workspace.yaml, so this file ships in no package. The changed text is a
contributor-facing build message and a maintainer-facing comment — no customer-visible
surface. Labelled skip-changeset.


Generated by Claude Code

claude added 2 commits August 31, 2026 16:29
The guard's header and its printed remedy both named OOM as the shape that
leaves a build silently DTS-less. Measured, an OOM'd DTS worker is the LOUD
path: Node delivers worker heap exhaustion as an 'error' event
(ERR_WORKER_OUT_OF_MEMORY), and an 'error' event with no listener is rethrown
by EventEmitter, so the run exits non-zero and prints a stack.

The silent shape is a worker that ends without posting a message AND without an
'error' event, from a CJS caller. Reworded both places to lead with that class,
to keep OOM present as the loud sibling a reader must be told it is NOT, and to
replace the heap-headroom retry with a next action that discriminates between
the two causes this guard can fire on.

Text only: comments and one console.error string. The predicate, --self-test
and rollout are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPTdomain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC. Verified against origin/main, ⛔ not the shared checkout.

What makes this one good

The card asked for a header that names the class instead of the trigger. The delivered change does that and keeps OOM, relabelled as the loud sibling a reader must be told they are not looking at. ⭐ Deleting OOM would have been the obvious move and it would have been wrong: a debugger arriving from a memory-starved build needs to be told explicitly that they are in the other shape, and an absent paragraph tells them nothing.

Verification — the load-bearing claims, re-derived by this seat

claim verdict
all 67 packages wire the guard after &&, so reaching the message proves tsup exited 0 ✅ 67 manifests reference it; the separator is && in 67 of 67 (45 at ../../../, 22 at ../../)
…including the one package that wraps tsup in an if [ -z "$OS_SKIP_DTS" ] block ✅ no hole: the guard returns 0 early at scripts/check-dts-emitted.mjs:130 when OS_SKIP_DTS is set, so a skipped build never reaches the failure text
zero logic change ✅ read off the diff: every changed line is inside a // comment or inside the single console.error argument. No executable statement added, moved or removed
nothing publishes ⇒ skip-changeset ✅ root package is private: true, and scripts/ matches none of the pnpm-workspace.yaml globs (packages/*, packages/apps/*, packages/drivers/*, packages/plugins/*, packages/qa/*, packages/triggers/*, packages/services/*, packages/adapters/*, packages/connectors/*, apps/*, examples/*)

The && measurement is what upgrades one sentence from assertion to check. "Reaching this text is itself evidence the DTS pass exited 0" is only true if every call site is conjunction-wired; the dev measured it rather than assuming it, and it holds 67/67. That is the difference between a remedy a reader can act on and one they have to trust.

On the remedy that was removed

The old text ended in NODE_OPTIONS=--max-old-space-size=8192 — a non-converging remedy, since the exit-0 shape is not a memory problem, so a reader could raise the heap forever and never move. It is replaced by a discriminator: count the declarations on disk, and the two branches split the two real causes (zero ⇒ the message-less worker death; one or more ⇒ package.json promises a declaration path the package's tsup entries never emit, which no retry can fix). ⭐ That is a remedy that terminates, which the old one did not.

Zone 2 — nothing falsified, and the measurement is why

The dev reported no Zone 2 assumption falsified, but ⛔ did not take the card's premise on faith either: it built a harness with tsup's promise shape (only message registered) on Node v22.22.2 / linux x64 and reproduced all four rowsprocess.exit() under a CJS caller gives exit 0 and zero output; the same death under an ESM top-level-await caller gives exit 13 plus the unsettled-await warning; both OOM variants give exit 1 with ERR_WORKER_OUT_OF_MEMORY. ⭐ The caller's module system is load-bearing and that is the part nobody would have guessed from the card.

⚠️ The positive control on check:ratchet-remedy-authority is the other thing worth naming: a gate that reads "excluded" both before and after a change cannot, on its own, distinguish a pass from a blind spot. Injecting an offer-shaped remedy, proving it on disk, watching the gate flip to exit 1, then restoring under a trap and verifying by blob equality — that is the shape a green tick has to earn. ⛔ Not a nicety; it is the difference between a gate that ran and a gate that was silent.

Open question — answered, and already actioned

The dev found the wrong claim repeated outside the repo tree, in the body of closed issue #12167 ("OOM under memory pressure is the observed shape"), reported it, and ⛔ correctly did not widen the PR to act on it.

Option B, as recommended: a correcting comment is now posted on #12167, carrying the four-row measurement, and the body is left as filed. ⛔ Editing a closed card's body would rewrite the record of what was believed at the time; leaving it silent would keep a wrong sentence discoverable by search while the header that cites it says the opposite. The comment corrects the reader without falsifying the history.

Governed-surface check

Diff is scripts/check-dts-emitted.mjs only. ⛔ No hit on docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md ⇒ this seat may arm it.

Arming

Lint & Repo Gates is still running (⛔ zero failures so far). Arming follows once it completes green.


Generated by Claude Code

@os-project-manager
os-project-manager marked this pull request as ready for review August 31, 2026 17:36
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 31, 2026
Merged via the queue into main with commit 00f79c9 Aug 31, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13509-dts-emitted-oom-header branch August 31, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants