Repository navigation
Bug: Tool.define() accumulates wrapper closures: unbounded memory leak + RangeError crash in server mode #17047
Description
Description
Tool.define() in src/tool/tool.ts:55 has an unbounded memory leak that causes RangeError: Maximum call stack size exceeded in long-running processes.
When init is an object literal (not a function), Tool.define() returns the same object reference on every init() call. Each call wraps toolInfo.execute with a new closure layer that calls the previous one, building an ever-growing chain:
Call 1: toolInfo.execute = wrap₁(original)
Call 2: toolInfo.execute = wrap₂(wrap₁(original))
Call N: toolInfo.execute = wrapₙ(wrapₙ₋₁(...wrap₁(original)))
Since init() is called on every agentic step (via prompt.ts → resolveTools() → ToolRegistry.tools() → t.init()), the chain grows with every tool-call round-trip across all sessions. Each wrapper closure is retained in memory permanently. After ~1,000+ steps, invoking any affected tool recurses through the entire chain and crashes.
Impact
- Memory leak: Each wrapper closure is ~1KB. At production traffic (~800 sessions/hr, ~10 steps/session), this leaks ~8MB/hr of closures that can never be GC'd
- Crash: After ~1,000+ accumulated steps, any object-defined tool throws
RangeError: Maximum call stack size exceeded - Silent failure: The error is caught by the AI SDK tool wrapper and passed as a tool-error event. No stack trace appears in OpenCode logs — the failure is invisible
- Server mode: Particularly severe for
opencode servesince the process is long-lived. In TUI mode, single sessions rarely hit the threshold
Affected tools
Affected (object-defined init) |
Safe (function-defined init) |
|---|---|
| Read, Glob, Grep, Edit, Write, TodoRead, TodoWrite | Bash, Task, WebSearch, Skill, Batch |
Function-defined tools return a fresh object each init() call, so nothing accumulates.
The fix
One-line change in tool.ts:55 — spread to create a fresh copy:
// Before (bug): reuses same object, wrappers accumulate
const toolInfo = init instanceof Function ? await init(initCtx) : init
// After (fix): fresh copy each time, original never mutated
const toolInfo = init instanceof Function ? await init(initCtx) : { ...init }PR with fix + regression tests: #16952
Stack trace
Captured via diagnostic instrumentation on a production-like workload:
RangeError: Maximum call stack size exceeded.
at containsPath (src/project/instance.ts:97:29)
at assertExternalDirectory (src/tool/external-directory.ts:17:16)
at execute (src/tool/read.ts:45:11)
at <anonymous> (src/tool/tool.ts:69:32)
at <anonymous> (src/tool/tool.ts:69:32)
at <anonymous> (src/tool/tool.ts:69:32)
[... tool.ts:69 repeated hundreds of times ...]
OpenCode version
v1.2.24 (bug exists on current dev as well)
Steps to reproduce
- Run
opencode serve - Send ~1,000+ agentic requests with tool calls (across any number of sessions)
- Any object-defined tool (Read, Glob, Grep, Edit, Write) crashes with
RangeError: Maximum call stack size exceeded
Alternatively, call tool.init() in a loop — after ~1,000 iterations the wrapper chain is deep enough to overflow:
const tool = Tool.define("test", { description: "test", parameters: z.object({ input: z.string() }), async execute() { return { title: "", output: "ok", metadata: {} } } })
for (let i = 0; i < 2000; i++) await tool.init()
const resolved = await tool.init()
await resolved.execute({ input: "hello" }, ctx) // RangeErrorOperating System
Linux x86_64 (Amazon Linux 2023) — also reproduced on macOS
Activity
heimoshuiyu commentedon Mar 24, 2026
Can reproduce on ArchLinux x86_64 v1.3.0
{ "type": "tool", "callID": "call_iKSigtfPz2PRnvvBgyFskrEf", "tool": "list", "state": { "status": "error", "input": { "path": "/home/hmsy/workspace", "depth": 2 }, "error": "RangeError: Maximum call stack size exceeded.", "time": { "start": 1774239750763, "end": 1774239750771 } }, "metadata": { "openai": { "itemId": "fc_01da21a2b4b5b6e90169c0c006a6048191a497ece6c9f67c00" } } }jpcarranza94 commentedon Apr 1, 2026
Hey @rekram1-node! This is a quite problematic bug in the context in which we are using opencode, thousands of sessions.
Since I implemented this fix I have not encountered any stack overflow type errors even when running the server for more than 24 hours. We have also not encountered any other problems resulting from this simple fix (though we still have a cron to restart the opencode server every 24 hours because of other unidentified memory leaks).
This is a very simple fix and is definitely a simple way of reducing an existing memory leak on the production version of opencode. The fix would also open more creative uses of opencode outside of just coding to other users.
Any chance this can be reviewed soon so that we can stop using our fork and go back to the main version? Thanks a lot in advance!
[-]Bug: Tool.define() accumulates wrapper closures — unbounded memory leak + RangeError crash in server mode[/-][+]Bug: Tool.define() accumulates wrapper closures: unbounded memory leak + RangeError crash in server mode[/+]thdxr commentedon Apr 2, 2026
Closing in favor of #20695 where we're collecting all memory-related problems. Please follow that issue for updates.