github / github/copilot-cli

Node.js OOM crash after ~37 min — 31,965 leaked async libuv handles (SEA ignores NODE_OPTIONS)

Ouverte
#4,686 3 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

area:sessions
Langage dominant
Shell
Étoiles
11.2k
Forks
1.9k
Merge moyen
14 h 16 min
PR mergées (30 j)
6

Description

Bug: Node.js OOM crash after ~37 minutes — 31,965 leaked async libuv handles

Version: 1.0.82
Platform: Linux x86_64 (Amazon EC2, Node v24.20.0 embedded in SEA)
Observed: 2026-09-01, crash at ~37 min session uptime

Summary

Every session crashes with FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory after roughly 30–60 minutes of use. The crash is caused by a monotonic leak of async libuv handles (~32k by crash time), not by conversation-context growth. /compact and increasing NODE_OPTIONS (which the SEA ignores) do not prevent the crash.

Heap report metrics (from report.20260901.153402.2817455.0.001.json)

Metric Value
Session uptime at crash 2,203 s (~37 min)
old_space used / capacity 3,899 MB / 3,899 MB (100% saturated)
externalMemory 36 MB
mallocedMemory ~1 MB
memoryLimit 4,298 MB (~4.0 GB)
RSS at crash 4,634 MB
GC current mu 0.173 (thrashing — normally >0.5 is healthy)
CPU at crash ~292% (GC threads saturating 3 cores)

Growth rate: ~100 MB/min of old_space objects throughout the session.

libuv handle leak

At crash time, the report shows:

libuv handle count: 31,996
  type=async, is_active=true, is_referenced=false: 31,965
  type=signal, is_active=true:                         19
  • 31,965 active-but-unreferenced async handles — every one holds a JS closure/context in old_space. With no detail string (details: ""), these are likely AsyncResource / AsyncLocalStorage contexts that were created but never destroyed.
  • 19 active signal handles (vs. the ~17 signals that exist): duplicate SIGABRT, SIGIO registrations suggest repeated process.on('signal', ...) calls without corresponding removeListener — consistent with code that re-registers handlers per turn or per tool call.

The combination is a textbook listener/async-context leak: each model turn or tool execution allocates one or more async handles that are never cleaned up. After ~32k turns/tool calls the heap saturates.

Why NODE_OPTIONS and heap increases appear to help but don't

The copilot binary is a Node Single Executable Application (SEA) (NODE_SEA_BLOB present; 159 MB on disk). SEAs ignore NODE_OPTIONS entirely — empirically verified: NODE_OPTIONS="--this-is-not-a-real-flag" copilot --version exits 0 with no error.

The commandLine in the crash report confirms only Copilot's own flags are applied:

['copilot','--no-warnings','--report-on-fatalerror','--optimize-for-size','--expose-gc','copilot']

Increasing the (ignored) NODE_OPTIONS heap size is a placebo — the 4 GB cap is always in effect.

Reproduction pattern

The crash is reproducible by running a session of moderate length (>30 min) with regular tool calls (bash, view, grep). The leak appears to be per tool execution or per model turn rather than per session.

The --report-on-fatalerror flag already embedded in the binary's commandLine will produce a report.YYYYMMDD.*.json in the CWD on every crash — these reports contain full diagnostics.

Suggested investigation areas

  1. AsyncResource / AsyncLocalStorage contexts created per turn/tool that escape cleanup (search for new AsyncResource or AsyncLocalStorage usages in app.js)
  2. process.on(signal, ...) calls without process.removeListener — each session re-registration without cleanup would explain the 19 duplicate signal handles
  3. Any setInterval/setTimeout created per tool call that is never clearInterval-ed — these register as async handles too

Workaround (until fixed)

Run the Copilot JS dist under a real node with a larger heap:

~/.nvm/versions/node/v24.20.0/bin/node \
    --max-old-space-size=12288 \
    --expose-gc --no-warnings --optimize-for-size \
    ~/.cache/copilot/pkg/linux-x64/1.0.82/index.js \
    "$@"

This buys ~2 hours before the same leak exhausts 12 GB — same leak, bigger bucket.

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par reproduire une session avec des appels d’outils normaux et examinez le rapport généré report.YYYYMMDD.*.json, puis lisez le code de gestion du contexte asynchrone et des signaux dans app.js. Recherchez les chemins mentionnés liés à AsyncResource, AsyncLocalStorage, process.on, timer et cleanup ; le travail est considéré comme terminé lorsque le nombre de handles n’augmente plus de manière monotone et que les sessions prolongées évitent l’OOM signalé.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript, node.js
Domaine
cli, devtools, performance
Type d'issue
Bug
Difficulté
5/5
Temps estimé
Plus d'une semaine
Activité
Active
Clarté
À clarifier
Accessibilité débutants
28/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.