Skip to content

Bug: Tool.define() accumulates wrapper closures: unbounded memory leak + RangeError crash in server mode #17047

Closed

Description

@jpcarranza94

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 serve since 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

  1. Run opencode serve
  2. Send ~1,000+ agentic requests with tool calls (across any number of sessions)
  3. 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) // RangeError

Operating System

Linux x86_64 (Amazon Linux 2023) — also reproduced on macOS

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Mar 11, 2026
  2. heimoshuiyu commented on Mar 24, 2026

    @heimoshuiyu
    Contributor

    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"
            }
        }
    }
    
  3. jpcarranza94 commented on Apr 1, 2026

    @jpcarranza94
    ContributorAuthor

    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!

  4. changed the title [-]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[/+] on Apr 1, 2026
  5. thdxr commented on Apr 2, 2026

    @thdxr
    Member

    Closing in favor of #20695 where we're collecting all memory-related problems. Please follow that issue for updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions