Skip to content

[Bug]: MaxListenersExceededWarning fires every session from contentscript.js:14083 — 11 close + 11 end listeners on shared stream emitter #3549

Description

@KooshaPari

Summary

In long-running sessions, forge emits a Node.js MaxListenersExceededWarning from a stack frame at contentscript.js:14083 (function n) every time an internal stream reaches its 11th iteration. The warning fires twice in immediate succession — once for close listeners and once for end listeners — indicating a shared EventEmitter (most likely a request/response stream in the SSE/chat transport) is having the same listener registered 11 times without removal.

The 11-count is stable across 4 separate emissions in the same 3-minute window of a captured session, so this is deterministic and reproducible, not a transient burst.

Repro

  1. Launch forge (observed on v2.13.13v2.13.14, model MiniMax-M3, host macos-arm64).

  2. Drive an active chat for ~3 minutes (long enough for the stream to re-iterate through the leak threshold).

  3. Observe in the captured session log (log.md:52-58 of the user's scratch repo):

    contentscript.js:14083 MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added. Use emitter.setMaxListeners() to increase limit
    n @ contentscript.js:14083
    contentscript.js:14083 MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 end listeners added. Use emitter.setMaxListeners() to increase limit
    
  4. The same warning fires 4× in the captured transcript (at log.md:52-53, log.md:54, log.md:56-57, log.md:58), each time with the identical 11 count, the identical line 14083, and the identical function name n.

Expected

The stream/EventEmitter should not accumulate listeners. Either:

  • The handler is once()-style and should be registered as such, OR
  • The previous handler should be removed before the new one is added, OR
  • A fresh emitter should be created per iteration instead of reusing the same one.

Failing that, the threshold should be raised explicitly via emitter.setMaxListeners(N) with a documented reason — and the leak should be tracked, not silenced.

Actual

A shared emitter accumulates identical close and end listeners across iterations. After 11 iterations, Node.js prints the warning to stderr. The session continues to function (no crash, no failure mode visible to the user) but the leak is unbounded: any session longer than 11 stream re-iterations will hit it, and every subsequent iteration adds another listener pair. The growth is O(2N) listeners per session for the lifetime of the emitter.

Why it matters

  • Every long session leaks. User statement: "every forge session has this issue".
  • The same contentscript.js:14083 line is the only source location — meaning a single, easily-fixable code site is responsible.
  • The leak is in a chat/stream transport path, which is the hottest code path in forge. The cost compounds with session length.
  • Suppressing the warning by raising setMaxListeners masks the root cause; the listeners themselves still pile up.

Source is opaque from the public repo

Searching the tailcallhq/forgecode repo for the source of these warnings:

  • gh search code 'contentscript' repo:tailcallhq/forgecode → 0 matches
  • gh search code 'setMaxListeners' repo:tailcallhq/forgecode → 0 matches
  • gh search code 'MaxListenersExceededWarning' repo:tailcallhq/forgecode → 0 matches
  • strings ~/.local/bin/forge | grep setMaxListeners → 0 matches
  • strings ~/.local/bin/forge | grep MaxListenersExceededWarning → 0 matches

The file is not in the public source tree, not in the released binary, and not in ~/.forge/. It is most likely:

  • A bundled/minified webview asset (Tauri webview frontend), or
  • A sourcemap-collapsed stack frame pointing to upstream Node.js / a vendored package, or
  • A child process spawned by the Rust binary whose bundle is not in this repo.

Action requested: identify the source of the contentscript.js:14083 frame and either (a) ship a fix in the same release, or (b) tell us which repo/PR hosts the file so we can send a PR.

Suggested fix (applies to whatever file is at line 14083)

- // assuming a shared emitter across iterations
- emitter.on("close", onClose);
- emitter.on("end",   onEnd);
+ // pick ONE of the following depending on intent:
+
+ // option A: one-shot handler (preferred when close is terminal)
+ emitter.once("close", onClose);
+
+ // option B: per-iteration emitter (preferred when state must persist)
+ const fresh = new PassThrough();
+ fresh.on("close", onClose);
+ fresh.on("end",   onEnd);
+ previousEmitter.removeAllListeners();
+ previousEmitter.destroy();
+
+ // option C: explicit threshold with a tracking comment
+ if (emitter.listenerCount("close") === 0) {
+   emitter.setMaxListeners(64); // see leak ticket #XXXX
+   emitter.on("close", onClose);
+ }

Also: add a paste.classify / stream.lifecycle OTEL event so future regressions are visible in traces, not just stderr.

Companion pattern

The same shape (unbounded accumulation, missing dedup) appears in the OmniRoute providers page as a setProviderNodes((prev) => [...prev, node]) pattern in the AddCompatibleProviderModal onCreated callback (3 modal flavors at page.tsx:1748, page.tsx:1758, page.tsx:1770). Tracked separately on diegosouzapw/OmniRoute.

Labels

type: bug, help (extra attention — source is opaque, needs maintainer triage first)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions