Skip to content

fix(agents): eliminate EMFILE from SSE tick scanning all meta.json files - #161

Open
hilash wants to merge 1 commit into
mainfrom
fix/emfile-conversation-meta-reads
Open

fix(agents): eliminate EMFILE from SSE tick scanning all meta.json files#161
hilash wants to merge 1 commit into
mainfrom
fix/emfile-conversation-meta-reads

Conversation

@hilash

@hilash hilash commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • getRunningConversationCounts was opening every meta.json across every cabinet on every 3-second SSE tick with no concurrency limit. Under multiple open rooms, the burst exhausted the OS fd limit (EMFILE / os error 24), which also starved turbopack's SST file reads.
  • Replaced with an in-memory Map bootstrapped once at startup; enqueueConversationNotification delta-updates it on every status transition — no disk reads on subsequent ticks.
  • Added mapCapped to bound concurrent file opens in listConversationMetas during the bootstrap scan (4 cabinets × 20 entries in flight max).

Test plan

  • Open multiple cabinets (rooms) simultaneously and confirm no EMFILE errors in the dev log
  • Start and complete an agent run — confirm the running badge updates correctly in the sidebar
  • Restart dev server — confirm running counts are correct on first SSE tick (bootstrap from disk)

Summary by CodeRabbit

  • Bug Fixes
    • Improved efficiency of running conversation tracking by maintaining cached counts, eliminating repeated file scans during real-time updates.
    • Reduced file-descriptor pressure through optimized metadata listing with concurrency controls.

Running counts were re-read from disk on every 3-second SSE tick by
opening every meta.json across every cabinet concurrently. Under multiple
open cabinets (rooms) the burst of parallel opens exhausted the OS fd
limit (EMFILE, os error 24), which also starved turbopack.

Two fixes:
- getRunningConversationCounts now keeps an in-memory Map bootstrapped
  once at startup; enqueueConversationNotification delta-updates it on
  every status transition so no disk reads are needed on subsequent ticks.
- listConversationMetas now uses mapCapped (4 cabinets × 20 entries) to
  bound concurrent file opens during the bootstrap scan and other callers.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

conversation-store.ts gains an in-memory _runningCounts map per agent slug, bootstrapped once from disk on first use and kept current via increments/decrements inside enqueueConversationNotification. listConversationMetas is rewritten to use a new mapCapped concurrency-limiting helper, and getRunningConversationCounts now returns the cached map instead of recomputing via reduce.

Changes

Running counts cache and concurrency cap

Layer / File(s) Summary
In-memory running counts state and notification sync
src/lib/agents/conversation-store.ts
Adds _runningCounts Map and _runningCountsBootstrapped flag; updates enqueueConversationNotification to increment the map on running status and decrement/remove it on completed or failed when bootstrapped.
mapCapped, listConversationMetas rewrite, and getRunningConversationCounts refactor
src/lib/agents/conversation-store.ts
Introduces mapCapped to cap concurrency during cabinet and conversation directory scans; replaces unconstrained Promise.all in listConversationMetas; refactors getRunningConversationCounts to bootstrap _runningCounts once and return the cached map on all subsequent calls.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 A map was born, no disk to trawl,
Each running tick, no re-scan at all.
With mapCapped set, the fds stay tame,
One bootstrap call, then back the same.
Hop hop, the cache has caught the flame! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing EMFILE errors caused by SSE tick scanning all meta.json files, which aligns with the PR's primary objective of eliminating file descriptor exhaustion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/emfile-conversation-meta-reads

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/agents/conversation-store.ts`:
- Around line 175-176: The conditional check at line 175 only decrements the
running conversation count for "completed" or "failed" statuses, but the
"cancelled" status should also be treated as a terminal state that decrements
the counter. Modify the condition that checks notification.status to include
"cancelled" alongside "completed" and "failed" so that when a conversation is
cancelled, the _runningCounts.get(slug) value is properly decremented and the
badge count stays accurate.
- Around line 1818-1824: The bootstrap logic guarded by
_runningCountsBootstrapped has a race condition where multiple concurrent
callers can all observe the flag as false and execute the bootstrap
concurrently, causing duplicate increments in _runningCounts. Fix this by
introducing a shared in-flight promise variable (e.g.,
_runningCountsBootstrapPromise) that is set before the bootstrap begins. Check
if this promise exists and await it if it does; otherwise, create the promise,
execute the bootstrap logic that fetches running conversations and populates
_runningCounts, and then mark _runningCountsBootstrapped as true. This ensures
only one bootstrap execution occurs while concurrent callers wait for the same
promise to resolve.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8dc50828-f19c-47c3-a045-49e33d7d63cf

📥 Commits

Reviewing files that changed from the base of the PR and between 5842d10 and 60071f3.

📒 Files selected for processing (1)
  • src/lib/agents/conversation-store.ts

Comment on lines +175 to +176
} else if (notification.status === "completed" || notification.status === "failed") {
const cur = _runningCounts.get(slug) ?? 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle cancelled as a terminal decrement state.

Line 175 only decrements for "completed"/"failed". If a running conversation transitions to "cancelled", the cached running count never drops and badges stay stale.

Suggested fix
-    } else if (notification.status === "completed" || notification.status === "failed") {
+    } else if (
+      notification.status === "completed" ||
+      notification.status === "failed" ||
+      notification.status === "cancelled"
+    ) {
       const cur = _runningCounts.get(slug) ?? 0;
       if (cur <= 1) _runningCounts.delete(slug);
       else _runningCounts.set(slug, cur - 1);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (notification.status === "completed" || notification.status === "failed") {
const cur = _runningCounts.get(slug) ?? 0;
} else if (
notification.status === "completed" ||
notification.status === "failed" ||
notification.status === "cancelled"
) {
const cur = _runningCounts.get(slug) ?? 0;
if (cur <= 1) _runningCounts.delete(slug);
else _runningCounts.set(slug, cur - 1);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/agents/conversation-store.ts` around lines 175 - 176, The conditional
check at line 175 only decrements the running conversation count for "completed"
or "failed" statuses, but the "cancelled" status should also be treated as a
terminal state that decrements the counter. Modify the condition that checks
notification.status to include "cancelled" alongside "completed" and "failed" so
that when a conversation is cancelled, the _runningCounts.get(slug) value is
properly decremented and the badge count stays accurate.

