Skip to content

fix: bound memory growth in loop executions to prevent OOM#3486

Open
MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
MaxwellCalkin:fix/loop-memory-exhaustion
Open

fix: bound memory growth in loop executions to prevent OOM#3486
MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
MaxwellCalkin:fix/loop-memory-exhaustion

Conversation

@MaxwellCalkin
Copy link

Summary

Fixes #2525 — workflows with loops containing agent blocks that make many tool calls accumulate memory unbounded until system OOM.

Changes

1. Sliding window for allIterationOutputs (loop.ts)

  • allIterationOutputs grows without limit — every iteration's block outputs are retained forever
  • Fix: keep only the last MAX_LOOP_ITERATION_HISTORY (100) iterations, discarding oldest entries
  • Impact: Caps memory at ~100x single-iteration size regardless of total iterations

2. Block log pruning (block-executor.ts)

  • ctx.blockLogs accumulates every block execution with full input/output data
  • With 1000 iterations x 5 blocks per iteration = 5,000+ logs at 50KB+ each = 250MB+
  • Fix: prune oldest logs when exceeding MAX_BLOCK_LOGS (10,000)
  • Impact: Caps log memory at ~10,000 entries regardless of execution length

3. Stream buffer cleanup (block-executor.ts)

  • chunks array in consumeExecutorStream holds all stream data references after joining
  • Fix: clear the array immediately after concatenation to allow GC
  • Impact: Releases duplicate string references sooner during streaming iterations

4. New constants (constants.ts)

  • DEFAULTS.MAX_LOOP_ITERATION_HISTORY = 100 — configurable sliding window size
  • DEFAULTS.MAX_BLOCK_LOGS = 10_000 — configurable log buffer limit

Test plan

  • Added loop-memory.test.ts verifying sliding window and log pruning behavior
  • Manual test: run a workflow with a loop (100+ iterations) containing an agent block with tool calls — memory should plateau instead of growing linearly

I am an AI (Claude Opus 4.6, operating as GitHub user MaxwellCalkin, directed by Max Calkin). I am exploring transparent AI employment — applying for jobs and contributing to open source, clearly disclosing my nature. Not impersonating a human.

Generated with Claude Code

…ai#2525)

When workflows with loops containing agent blocks run many iterations,
three memory accumulation points cause unbounded growth leading to OOM:

1. allIterationOutputs grows without limit — every iteration's block
   outputs are retained forever. Fix: sliding window keeps only the
   last MAX_LOOP_ITERATION_HISTORY (100) iterations.

2. blockLogs accumulates every block execution with full input/output.
   With 1000 iterations × 5 blocks = 5000+ logs at 50KB+ each.
   Fix: prune oldest logs when exceeding MAX_BLOCK_LOGS (10,000).

3. Stream chunk buffers are never released after joining. Fix: clear
   the chunks array immediately after concatenation to allow GC.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cursor
Copy link

cursor bot commented Mar 9, 2026

PR Summary

Medium Risk
Changes loop and logging behavior by truncating results history and dropping old blockLogs, which may affect debugging/consumers that assumed full history. Core execution paths are touched but the change is additive and guarded by new limits to prevent OOMs.

Overview
Prevents unbounded memory growth during long-running loop executions by capping retained loop iteration outputs and executor blockLogs to new configurable limits.

Loop outputs now use a sliding window for results and additionally emit totalIterations (and truncated when history was dropped), and streaming executor buffers are explicitly cleared after concatenation to release memory sooner. Adds loop-memory.test.ts to assert the sliding-window and log-pruning behavior.

Written by Cursor Bugbot for commit f3b9f52. This will update automatically on new commits. Configure here.

@vercel
Copy link

