Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneesstack72

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.club every minute under swamp serve. Its resource total_events has 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-summary report 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 when autoGc: true is set in .swamp.yaml (0c0de390, swamp-club#1304). Without autoGc the history grows forever, and the only remedy is repo-wide swamp 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. autoGc runs only after the method completes (libswamp/models/run.ts:1215). The method's own write overflows first at unified_data_repository.ts:1913, so turning autoGc on 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 :941 recomputing 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: :1777 spreads versions once 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-wide swamp 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)); // RangeError

Or: 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:941 Math.max(...versions)
  • …:1072 Math.max(...versions) + 1 (tombstone version)
  • …:1144 Math.max(...remaining)
  • …:1444 Math.max(...versions) (latest version lookup)
  • …:1777 Math.max(...versions) inside the duration-GC loop (also O(n²))
  • …:1844 Math.max(...currentVersions)
  • …:1913 Math.max(...versions) + 1: next version on write; this is what breaks the method
  • …:2023 Math.max(...versions)
  • src/libswamp/data/versions.ts:156 Math.max(...versionNumbers)
  • src/infrastructure/persistence/in_memory_data_repository.ts:173 Math.max(...versions)

Latent, input-size dependent (same failure once the input is large enough):

  • src/libswamp/models/output_logs.ts:215 allEntries.push(...lines): one log artifact with >~129k lines crashes model output logs.
  • src/infrastructure/persistence/json_telemetry_repository.ts:125 entries.push(...dayEntries): more than ~129k local telemetry entries in one day.
  • src/domain/workflows/workflow_run.ts:553 this._steps.splice(templateIndex, 1, ...insertions): a forEach expanding to >~129k steps.
  • src/serve/audit_sinks/store_sink.ts:108 this.#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

  1. 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, so versions.at(-1) also works where that's guaranteed. For push(...x), use for (const e of x) out.push(e) or out = out.concat(x).
  2. 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.ts has already hit this once.
  3. Separately: garbageCollection: N does nothing unless autoGc is 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 a swamp 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.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 8 MOREREVIEW+ 7 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

9/27/2026, 1:24:56 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack729/26/2026, 11:26:38 PM

Sign in to post a ripple.