Summary
swarmvault compile throws EMFILE: too many open files for a large fraction of pages during the embedding-build phase, on every run, once a vault grows to ~18k pages. The failures are swallowed into a warning and printed as [swarmvault] Warning: could not read page <path> for embedding, with no error code/message surfaced — so the real cause (EMFILE) is invisible unless you patch the CLI locally to log it.
swarmvault retrieval rebuild against the same vault does not reproduce this — only the full compile pipeline does.
Environment
@swarmvaultai/cli 3.20.0 (swarmvault --version / package.json)
- Windows 11
- Vault size at time of repro: 7,914 sources / 18,423 pages / 15,429 nodes / 65,659 edges (
swarmvault doctor --json)
Steps to reproduce
- Have a vault with a few thousand+ pages under
wiki/.
- Run
swarmvault compile from the vault root.
- Watch stderr.
Actual behavior
Stderr fills with warnings like:
[swarmvault] Warning: could not read page graph/communities/global-module-2617.md for embedding
[swarmvault] Warning: could not read page graph/communities/handler-module-2628.md for embedding
...
In our repro this printed 159,641 such warnings across the run (before we killed it), i.e. close to the total page count (18,423) repeated across ~9 internal calls to the embedding-build step.
We patched a local copy of the CLI to log the actual caught error instead of discarding it (see root cause below), and reran. The real error for every one of these is:
EMFILE: too many open files, open 'C:\Repos\wiki\graph\communities\global-module-2617.md'
For comparison, swarmvault retrieval rebuild (a narrower command against the same, quiescent vault) completes with "warnings":[] every time — no EMFILE, no missing pages. swarmvault doctor also reports a clean vault ("ok":true) both before and after a compile run that hit this, so the graph/retrieval index itself isn't corrupted — the embedding step is just silently losing most of its input.
Root cause
loadPageContents in @swarmvaultai/engine (src/embeddings.ts, compiled into dist/chunk-*.js — we found this exact function duplicated verbatim across 30+ separate esbuild chunks, apparently once per CLI entry point):
async function loadPageContents(rootDir, graph) {
const { paths } = await loadVaultConfig(rootDir);
const contents = new Map();
await Promise.all(
graph.pages.map(async (page) => {
const absolutePath = path.join(paths.wikiDir, page.path);
const content = await fs.readFile(absolutePath, "utf8").catch(() => {
process.stderr.write(`[swarmvault] Warning: could not read page ${page.path} for embedding\n`);
return "";
});
if (content) contents.set(page.id, content);
})
);
return contents;
}
Two compounding issues:
- No concurrency limit.
graph.pages.map(...) inside Promise.all fires a fs.readFile for every page in the vault simultaneously — 18k+ concurrent opens in our case — which exceeds the OS open-file-handle ceiling (EMFILE on Windows; likely also reachable via ulimit -n on Linux/macOS for large enough vaults).
- The actual error is discarded.
.catch(() => { ...; return ""; }) drops the caught error entirely, so there's no way to distinguish "page genuinely missing/corrupt" from "OS resource exhaustion mid-run" from the CLI's own output. We only found the real cause by patching a local copy to log err.code/err.message.
Net effect: on any reasonably large vault, compile's embedding index silently loses coverage for a large, semi-random subset of pages every time it runs, without any indication in the output that it's a resource limit rather than a real per-file problem.
Suggested fix
- Throttle
loadPageContents (and any other unbounded Promise.all over graph.pages) to a bounded concurrency, e.g. a small worker-pool (p-limit-style, concurrency ~16–32) instead of firing all reads at once.
- Surface the caught error's
code/message in the warning text (or at least count-and-summarize distinct error codes at the end of compile), so a resource-exhaustion failure doesn't look identical to a missing-file failure.
- Since this logic is duplicated across ~30 bundled chunks, consider whether the build should be sharing one chunk instead of triggering esbuild to duplicate it per entry point — makes any future fix here easy to under-apply.
Workaround in the meantime
None available from the CLI surface — swarmvault compile --help has no flag to skip or defer the embedding phase, so compile + a separate retrieval rebuild isn't currently possible as a split. Only mitigation right now is reducing vault size or accepting partial embedding coverage per run.
Summary
swarmvault compilethrowsEMFILE: too many open filesfor a large fraction of pages during the embedding-build phase, on every run, once a vault grows to ~18k pages. The failures are swallowed into a warning and printed as[swarmvault] Warning: could not read page <path> for embedding, with no error code/message surfaced — so the real cause (EMFILE) is invisible unless you patch the CLI locally to log it.swarmvault retrieval rebuildagainst the same vault does not reproduce this — only the fullcompilepipeline does.Environment
@swarmvaultai/cli3.20.0 (swarmvault --version/ package.json)swarmvault doctor --json)Steps to reproduce
wiki/.swarmvault compilefrom the vault root.Actual behavior
Stderr fills with warnings like:
In our repro this printed 159,641 such warnings across the run (before we killed it), i.e. close to the total page count (18,423) repeated across ~9 internal calls to the embedding-build step.
We patched a local copy of the CLI to log the actual caught error instead of discarding it (see root cause below), and reran. The real error for every one of these is:
For comparison,
swarmvault retrieval rebuild(a narrower command against the same, quiescent vault) completes with"warnings":[]every time — no EMFILE, no missing pages.swarmvault doctoralso reports a clean vault ("ok":true) both before and after acompilerun that hit this, so the graph/retrieval index itself isn't corrupted — the embedding step is just silently losing most of its input.Root cause
loadPageContentsin@swarmvaultai/engine(src/embeddings.ts, compiled intodist/chunk-*.js— we found this exact function duplicated verbatim across 30+ separate esbuild chunks, apparently once per CLI entry point):Two compounding issues:
graph.pages.map(...)insidePromise.allfires afs.readFilefor every page in the vault simultaneously — 18k+ concurrent opens in our case — which exceeds the OS open-file-handle ceiling (EMFILE on Windows; likely also reachable viaulimit -non Linux/macOS for large enough vaults)..catch(() => { ...; return ""; })drops the caught error entirely, so there's no way to distinguish "page genuinely missing/corrupt" from "OS resource exhaustion mid-run" from the CLI's own output. We only found the real cause by patching a local copy to logerr.code/err.message.Net effect: on any reasonably large vault,
compile's embedding index silently loses coverage for a large, semi-random subset of pages every time it runs, without any indication in the output that it's a resource limit rather than a real per-file problem.Suggested fix
loadPageContents(and any other unboundedPromise.allovergraph.pages) to a bounded concurrency, e.g. a small worker-pool (p-limit-style, concurrency ~16–32) instead of firing all reads at once.code/messagein the warning text (or at least count-and-summarize distinct error codes at the end ofcompile), so a resource-exhaustion failure doesn't look identical to a missing-file failure.Workaround in the meantime
None available from the CLI surface —
swarmvault compile --helphas no flag to skip or defer the embedding phase, socompile+ a separateretrieval rebuildisn't currently possible as a split. Only mitigation right now is reducing vault size or accepting partial embedding coverage per run.