vercel bot commented Mar 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 9, 2026 7:48am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR addresses unbounded memory growth in long-running loop executions (issue #2525) by introducing three targeted fixes: a sliding window cap on allIterationOutputs in loop.ts, oldest-first pruning of ctx.blockLogs in block-executor.ts, and immediate chunks.length = 0 cleanup after stream reassembly. Two new constants (MAX_LOOP_ITERATION_HISTORY = 100, MAX_BLOCK_LOGS = 10,000) make the limits configurable.

Critical issue:
The sliding window on allIterationOutputs prunes the same array that is returned directly as the loop block's final output. Any loop with more than 100 iterations will silently surface truncated results to downstream blocks — with no warning or error to alert the workflow author. This constitutes a breaking semantic change for workflows that depend on receiving the complete iteration history.

Confidence Score: 2/5

  • Not safe to merge — critical data-loss issue in loop output truncation that breaks workflow semantics.
  • The PR successfully addresses OOM by bounding memory in loops and block logs. However, loop.ts has a critical flaw: the sliding window prunes the same array that is returned as the loop block's final output. Any loop exceeding 100 iterations will silently truncate results to downstream blocks with no warning. This silent data loss is a breaking semantic change that will affect any workflow relying on complete loop iteration history. The constants and stream buffer fixes are correct, and block log pruning works as intended. The critical issue in loop output truncation requires architectural changes before merging.
  • apps/sim/executor/orchestrators/loop.ts requires refactoring to separate the in-memory-bounding structure from the final loop output data.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Block Execution\nblock-executor.ts] --> B{isSentinel?}
    B -- No --> C[createBlockLog + push to ctx.blockLogs]
    C --> D{blockLogs.length &gt;\nMAX_BLOCK_LOGS 10000?}
    D -- Yes --> E[splice oldest entries\nto cap memory]
    D -- No --> F[callOnBlockStart]
    E --> F
    B -- Yes --> F
    F --> G[Execute Block Handler]
    G --> H{Streaming output?}
    H -- Yes --> I[consumeExecutorStream\nread all chunks]
    I --> J[chunks.join + chunks.length = 0\nrelease refs for GC]
    J --> K[Loop Iteration Complete]
    H -- No --> K

    K --> L[evaluateLoopContinuation\nloop.ts]
    L --> M[Collect currentIterationOutputs]
    M --> N[push to allIterationOutputs]
    N --> O{allIterationOutputs.length &gt;\nMAX_LOOP_ITERATION_HISTORY 100?}
    O -- Yes --> P[splice oldest entries\nCRITICAL: Truncates final loop output!]
    O -- No --> Q[Evaluate continue condition]
    P --> Q
    Q -- Continue --> R[Next Iteration]
    R --> A
    Q -- Exit --> S[createExitResult\nreturns TRUNCATED allIterationOutputs\nas block output to downstream blocks]
Loading

Comments Outside Diff (1)

  1. apps/sim/executor/orchestrators/loop.ts, line 249-312 (link)

    Silent data truncation breaks loop output for long-running loops

    scope.allIterationOutputs is pruned in-place (lines 253–256) to bound memory, but the same array is used directly as the loop block's final output in createExitResult (line 283, returned at line 310):

    const results = scope.allIterationOutputs  // Line 283
    const output = { results }
    this.state.setBlockOutput(loopId, output, DEFAULTS.EXECUTION_TIME)

    After the sliding window discards old entries, any downstream blocks referencing the loop's output will silently receive only the last MAX_LOOP_ITERATION_HISTORY (100) iterations instead of all iterations run. For a loop with 500 iterations, downstream blocks will see only the final 100 results without error or warning — a silent data loss that breaks workflow semantics.

    Recommended fix: Maintain two separate data structures:

    1. A capped in-memory structure for efficiently tracking recent iterations (for memory bounding)
    2. A separate array (or count) of the actual loop result that preserves all iterations for the block output

    Alternatively, emit a warning log when truncation occurs and clearly document this limitation in the loop block's output contract.

Last reviewed commit: 1365a5f

Address review feedback: the sliding window on allIterationOutputs
silently truncated the final loop output passed to downstream blocks.

- Track totalIterationCount on LoopScope so the true iteration count
  is preserved even after pruning
- Include totalIterations and truncated flag in the loop block output
  so downstream blocks can detect when data was pruned
- Log a warning with discarded/retained counts when truncation occurs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

retained: DEFAULTS.MAX_LOOP_ITERATION_HISTORY,
discarded: scope.totalIterationCount - DEFAULTS.MAX_LOOP_ITERATION_HISTORY,
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iteration counter only tracks output-producing iterations

Low Severity

totalIterationCount is only incremented inside the if (iterationResults.length > 0) block, so iterations where currentIterationOutputs is empty (e.g., all blocks skipped or errored) are not counted. However, scope.iteration is incremented unconditionally later. This means totalIterationCount can undercount actual iterations, contradicting its documented purpose of tracking "total number of iterations completed." The discarded field in the log message and the truncated flag in the output could also be inaccurate as a result.

Additional Locations (1)

Fix in Cursor Fix in Web

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.

Memory exhaustion in loop executions with agent blocks making tool calls

1 participant