fix: bound memory growth in loop executions to prevent OOM#3486
fix: bound memory growth in loop executions to prevent OOM#3486MaxwellCalkin wants to merge 2 commits intosimstudioai:mainfrom
Conversation
…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>
PR SummaryMedium Risk Overview Loop outputs now use a sliding window for Written by Cursor Bugbot for commit f3b9f52. This will update automatically on new commits. Configure here. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThis PR addresses unbounded memory growth in long-running loop executions (issue #2525) by introducing three targeted fixes: a sliding window cap on Critical issue: Confidence Score: 2/5
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 >\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 >\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]
|
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>
There was a problem hiding this comment.
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, | ||
| }) | ||
| } |
There was a problem hiding this comment.
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.


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)allIterationOutputsgrows without limit — every iteration's block outputs are retained foreverMAX_LOOP_ITERATION_HISTORY(100) iterations, discarding oldest entries2. Block log pruning (
block-executor.ts)ctx.blockLogsaccumulates every block execution with full input/output dataMAX_BLOCK_LOGS(10,000)3. Stream buffer cleanup (
block-executor.ts)chunksarray inconsumeExecutorStreamholds all stream data references after joining4. New constants (
constants.ts)DEFAULTS.MAX_LOOP_ITERATION_HISTORY = 100— configurable sliding window sizeDEFAULTS.MAX_BLOCK_LOGS = 10_000— configurable log buffer limitTest plan
loop-memory.test.tsverifying sliding window and log pruning behaviorGenerated with Claude Code