You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Disclaimer: This report — including the root-cause analysis, line references, and suggested fix direction — was produced with the assistance of an AI agent investigating a crash in a private monorepo. The reproduction (cold-OK / warm-crash) and the 6.17.1 / 6.18.0 behaviors described below were observed directly, but the source-level explanation may contain inaccuracies. Please verify before acting on it.
Summary
With --cache, the first (cold) run succeeds but the second (warm) run fails deterministically:
RangeError: Invalid array length
at Array.push (<anonymous>)
at WorkspaceWorker.runPlugins (.../knip/dist/WorkspaceWorker.js:407:40)
at async build (.../knip/dist/graph/build.js:133:35)
Running without --cache always works. This makes --cache unusable in a pre-commit hook: the first commit passes, every subsequent one fails until node_modules/.cache/knip is deleted.
Confirmed on both 6.17.1 (warm run throws the RangeError at ~26s) and 6.18.0 (warm run does not throw — instead it spins at 100% CPU with steadily climbing memory, i.e. the same runaway loop heading toward OOM rather than the array-length cap). Without --cache, both versions complete normally.
Root cause (traced in dist/WorkspaceWorker.js, 6.17.1)
runPlugins accumulates into a single array and drains a per-workspace plugin → config-files map:
On a warm cache, each config file takes the cached branch, which re-adds the config back into the map being iterated and then continues without updating the seen set that the non-cached path uses to dedupe:
if(fd?.meta?.data&&!fd.changed){// L294constdata=fd.meta.data;if(data.resolveConfig)for(constidofdata.resolveConfig)addInput(id,configFilePath);if(data.resolveFromAST)for(constidofdata.resolveFromAST)addInput(id,configFilePath);if(data.configFile)addInput(data.configFile);// re-stores config filecontinue;// L304 — seen never updated (cf. L375-377)}
constaddInput=(input, ...)=>{if(isConfig(input))storeConfigFilePath(input.pluginName,{...});// re-inserts into configFilesMapelseinputs.push(...);};
So:
The cached read calls addInput(data.configFile) → storeConfigFilePath → re-inserts the config-file entry into the same configFilesMap Map that the for…of at L403 is iterating.
Per the JS spec, a Map iterator yields entries inserted during iteration. The re-added entry is visited again → plugin re-runs → re-adds it again → visited again → … The loop never terminates.
On a cold cache the non-cached path updates seen (L375-377), so the cycle is broken and the run completes — exactly why cold passes and warm fails.
Each cycle calls inputs.push(...). On 6.17.1 the array length hits Array.prototype.push's hard ceiling of 2**32 - 1 (4,294,967,295) and throws RangeError: Invalid array length (~26s). On 6.18.0 the same loop instead grows memory without bound (100% CPU, RSS climbing past 2 GB within minutes), so it trends toward OOM before reaching the length cap. Either way the loop never terminates — this is not plugin output volume but the same handful of cached config entries re-processed indefinitely.
Cache evidence
The per-workspace cache (v8.serialized Map) for the affected workspace shows every entry carries a configFile field — the value re-stored on the warm path (only TypeScript + Vitest config files are involved here):
Large Nx + pnpm monorepo (nx: false, ~95 auto-discovered workspaces, includeEntryExports: true, ESLint/Vitest/Playwright/Storybook/Orval plugins). Could not reproduce on a trivial repo; appears to require a workspace whose cached config entries re-store config files.
The cached branch (L294-304) should participate in the same dedupe/termination bookkeeping as the non-cached branch — e.g. update seen for re-stored config files, and/or guard the for…of at L403 against re-processing entries already handled in the current runPlugins invocation (snapshot the keys, or track processed pluginNames in a local set), so re-insertion during iteration cannot loop forever.
Environment
knip: reproduced on 6.17.1 and 6.18.0 (latest)
Node: v24.15.0
pnpm: 11.1.3
OS: macOS
Happy to test a patch/branch or provide more --debug output. The cold-OK / warm-fail cycle is 100% reproducible locally.
Note
Disclaimer: This report — including the root-cause analysis, line references, and suggested fix direction — was produced with the assistance of an AI agent investigating a crash in a private monorepo. The reproduction (cold-OK / warm-crash) and the 6.17.1 / 6.18.0 behaviors described below were observed directly, but the source-level explanation may contain inaccuracies. Please verify before acting on it.
Summary
With
--cache, the first (cold) run succeeds but the second (warm) run fails deterministically:Running without
--cachealways works. This makes--cacheunusable in a pre-commit hook: the first commit passes, every subsequent one fails untilnode_modules/.cache/knipis deleted.Confirmed on both 6.17.1 (warm run throws the
RangeErrorat ~26s) and 6.18.0 (warm run does not throw — instead it spins at 100% CPU with steadily climbing memory, i.e. the same runaway loop heading toward OOM rather than the array-length cap). Without--cache, both versions complete normally.Root cause (traced in
dist/WorkspaceWorker.js, 6.17.1)runPluginsaccumulates into a single array and drains a per-workspace plugin → config-files map:On a warm cache, each config file takes the cached branch, which re-adds the config back into the map being iterated and then
continues without updating theseenset that the non-cached path uses to dedupe:So:
addInput(data.configFile)→storeConfigFilePath→ re-inserts the config-file entry into the sameconfigFilesMapMap that thefor…ofat L403 is iterating.Mapiterator yields entries inserted during iteration. The re-added entry is visited again → plugin re-runs → re-adds it again → visited again → … The loop never terminates.seen(L375-377), so the cycle is broken and the run completes — exactly why cold passes and warm fails.Each cycle calls
inputs.push(...). On 6.17.1 the array length hitsArray.prototype.push's hard ceiling of2**32 - 1(4,294,967,295) and throwsRangeError: Invalid array length(~26s). On 6.18.0 the same loop instead grows memory without bound (100% CPU, RSS climbing past 2 GB within minutes), so it trends toward OOM before reaching the length cap. Either way the loop never terminates — this is not plugin output volume but the same handful of cached config entries re-processed indefinitely.Cache evidence
The per-workspace cache (
v8.serializedMap) for the affected workspace shows every entry carries aconfigFilefield — the value re-stored on the warm path (only TypeScript + Vitest config files are involved here):Reproduction
Large Nx + pnpm monorepo (
nx: false, ~95 auto-discovered workspaces,includeEntryExports: true, ESLint/Vitest/Playwright/Storybook/Orval plugins). Could not reproduce on a trivial repo; appears to require a workspace whose cached config entries re-store config files.--debugon the warm run (the workspace name is the first one where a cached config re-stores a config file):Suggested fix direction
The cached branch (L294-304) should participate in the same dedupe/termination bookkeeping as the non-cached branch — e.g. update
seenfor re-stored config files, and/or guard thefor…ofat L403 against re-processing entries already handled in the currentrunPluginsinvocation (snapshot the keys, or track processedpluginNames in a local set), so re-insertion during iteration cannot loop forever.Environment
6.17.1and6.18.0(latest)v24.15.011.1.3Happy to test a patch/branch or provide more
--debugoutput. The cold-OK / warm-fail cycle is 100% reproducible locally.