fix: prevent Tool.define() wrapper accumulation on object-defined tools#16952
Open
jpcarranza94 wants to merge 2 commits intoanomalyco:devfrom
Open
fix: prevent Tool.define() wrapper accumulation on object-defined tools#16952jpcarranza94 wants to merge 2 commits intoanomalyco:devfrom
jpcarranza94 wants to merge 2 commits intoanomalyco:devfrom
Conversation
…apper accumulation Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #17047
Type of change
What does this PR do?
Tool.define()wrapstoolInfo.executewith a validation + truncation layer each timeinit()is called. Wheninitis an object literal (not a function), line 55 returns the same object reference every call. This means eachinit()call:toolInfo.execute(already wrapped N times)wrap_N → wrap_{N-1} → ... → wrap_1 → originalThis is an unbounded memory leak — each wrapper closure (~1KB) is retained permanently and can never be GC'd. At production traffic (~800 sessions/hr, ~10 steps/session), this leaks ~8MB/hr.
After ~1,000+ accumulated steps, invoking any affected tool recurses through the entire chain and crashes with
RangeError: Maximum call stack size exceeded.Affected tools: all object-defined tools (Read, Glob, Grep, Edit, Write, TodoRead, TodoWrite). Function-defined tools (Bash, Task, WebSearch, etc.) are safe because
await init(initCtx)returns a fresh object each time.The fix: shallow-copy the object with
{ ...init }so eachinit()call gets a fresh copy. The original object is never mutated, each copy gets exactly one wrapper layer, and the copy is GC'd after the step completes. This matches how function-defined tools already work.How did you verify your code works?
1. Regression tests (
test/tool/tool-define.test.ts) — 5 tests that directly verify the fix:does not mutate the original init objectoriginal.executereference is unchanged after multipleinit()callsdoes not accumulate wrapper layersexecutestays constant (<5tool.tsframes) after 100init()callsfunction-defined tool returns fresh objectsinit()object-defined tool returns distinct objects per init()init()returns a different object reference (spread creates a copy)validation still works after many init() callsinit()cyclesTests 1 and 4 fail without the fix, confirming they catch the exact bug.
2. Stress testing — ran
opencode serveunder sustained load (~5,000 sessions/hr, 128 concurrent workers). Without the fix, RangeErrors started appearing at ~1,275 sessions. Stack trace confirmedtool.ts:69repeated hundreds of times. With the fix applied, no RangeErrors after thousands of sessions.3. Typecheck —
bun turbo typecheckpasses (all 13 packages).Screenshots / recordings
Stack trace captured from a failing instance (pre-fix), showing
tool.ts:69recursion:Checklist