Comment on lines +1818 to +1824
if (!_runningCountsBootstrapped) {
const running = await listConversationMetas({ status: "running", limit: 1000 });
for (const m of running) {
_runningCounts.set(m.agentSlug, (_runningCounts.get(m.agentSlug) ?? 0) + 1);
}
_runningCountsBootstrapped = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard bootstrap with a shared in-flight promise to prevent double-counting.

Line 1818 has a race: concurrent callers can all observe false, run bootstrap, and each increment _runningCounts, inflating counts.

Suggested fix
 const _runningCounts = new Map<string, number>();
 let _runningCountsBootstrapped = false;
+let _runningCountsBootstrapPromise: Promise<void> | null = null;

 export async function getRunningConversationCounts(): Promise<Record<string, number>> {
   if (!_runningCountsBootstrapped) {
-    const running = await listConversationMetas({ status: "running", limit: 1000 });
-    for (const m of running) {
-      _runningCounts.set(m.agentSlug, (_runningCounts.get(m.agentSlug) ?? 0) + 1);
-    }
-    _runningCountsBootstrapped = true;
+    _runningCountsBootstrapPromise ??= (async () => {
+      const running = await listConversationMetas({ status: "running", limit: 1000 });
+      _runningCounts.clear();
+      for (const m of running) {
+        _runningCounts.set(m.agentSlug, (_runningCounts.get(m.agentSlug) ?? 0) + 1);
+      }
+      _runningCountsBootstrapped = true;
+    })();
+    await _runningCountsBootstrapPromise;
   }
   return Object.fromEntries(_runningCounts);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!_runningCountsBootstrapped) {
const running = await listConversationMetas({ status: "running", limit: 1000 });
for (const m of running) {
_runningCounts.set(m.agentSlug, (_runningCounts.get(m.agentSlug) ?? 0) + 1);
}
_runningCountsBootstrapped = true;
}
if (!_runningCountsBootstrapped) {
_runningCountsBootstrapPromise ??= (async () => {
const running = await listConversationMetas({ status: "running", limit: 1000 });
_runningCounts.clear();
for (const m of running) {
_runningCounts.set(m.agentSlug, (_runningCounts.get(m.agentSlug) ?? 0) + 1);
}
_runningCountsBootstrapped = true;
})();
await _runningCountsBootstrapPromise;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/agents/conversation-store.ts` around lines 1818 - 1824, The bootstrap
logic guarded by _runningCountsBootstrapped has a race condition where multiple
concurrent callers can all observe the flag as false and execute the bootstrap
concurrently, causing duplicate increments in _runningCounts. Fix this by
introducing a shared in-flight promise variable (e.g.,
_runningCountsBootstrapPromise) that is set before the bootstrap begins. Check
if this promise exists and await it if it does; otherwise, create the promise,
execute the bootstrap logic that fetches running conversations and populates
_runningCounts, and then mark _runningCountsBootstrapped as true. This ensures
only one bootstrap execution occurs while concurrent callers wait for the same
promise to resolve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant