* perf(compression): memory and OOM mitigations for large payload hashing and token estimation
* fix(compression): implement getMemoStats observability for result memo (#7847)
Adds the missing memo observability layer referenced by
tests/unit/compression/oom-memo-memory.test.ts and the monitoring API:
- resultMemo.ts: lifetime hit/miss counters + bounded time-ordered ring
buffer (10k entries, ~90KB) powering 1m/5m/15m/1h hit-rate windows;
getMemoStats() reports size/capacity/hits/misses/hitRate + windows.
- memoLookup() tags served results with stats.memoHit = true.
- clearMemoStore() also resets counters and the ring.
- compression/index.ts re-exports getMemoStats for the monitoring route.
- types.ts: optional memoHit field on CompressionStats.
- New GET /api/monitoring/compression route exposing the stats snapshot
(lightweight, no DB) for operators to track cache-hit efficiency.
* fix(compression): align memo contract with upstream #11727 — return caller object, reset lookup counter in clearMemoStore
* fix(compression): restore unwrapEventEnvelope in stream payload collector summaries
The OOM-mitigation commit accidentally replaced unwrapEventEnvelope(evt.data)
with asRecord(evt.data) in the summary builders and live push, breaking
translate-mode {event, data} envelope unwrapping (clientPayload type detection)
and failing 2 stream-payload-collector tests. Restored upstream semantics;
kept the jsonLength OOM optimization as the only delta in this file.
* refactor(compression): break down writeValue and writeEncodedString to pass complexity ratchets
Refactors jsonSha256 internal helpers (writeValue, writeEncodedString)
into small, single-responsibility sub-functions under the complexity
threshold (max cyclomatic 15, max cognitive 15). Preserves exact
JSON.stringify parity, circular reference guards on both arrays and
plain objects, and escape behavior (all 530 relevant tests pass).
* test(compression): make oom-memo heap assertion robust without expose-gc
The CI unit-test shard runner does not pass --expose-gc, so global.gc is
undefined and heapUsed can still momentarily hold GC-pending transients
(observed 53.4 MiB after a 3MiB body). Gate the retained-heap assertion
on forced collection being available (3 forced cycles for array buffers)
instead of skipping it silently, and keep it fully active when
--expose-gc is present.
* fix(compression): restore worker-pool offload path in runCompressionAsync
The OOM-mitigation refactor dropped the isCompressionWorkerEligible /
runCompressionInWorker dispatch at the top of runCompressionAsync, silently
removing the base's worker-thread offload for eligible large payloads.
Restore the block exactly as on release/v3.8.51, ahead of the result-memo
path, keeping the memoization and hashing improvements intact.
* docs(api): document GET /api/monitoring/compression and log route errors via pino
Add the new monitoring endpoint to docs/openapi.yaml following the
neighboring System entries, and replace the route's console.error with
the repo-standard pino logger.
* fix(skills): regenerate omni-resilience and add changelog fragment
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Andrian Balanescu <AndrianBalanescu@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Two changes with one root cause: several hot paths built a full JSON string only to read
its .length, and one of them silently changed the answer.
1. CORRECTNESS -- combo's fallback-compression trigger
estimateTokens(JSON.stringify(attemptBody)) took the STRING branch of estimateTokens,
which is ceil(length / CHARS_PER_TOKEN) over the raw JSON. An inline base64 image is
then charged as if every character of the data URL were prose. Measured on a 200 KB
inline image:
via string (before) 50,039 tokens
via object (after) 1,231 tokens
a 40x over-count, tripping fallback compression on requests nowhere near the context
window. This is the same class #8368/#8401 fixed on the request path; the combo call
site was missed. Passing the object routes through extractImageTokens, which charges
images structurally. Text-only bodies are unaffected -- verified identical, and pinned
by a test.
2. ALLOCATION -- jsonLength()
Adds an exact serialized-length walker: same O(n) scan, no string. Used by
estimateTokens' object branch and by streamReadinessPolicy (which runs on every
streaming request and only ever used .length).
Exactness matters because every consumer feeds a threshold, so this is property-tested
against JSON.stringify over 4000 generated structures covering escaping, lone
surrogates, omitted values, non-finite numbers, toJSON, Date, Map, cycles and BigInt.
Anything outside the plain-JSON subset falls back to JSON.stringify for THAT SUBTREE
only, so an exotic leaf never forces the message history back onto the allocating path.
Honest scoping of the memory win: the string was always transient, and V8 collects it
efficiently, so this is not 3 MiB of retained heap. Measured allocation churn over 20 calls
on a 3.06 MiB body: 3.1 MiB -> 0.5 MiB, about 6x less. The #8549 benchmark row for this
mechanism measures a HELD string and therefore overstates it; the correctness fix above is
the larger deliverable here.