({
criticalBytes,
highBytes,
intervalMs = 10_000,
onCritical,
onHigh,
onWarn,
warnBytes = 600 * MB
}: MemoryMonitorOptions = {})
| 88 | } |
| 89 | |
| 90 | export function startMemoryMonitor({ |
| 91 | criticalBytes, |
| 92 | highBytes, |
| 93 | intervalMs = 10_000, |
| 94 | onCritical, |
| 95 | onHigh, |
| 96 | onWarn, |
| 97 | warnBytes = 600 * MB |
| 98 | }: MemoryMonitorOptions = {}): () => void { |
| 99 | const { critical, high } = resolveThresholds(criticalBytes, highBytes) |
| 100 | const dumped = new Set<Exclude<MemoryLevel, 'normal'>>() |
| 101 | const inFlight = new Set<Exclude<MemoryLevel, 'normal'>>() |
| 102 | |
| 103 | // Early-warning state (#34095): the silent-death regime is BELOW `high`, so |
| 104 | // the level machine above never sees it. Track the previous sample and fire |
| 105 | // onWarn at most once when heap both crosses a modest absolute floor AND is |
| 106 | // climbing steeply (≥150MB between 10s ticks) — the signature of a render- |
| 107 | // tree blowup — so the user gets a visible heads-up before Node OOMs under |
| 108 | // the exit threshold. Re-armed only after heap falls back below the floor. |
| 109 | // `lastHeap < 0` marks the un-seeded first sample so a cold start that opens |
| 110 | // already-high can't be mistaken for sudden growth (growth = current - last). |
| 111 | let lastHeap = -1 |
| 112 | let warned = false |
| 113 | const WARN_GROWTH_STEP = 150 * MB |
| 114 | |
| 115 | // Cooldown prevents repeated auto dumps when heap oscillates around the |
| 116 | // threshold (issue #21767). `dumped` alone is not enough — it clears on |
| 117 | // every transition back to `normal`. |
| 118 | const cooldownRaw = process.env.CLAWCODEX_AUTO_HEAPDUMP_COOLDOWN_MS?.trim() |
| 119 | const cooldownParsed = cooldownRaw ? Number(cooldownRaw) : NaN |
| 120 | const cooldownMs = Number.isFinite(cooldownParsed) && cooldownParsed >= 0 ? cooldownParsed : 600_000 |
| 121 | let lastAutoDumpAt = 0 |
| 122 | |
| 123 | const tick = async () => { |
| 124 | const { heapUsed, rss } = process.memoryUsage() |
| 125 | |
| 126 | // Sub-threshold abnormal-growth warning. Skip on the first (un-seeded) |
| 127 | // sample — we need a prior reading to measure a delta against. |
| 128 | if (heapUsed < high && lastHeap >= 0) { |
| 129 | if (!warned && heapUsed >= warnBytes && heapUsed - lastHeap >= WARN_GROWTH_STEP) { |
| 130 | warned = true |
| 131 | onWarn?.({ heapUsed, level: 'normal', rss }) |
| 132 | } else if (heapUsed < warnBytes) { |
| 133 | warned = false |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | lastHeap = heapUsed |
| 138 | |
| 139 | const level: MemoryLevel = heapUsed >= critical ? 'critical' : heapUsed >= high ? 'high' : 'normal' |
| 140 | |
| 141 | if (level === 'normal') { |
| 142 | dumped.clear() |
| 143 | |
| 144 | return |
| 145 | } |
| 146 | |
| 147 | if (dumped.has(level) || inFlight.has(level)) { |
no test coverage detected