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
-
Launch forge (observed on v2.13.13 → v2.13.14, model MiniMax-M3, host macos-arm64).
-
Drive an active chat for ~3 minutes (long enough for the stream to re-iterate through the leak threshold).
-
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
-
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)
Summary
In long-running sessions, forge emits a Node.js
MaxListenersExceededWarningfrom a stack frame atcontentscript.js:14083(functionn) every time an internal stream reaches its 11th iteration. The warning fires twice in immediate succession — once forcloselisteners and once forendlisteners — 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
Launch
forge(observed onv2.13.13→v2.13.14, modelMiniMax-M3, hostmacos-arm64).Drive an active chat for ~3 minutes (long enough for the stream to re-iterate through the leak threshold).
Observe in the captured session log (
log.md:52-58of the user's scratch repo):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 identical11count, the identical line14083, and the identical function namen.Expected
The stream/EventEmitter should not accumulate listeners. Either:
once()-style and should be registered as such, ORFailing 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
closeandendlisteners 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
contentscript.js:14083line is the only source location — meaning a single, easily-fixable code site is responsible.setMaxListenersmasks the root cause; the listeners themselves still pile up.Source is opaque from the public repo
Searching the
tailcallhq/forgecoderepo for the source of these warnings:gh search code 'contentscript' repo:tailcallhq/forgecode→ 0 matchesgh search code 'setMaxListeners' repo:tailcallhq/forgecode→ 0 matchesgh search code 'MaxListenersExceededWarning' repo:tailcallhq/forgecode→ 0 matchesstrings ~/.local/bin/forge | grep setMaxListeners→ 0 matchesstrings ~/.local/bin/forge | grep MaxListenersExceededWarning→ 0 matchesThe file is not in the public source tree, not in the released binary, and not in
~/.forge/. It is most likely:Action requested: identify the source of the
contentscript.js:14083frame 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)
Also: add a
paste.classify/stream.lifecycleOTEL 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 theAddCompatibleProviderModalonCreatedcallback (3 modal flavors atpage.tsx:1748,page.tsx:1758,page.tsx:1770). Tracked separately ondiegosouzapw/OmniRoute.Labels
type: bug,help(extra attention — source is opaque, needs maintainer triage first)