Relationships
#2565 Maximum call stack size exceeded once a resource passes ~129k versions (Math.max(...versions) in the data repository)
Opened by keeb · 9/26/2026· Shipped 9/27/2026
Summary
Once a data resource passes roughly 125–130k versions, every write to it fails with RangeError: Maximum call stack size exceeded. swamp data versions on it fails the same way. The cause is Math.max(...versions): spreading an array into a function call hits V8's argument limit. A minute-cadence poller reaches that count in about 88 days.
In Deno 2.9.6 (the runtime swamp extracts), Math.max(...a), arr.push(...a) and String.fromCharCode(...a) all throw at ~129,620 elements at shallow depth. Deeper async stacks lower that ceiling, so a resource just under it flaps: most writes fail, a few succeed.
Observed
- A model polling
telemetry.swamp.clubevery minute underswamp serve. Its resourcetotal_eventshas 126,614 versions (v1 = 2026-05-20). - Starting 2026-09-19 11:37 UTC, ~60 runs/hour fail. About 1 in 1,000 succeeds.
- The method body finishes (the fetch logs its value). The failure is in
writeResource→ the version allocation below. The@swamp/method-summaryreport fails with the same error. swamp data versions <model> <name> --json:Maximum call stack size exceeded at src/libswamp/data/versions.ts:156:34- The model declares
garbageCollection: 1440, but that is only enforced on write whenautoGc: trueis set in.swamp.yaml(0c0de390, swamp-club#1304). WithoutautoGcthe history grows forever, and the only remedy is repo-wideswamp data gc, which also prunes every other model's history. There is no per-model or per-resource gc.
Why an oversized resource can't recover through swamp
- autoGc never reaches it.
autoGcruns only after the method completes (libswamp/models/run.ts:1215). The method's own write overflows first atunified_data_repository.ts:1913, so turningautoGcon can't shrink a resource that is already over the limit. It stays broken until something outside the method path prunes it. - Per-version delete overflows, and leaves things half-done.
delete(type, modelId, name, version)removes the version dir and its catalog row first, then overflows at:941recomputing latest. The latest marker is left un-updated, pointing at whatever it pointed at before, possibly the version just removed. - Rename overflows at
:1072(tombstone version) and its rollback at:1144. - Duration-based GC overflows:
:1777spreadsversionsonce per candidate version inside the loop (also O(n²)). Only numeric keep-N GC is safe: it slices the list and only spreads the survivors at:1844. - What still works: whole-resource
swamp data delete <model> <name>(removes the dir, no spread), which destroys all history, and repo-wideswamp data gc, which prunes every model. There is no scoped keep-N prune for one resource.
Reproduce
// Deno 2.9.6
Math.max(...Array.from({ length: 130_000 }, (_, i) => i)); // RangeErrorOr: write ~130k versions to one resource with autoGc unset, then run the method again or swamp data versions.
Sites (scan of src/ at b1ac6226)
Unbounded, grow with a resource's version count (confirmed failing):
src/infrastructure/persistence/unified_data_repository.ts:941Math.max(...versions)…:1072Math.max(...versions) + 1(tombstone version)…:1144Math.max(...remaining)…:1444Math.max(...versions)(latest version lookup)…:1777Math.max(...versions)inside the duration-GC loop (also O(n²))…:1844Math.max(...currentVersions)…:1913Math.max(...versions) + 1: next version on write; this is what breaks the method…:2023Math.max(...versions)src/libswamp/data/versions.ts:156Math.max(...versionNumbers)src/infrastructure/persistence/in_memory_data_repository.ts:173Math.max(...versions)
Latent, input-size dependent (same failure once the input is large enough):
src/libswamp/models/output_logs.ts:215allEntries.push(...lines): one log artifact with >~129k lines crashesmodel output logs.src/infrastructure/persistence/json_telemetry_repository.ts:125entries.push(...dayEntries): more than ~129k local telemetry entries in one day.src/domain/workflows/workflow_run.ts:553this._steps.splice(templateIndex, 1, ...insertions): aforEachexpanding to >~129k steps.src/serve/audit_sinks/store_sink.ts:108this.#batch.push(...events): safe only as long as callers pass small event arrays.
Checked and bounded (no action): verification_proof.ts:53 (signature bytes), execution_service.ts:2957 (step concurrencies), serve.ts:4811 (runs still active at shutdown), models/bundle.ts:74 (already chunked; its comment at :61 documents this exact hazard), plus the many push(...) calls over file lists, validation errors and grants, and all of src/presentation/** (column widths over rendered rows).
Suggested fix
- Replace spread-into-call on unbounded arrays with a loop or
reduce, e.g.versions.reduce((m, v) => (v > m ? v : m), -Infinity). Most listings already come back sorted, soversions.at(-1)also works where that's guaranteed. Forpush(...x), usefor (const e of x) out.push(e)orout = out.concat(x). - Add a lint rule or a grep test that forbids
Math.max(.../Math.min(.../.push(...on arrays not provably bounded, so this doesn't come back.bundle.tshas already hit this once. - Separately:
garbageCollection: Ndoes nothing unlessautoGcis on, and nothing warns about it. Either enforce the declared cap on write by default, warn when a resource passes its cap by a wide margin, or add aswamp data gc <model> [<name>]scope so one runaway resource can be pruned without pruning the whole repo.
Environment
- swamp dev build
b1ac6226(2026-09-18), Deno 2.9.6, Linux x86_64 - Running under
swamp serve(trigger.schedule: "* * * * *"), and reproduced from the CLI.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.