Problem
computeBriefingFingerprint (src/services/briefing-cache.ts) fingerprints the
workspace activity-log dir and every running bundle's entity-data dir by walking
each tree with synchronous readdirSync + lstatSync (dirSignature). It runs
inside the async nb__briefing tool handler (src/tools/core-source.ts:754), on
every briefing call that is not a force_refresh.
Bun runs on a single event-loop thread. A synchronous directory walk blocks that
thread — no other request (chat stream, tool call, page load) is served until the
walk returns.
Today this is cheap: fresh workspaces have a handful of log files and small entity
dirs. But entity-backed bundles (todo, CRM, notes) store one file per row/note, so
a long-lived heavy workspace accumulates thousands of files. Every home-dashboard
load then stat()s every one of them synchronously, freezing the process for the
walk duration — and the freeze lands on unrelated requests, not just that user.
Fix
Make the walk async — fs/promises readdir / lstat, computeBriefingFingerprint
returns Promise<string>, await it at the core-source.ts call site. The thread
can serve other requests while waiting on disk.
Optional follow-on: cap the walk (max file count / max depth) and fall back to the
TTL if a tree is too large to fingerprint cheaply.
Problem
computeBriefingFingerprint(src/services/briefing-cache.ts) fingerprints theworkspace activity-log dir and every running bundle's entity-data dir by walking
each tree with synchronous
readdirSync+lstatSync(dirSignature). It runsinside the async
nb__briefingtool handler (src/tools/core-source.ts:754), onevery briefing call that is not a
force_refresh.Bun runs on a single event-loop thread. A synchronous directory walk blocks that
thread — no other request (chat stream, tool call, page load) is served until the
walk returns.
Today this is cheap: fresh workspaces have a handful of log files and small entity
dirs. But entity-backed bundles (todo, CRM, notes) store one file per row/note, so
a long-lived heavy workspace accumulates thousands of files. Every home-dashboard
load then stat()s every one of them synchronously, freezing the process for the
walk duration — and the freeze lands on unrelated requests, not just that user.
Fix
Make the walk async —
fs/promisesreaddir/lstat,computeBriefingFingerprintreturns
Promise<string>,awaitit at thecore-source.tscall site. The threadcan serve other requests while waiting on disk.
Optional follow-on: cap the walk (max file count / max depth) and fall back to the
TTL if a tree is too large to fingerprint cheaply.