* fix(compression): fall back in-process when the compression worker fails (#13145)
The worker pool resolved every worker fault with the *uncompressed* body instead
of reporting it. `PendingJob` had no reject path at all, so a thread error, a
worker exit, a dispatch timeout, or an engine error posted back as
`type: "error"` all resolved as `{ compressed: false, stats: null }`.
`applyCompressionAsync` then treated that as a legitimate "nothing to compress"
result and returned it as-is, so the request reached the provider uncompressed
while the response header still announced the selected plan ("stacked") — the
header is emitted before the pipeline runs. Nothing was logged at any level, and
`compression_analytics` stayed empty because rows are only written when a
compressed result is reported. The net effect was compression silently disabled
for every worker-eligible request.
The worker is a throughput optimisation, not a behavioural variant, so a worker
fault must degrade to the in-process pipeline rather than to no compression:
- `PendingJob` gains `reject`; `fail()` delegates to a new `abort()` that clears
the slot timeout and rejects with a diagnostic cause (thread error, exit code,
or timeout budget).
- An `error` message from the worker is propagated instead of being swallowed.
- `applyCompressionAsync` catches the rejection and falls through to the
in-process path, logging the cause. The logger is imported lazily and
defensively: `compressionWorker.ts` imports this module, so a static import
would pull the logger into the worker bundle, and a logging failure must never
be able to break compression itself.
`close()` keeps resolving with the unchanged body — shutdown is not a fault.
The regression test drives a real worker fault via
`OMNI_COMPRESSION_WORKER_TIMEOUT_MS` rather than mocking the module, since this
project's tsx/ESM + node:test setup has no `mock.module()` support. Its options
are fully populated on purpose: `runCompressionAsync` forwards them into
`workerOptions`, and `isStrictlySerializable` rejects an object holding
`undefined` values — which would route the test through the in-process path and
assert nothing. Production requests always carry all of those fields, which is
why the worker path is taken there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGJQT3E6iJZq4zNGwfjkPs
* fix(compression): keep the timeout path uncompressed, retry only fast worker faults (#13145)
Review follow-up: the in-process fallthrough ran the full pipeline on the main
event loop for *every* worker fault, including a dispatch timeout. A timeout
means the worker already spent its whole budget on that body, so re-running the
same CPU-bound work inline would stall other in-flight requests — strictly worse
than not compressing on a shared gateway.
Faults are now typed by whether recovery is cheap:
- `CompressionWorkerError.retryInProcess` distinguishes fast faults (thread
error, worker exit, engine throw — no work was done, so the in-process path
costs what the worker would have) from a dispatch timeout.
- Timeouts keep the original degrade-to-uncompressed behaviour, but are now
reported. The defect this PR fixes is the silent swallow, not the degrade.
Also strips `reject` from the structured-clone wire job. It is a function, so
leaving it on the object handed to `postMessage` threw `DataCloneError` before
the worker ever saw the job — turning every dispatch into an immediate fault.
Adds the missing `changelog.d/fixes/` fragment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(compression): narrow the worker thread-error type for typecheck:core
@types/node 26 types the Worker "error" event payload as unknown, not
Error, so `error?.message` failed typecheck:core (TS2339). Narrow with
an instanceof check before reading .message.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: marcs7 <marcs7@users.noreply.github.com>
`remove(slot, false)` dropped the slot without `terminate()`, leaving the OS thread and its heap alive — invisible to RSS, which is why 55 orphaned `MessagePort`s took 16 h to surface. Removing the parameter rather than keeping it is the correct call: the pool was its only owner.
---
Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.
- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches
⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.
Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
* fix: resolve compression worker file using runtime anchors instead of import.meta.url
Replace workerUrl() function that used import.meta.url with resolveWorkerFile()
function that uses runtime anchors (process.cwd() and process.argv[1]) to locate
the worker file. This fixes webpack module resolution in Next.js standalone
bundles where import.meta.url is replaced with a stub pointing to build machine
path.
Also update Dockerfile to copy required worker-related scripts and adjust
npm install flags for better compatibility.
* fix(docker): restore base Dockerfile — keep npm ci --ignore-scripts supply-chain guard
Revert every Dockerfile change from this branch back to release/v3.8.51:
the branch dropped --ignore-scripts (reopening install-time script
execution for all transitive deps), swapped the reproducible npm ci for
npm install, invoked the nonexistent 'npm approve-scripts' command, and
broke the better-sqlite3 smoke test with a stray space in ':memory: '.
The worker-file fix does not need any Dockerfile change.
* test(compression): export runtime-anchor helpers and cover worker-file resolution
firstAncestorWith's doc already claimed 'exported for tests' without the
export; export it together with resolveWorkerFile and add unit coverage
for the runtime-anchor resolution: cwd anchor, dirname(argv[1]) anchor,
bounded walk-up (8-level cap boundary), prod-first .js-over-.ts ordering,
dev .ts fallback and the fail-open cwd fallback when nothing exists.
Fixtures live in mkdtemp sandboxes only — the repo tree is never touched.
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Retargetado para release/v3.8.51 (release/v3.8.50 está congelada — freeze issue #11439). Validado em lote combinado (batch-0824h2, junto de #11435/#11436/#11437) contra o tip de release/v3.8.51: typecheck:core limpo, gates estáticos OK, 127/127 testes focados passando.
Investigação sólida com repro real via container isolado, três causas independentes identificadas e corrigidas com testes de regressão dedicados para cada uma. Obrigado pela contribuição!