Skip to content

fix: cap uncapped string accumulators (RangeError: Invalid string length)#307

Merged
jpr5 merged 2 commits into
mainfrom
fix/cap-uncapped-string-accumulators
Jul 17, 2026
Merged

fix: cap uncapped string accumulators (RangeError: Invalid string length)#307
jpr5 merged 2 commits into
mainfrom
fix/cap-uncapped-string-accumulators

Conversation

@jpr5

@jpr5 jpr5 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Problem

RangeError: Invalid string length was firing ~1/sec in prod (caught at server.ts:1230). V8's max string length on 64-bit is 2^29-1 (~512MiB of UTF-16 code units); appending past it throws.

Two uncapped in-memory string accumulators in the aimock binary build a JS string past that limit:

  1. src/agui-recorder.ts — on stream end the recorder does Buffer.concat(chunks).toString() over every buffered upstream SSE chunk with no byte cap (unlike the generic proxy path in recorder.ts, which is capped at 64MiB maxProxyBufferBytes / 256MiB PROXY_BUFFER_HARD_CEILING).
  2. src/stream-collapse.ts — the content += delta / reasoning += delta / entry.arguments += delta / audioB64 += … / argsStr += … accumulators (plus orderAtoms coalescing) grow unbounded across every collapser.

Amplified 07-13 by the real-key fixture-miss passthrough (b2ef945): large real upstream responses now flow into these accumulators. The recorder.ts comment claiming "stream-collapse never throws Invalid string length" was false for these paths and is fixed here.

Fix

Mirrors the existing PROXY_BUFFER_HARD_CEILING / maxProxyBufferBytes discipline — bound then mark truncated, never fail the client. The client always receives its full response; only the recording is degraded to a clean skip.

  • agui-recorder.ts: new configurable record-buffer cap (AGUIRecordConfig.maxRecordBufferBytes, default 64MiB, clamped to a 256MiB AGUI_RECORD_BUFFER_HARD_CEILING). On trip: stop accumulating, free the buffer, skip fixture construction, keep the live client relay untouched — never Buffer.concat().toString() the oversized buffer.
  • stream-collapse.ts: guard each collapser's input under MAX_COLLAPSE_STRING_LENGTH (256Mi code units) before accumulation. The sum of any single accumulator can never exceed the total input, so bounding the input bounds every accumulator at one chokepoint (no per-+= whack-a-mole). Over-ceiling input is truncated and the result stamped truncated: true so the recorder skips journaling a partial fixture. Also covers the exported direct-call collapse entry points where no byte cap ran.
  • Fixed the false recorder.ts:31 comment.
  • Test-only overrides (setAGUIRecordBufferCeilingForTests / setCollapseStringLimitForTests) let the cap suites exercise truncation without allocating hundreds of MB.

Local red-green proof (real failure surface)

RED — AG-UI recorder (streaming ~535MiB of real SSE through proxyAndRecordAGUI on the unmodified build):

[repro] streaming ~535 MiB through the AG-UI recorder…
[aimock] NO AG-UI FIXTURE MATCH — proxying to http://127.0.0.1:49292
[RED] uncaughtException in recorder: Error: Cannot create a string longer than 0x1fffffe8 characters
RED EXIT: 1

(0x1fffffe8 = 2^29-1 — the exact V8 max-string-length boundary, i.e. the prod Invalid string length.)

GREEN — same repro, post-fix:

[repro] streaming ~535 MiB through the AG-UI recorder…
[aimock] AG-UI upstream response exceeded the 67108864-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory
[aimock] AG-UI record buffer cap exceeded — response relayed to client, recording skipped
[repro] client got status=200, bytesReceived=562041225
[GREEN] client received full body and recorder did not throw

Client received all 562,041,225 bytes — relay unaffected, only recording skipped.

stream-collapse mutation-guard (real collapseOpenAISSE, input just over the 256Mi ceiling):

  • pre-fix: content.length=276824064, truncated=undefined (unbounded, no clamp)
  • post-fix: content.length=260046848, truncated=true (clamped at ceiling, flagged)

Tests

  • src/__tests__/agui-record-buffer-cap.test.ts (2) — full relay + no crash + recording skipped over cap; normal recording under cap.
  • src/__tests__/stream-collapse-string-cap.test.ts (6) — every collapser clamps + flags truncated, never throws; no truncation under cap; Bedrock byte-cap.
  • Full suite: 4501 passed (153 files). Lint + prettier + build + tsc clean.

…string length

Two uncapped in-memory string accumulators in the aimock proxy/record path could
build a string past V8's max string length (2^29-1, ~512MiB), throwing
`RangeError: Invalid string length` (~1/sec in prod, caught at server.ts:1230).
Amplified by the real-key fixture-miss passthrough, which routes large real
upstream responses into these accumulators.

Two sites, both fixed to mirror the existing PROXY_BUFFER_HARD_CEILING pattern
(bound then mark truncated, never fail the client — recording is degraded to a
clean skip while the full response is still relayed):

1. agui-recorder.ts — `Buffer.concat(chunks).toString()` on stream end had NO
   byte cap. Add a configurable record-buffer cap (AGUIRecordConfig.
   maxRecordBufferBytes, default 64MiB, hard ceiling 256MiB): stop accumulating
   on trip, free the buffer, skip fixture construction, and keep the live client
   relay untouched.

2. stream-collapse.ts — the `content`/`reasoning`/tool `arguments`/`audioB64`/
   `argsStr` accumulators (and orderAtoms coalescing) were unbounded. Guard each
   collapser's input under MAX_COLLAPSE_STRING_LENGTH (256Mi code units) before
   accumulation begins — the sum of any one accumulator can never exceed the
   input, so bounding the input bounds every accumulator. Over-ceiling input is
   truncated and the result stamped `truncated: true`. Also covers the exported
   direct-call collapse entry points where no byte cap ran.

Also fixes the false recorder.ts comment that claimed stream-collapse "never
throws Invalid string length".

Test-only ceiling overrides (setAGUIRecordBufferCeilingForTests /
setCollapseStringLimitForTests) let the cap suites exercise the truncation path
without allocating hundreds of MB.
@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@307

commit: 1d08625

@jpr5
jpr5 merged commit b964ba2 into main Jul 17, 2026
27 checks passed
@jpr5
jpr5 deleted the fix/cap-uncapped-string-accumulators branch July 17, 2026 00:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant