Skip to content

fix: crash prevention with bounded caches and lazy subagent message loading#170

Open
psypeal wants to merge 1 commit intomatt1398:mainfrom
psypeal:fix/crash-prevention-bounded-caches
Open

fix: crash prevention with bounded caches and lazy subagent message loading#170
psypeal wants to merge 1 commit intomatt1398:mainfrom
psypeal:fix/crash-prevention-bounded-caches

Conversation

@psypeal
Copy link
Copy Markdown
Contributor

@psypeal psypeal commented Apr 5, 2026

Summary

  • Add LRU eviction (500 entries) for ProjectScanner caches to prevent unbounded growth over long sessions
  • Add invalidateCachesForSession() for single-file cache invalidation instead of purging entire project on every file-change event
  • Add renderer memory monitoring (5-min interval, 2GB warning threshold)
  • Add crash recovery with retry caps, unresponsive watchdog, and crash logging to ~/.claude/claude-devtools-crash.log
  • Add SubagentMessageCache (LRU, 10 entries, 10min TTL) for lazy-loaded subagent message bodies, distinct from the main DataCache
  • Add IPC + HTTP handlers (subagent:get-messages) for on-demand subagent message loading so the renderer can fetch bodies only when a subagent is expanded
  • Fix SubagentDetailBuilder path construction to use buildSubagentsPath() (was missing sessionId segment)

Test plan

  • pnpm typecheck && pnpm lint:fix — clean
  • pnpm test — all tests pass (including new SubagentMessageCache tests)
  • Run for 2+ hours with many session switches — verify no unbounded cache growth
  • Expand a subagent inline — verify message body loads via IPC (check main process logs)
  • Verify crash log written on renderer crash/unresponsive

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements
    • Enhanced application stability with automatic crash recovery, unresponsive window detection with forced recovery, and real-time memory usage monitoring with alerts.
    • Optimized performance through intelligent caching with automatic eviction and session-specific cache management.
    • Added subagent message lazy-loading capability for improved memory efficiency.
    • Improved diagnostic logging and crash reporting for better troubleshooting.

@coderabbitai coderabbitai bot added the bug Something isn't working label Apr 5, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces robust crash handling and memory monitoring in the main process, including a new crash logging utility and automated renderer process recovery. The chat interface is enhanced with a specialized Bash tool viewer, collapsible output sections for tool results, and Markdown preview toggles for file-related tools. Additionally, syntax highlighting is expanded to support several new languages, and a session refresh mechanism is implemented via IPC. Feedback addresses a regression in Markdown syntax highlighting, improvements to error logging for crash reports, the use of constants for IPC channels, and fixes for cache eviction logic and UI state persistence.


// If no highlighting support, return plain text as single-element array
if (keywords.size === 0 && !['json', 'css', 'html', 'bash', 'markdown'].includes(language)) {
if (keywords.size === 0 && !['json', 'css', 'bash'].includes(language)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Removing markdown from the exclusion list causes a regression in syntax highlighting for Markdown files. Since markdown is not present in the KEYWORDS map, keywords.size will be 0, and the function will now return the plain text line immediately, bypassing the basic tokenization (strings, numbers, comments) that was previously applied.

Suggested change
if (keywords.size === 0 && !['json', 'css', 'bash'].includes(language)) {
if (keywords.size === 0 && !['json', 'css', 'bash', 'markdown'].includes(language)) {

Comment thread src/main/index.ts
const entry =
`[${new Date().toISOString()}] ${label}\n` +
Object.entries(details)
.map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using JSON.stringify(v) for error details will result in an empty object {} if v is an Error instance, as error properties like message and stack are non-enumerable. It's better to explicitly handle Error objects to ensure useful information is logged.

Suggested change
.map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
.map(([k, v]) => ` ${k}: ${v instanceof Error ? v.stack ?? v.message : (typeof v === 'string' ? v : JSON.stringify(v))}`)

Comment thread src/main/index.ts
// hence the IPC bridge.
if ((input.control || input.meta) && !input.shift && input.key.toLowerCase() === 'r') {
event.preventDefault();
mainWindow.webContents.send('session:refresh');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The IPC channel name 'session:refresh' is hardcoded here as a string literal. It should use the SESSION_REFRESH constant defined in src/preload/constants/ipcChannels.ts to maintain consistency and avoid potential typos. You should add it to the duplicated constants list at the top of the file to follow the existing pattern.

Suggested change
mainWindow.webContents.send('session:refresh');
mainWindow.webContents.send(SESSION_REFRESH);

Comment on lines +113 to +122
private pruneCache<V>(cache: Map<string, V>): void {
if (cache.size <= ProjectScanner.MAX_CACHE_ENTRIES) return;
const excess = cache.size - ProjectScanner.MAX_CACHE_ENTRIES;
let removed = 0;
for (const key of cache.keys()) {
if (removed >= excess) break;
cache.delete(key);
removed++;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The pruneCache implementation follows a FIFO (First-In-First-Out) eviction strategy rather than LRU (Least Recently Used), as Map.keys() iterates in insertion order and the order is never updated when an entry is accessed via get(). If LRU behavior is intended, you should move the accessed key to the end of the map on every get operation. Otherwise, the documentation should be updated to reflect FIFO behavior.

: undefined;

const isMarkdownFile = /\.mdx?$/i.test(filePath);
const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The viewMode state is initialized only when the component mounts. If the component is reused for a different linkedTool (e.g., when switching between files in the same session), the viewMode will persist from the previous file instead of resetting to the appropriate default for the new file type. This can lead to a confusing UX where a code file opens in 'preview' mode if the previous file was Markdown.

  const isMarkdownFile = /\.mdx?$/i.test(filePath);
  const [viewMode, setViewMode] = React.useState<'code' | 'preview'>(isMarkdownFile ? 'preview' : 'code');

  // Reset view mode when file path changes
  React.useEffect(() => {
    setViewMode(isMarkdownFile ? 'preview' : 'code');
  }, [filePath, isMarkdownFile]);

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

This PR introduces crash logging and recovery features in the main process, implements cache eviction and session-scoped invalidation in ProjectScanner, and adds a SubagentMessageCache service along with corresponding IPC and HTTP endpoints to lazy-load and serve subagent parsed messages.

Changes

Cohort / File(s) Summary
Crash Diagnostics & Recovery
src/main/index.ts
Added writeCrashLog helper with timestamped diagnostic logging; wired unhandledRejection and uncaughtException handlers to capture errors. Introduced isQuitting flag to skip recovery during shutdown. Replaced renderer crash handling with capped auto-reload policy (max 3 reloads within 60s) for recoverable reasons. Added renderer console error logging, window unresponsiveness detection with force-reload (capped at 3 within 2 minutes), and 5-minute interval memory monitoring (~2GB threshold).
Cache Management
src/main/services/discovery/ProjectScanner.ts, src/main/services/infrastructure/FileWatcher.ts, src/main/services/infrastructure/SubagentMessageCache.ts, src/main/services/infrastructure/index.ts, test/main/services/infrastructure/SubagentMessageCache.test.ts
Added bounded cache eviction (MAX_CACHE_ENTRIES: 500) to ProjectScanner with pruneCache helper. Introduced new invalidateCachesForSession(sessionFilePath) method for targeted cache invalidation. Updated FileWatcher to call session-scoped invalidation. Implemented new SubagentMessageCache LRU service (maxSize: 10, 10-minute TTL) with key-based storage, LRU eviction, session/project invalidation, and lifecycle management. Added comprehensive test suite validating set/get, LRU eviction, TTL expiry, and cache enable/disable.
Service Context Integration
src/main/services/infrastructure/ServiceContext.ts, src/main/standalone.ts
Instantiated SubagentMessageCache (size 10) in ServiceContext constructor and added cleanup in dispose(). Wired subagentMessageCache to HTTP server startup payload in standalone mode.
Subagent Message API Layer
src/preload/constants/ipcChannels.ts, src/preload/index.ts, src/main/ipc/subagents.ts, src/main/http/index.ts, src/main/http/subagents.ts
Added SUBAGENT_GET_MESSAGES IPC channel constant and preload API method getSubagentMessages(projectId, sessionId, subagentId). Added HTTP dependency subagentMessageCache to HttpServices interface. Implemented new GET endpoint /api/projects/:projectId/sessions/:sessionId/subagents/:subagentId/messages with cache-first lookup and fallback file parsing; added matching IPC handler with identical logic.
Type & Path Resolution
src/main/services/analysis/SubagentDetailBuilder.ts, src/renderer/api/httpClient.ts, src/shared/types/api.ts
Updated buildSubagentDetail to use buildSubagentsPath(projectsDir, projectId, sessionId) for session-scoped path resolution. Added getSubagentMessages method to HttpAPIClient and updated ElectronAPI interface with new IPC method returning Promise<ParsedMessage[]>. Added fileSize?: number optional field to IpcFileChangePayload.

Possibly related PRs


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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (6)
src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx (1)

61-100: Consider extracting the markdown mode toggle into a shared component.

This block is now very similar to WriteToolViewer, so a shared MarkdownModeToggle (or hook + tiny component) would reduce duplication and prevent drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx` around
lines 61 - 100, Extract the repeated markdown view toggle UI from ReadToolViewer
into a shared component (e.g., MarkdownModeToggle) and use it in both
ReadToolViewer and WriteToolViewer: create a new component that accepts props
{isMarkdownFile, viewMode, setViewMode} (or a hook that returns [viewMode,
setViewMode]) and moves the two buttons and styling there, then replace the
inline toggle block in ReadToolViewer (the fragment using isMarkdownFile,
viewMode, setViewMode) with <MarkdownModeToggle .../> and do the same in
WriteToolViewer so both reuse the same logic and visuals.
src/main/services/discovery/ProjectScanner.ts (1)

109-122: FIFO eviction, not true LRU.

The pruneCache() method evicts entries based on Map insertion order, which is FIFO (first-in, first-out). True LRU eviction requires moving entries to the end of the map on cache hits (reads), so that frequently-accessed entries remain protected from eviction.

For the use case here (session metadata caching), FIFO is likely acceptable since cache hits rarely repeat immediately—sessions are scanned, then users navigate away. However, if this becomes a concern, consider using a proper LRU cache like lru-cache or re-inserting on hits.

Based on learnings: "Use LRU Cache to avoid re-parsing large JSONL files"—the current implementation is functionally a bounded FIFO cache rather than LRU.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/discovery/ProjectScanner.ts` around lines 109 - 122,
pruneCache currently evicts by Map insertion order (FIFO) which is not true LRU;
update the cache strategy used by ProjectScanner so reads refresh entry recency
or replace the Map with an LRU implementation: either (1) on every read/update
of the cache entry move it to the end of the Map (re-insert the key/value) so
pruneCache continues to work as LRU, or (2) swap the Map for a standard LRU
utility (e.g., the lru-cache package) and enforce MAX_CACHE_ENTRIES there;
modify places that access the cache to use the chosen approach and keep
pruneCache/ProjectScanner.MAX_CACHE_ENTRIES semantics consistent.
src/main/index.ts (1)

715-745: Memory monitor interval is not cleared on window close.

The memoryMonitorInterval is created but never cleared when the window is closed or destroyed. While unref() prevents it from blocking app exit, the interval will continue running and potentially throw errors when accessing mainWindow.webContents after the window is destroyed (though the if (!mainWindow || mainWindow.isDestroyed()) guard catches this).

For cleaner resource management, consider clearing the interval in the mainWindow.on('closed') handler.

🧹 Proposed fix to clear the interval

Add interval tracking and cleanup:

+  // Track interval for cleanup
+  let memoryMonitorInterval: ReturnType<typeof setInterval> | null = null;
+
   // ... existing code ...

-  const memoryMonitorInterval = setInterval(() => {
+  memoryMonitorInterval = setInterval(() => {
     // ... existing code ...
   }, MEMORY_CHECK_INTERVAL_MS);
   memoryMonitorInterval.unref();

   // In the 'closed' handler:
   mainWindow.on('closed', () => {
     mainWindow = null;
+    if (memoryMonitorInterval) {
+      clearInterval(memoryMonitorInterval);
+      memoryMonitorInterval = null;
+    }
     // ... existing cleanup ...
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/index.ts` around lines 715 - 745, The memoryMonitorInterval is never
cleared when the window is closed; add cleanup to stop the interval to avoid
background checks against a destroyed window. In the component that creates
memoryMonitorInterval (the block using MEMORY_CHECK_INTERVAL_MS and calling
mainWindow.webContents.getOSProcessId()), ensure you store the interval id in
memoryMonitorInterval and call clearInterval(memoryMonitorInterval) from the
mainWindow 'closed' (or 'close') event handler (e.g., inside
mainWindow.on('closed', ...) or similar teardown logic) and remove/unregister
any references so the interval is not left running after the window is
destroyed.
src/renderer/components/chat/items/LinkedToolItem.tsx (1)

44-45: Keep imports in guideline order.

Line 44 adds a path-alias type import after relative imports. Group alias imports before relative imports to stay consistent.

As per coding guidelines: organize imports as external packages, then path aliases, then relative imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx` around lines 44 - 45,
Reorder the imports in LinkedToolItem.tsx to follow the project guideline: place
path-alias imports (e.g., "import type { StepVariant } from
'@renderer/constants/stepVariants';" and "import type { LinkedToolItem as
LinkedToolItemType } from '@renderer/types/groups';") before any relative
imports; update the import block so external packages remain first, then the
path-alias imports (StepVariant, LinkedToolItemType), and finally relative
imports to restore the prescribed grouping order.
src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx (1)

30-40: Add explicit ARIA state to the toggle button.

This is already keyboard-friendly; adding aria-expanded and aria-controls would improve screen-reader clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx`
around lines 30 - 40, The toggle button in CollapsibleOutputSection lacks
explicit ARIA state; update the button rendered in the CollapsibleOutputSection
component to include aria-expanded tied to isExpanded and aria-controls pointing
to the id of the collapsible panel (the element rendered when isExpanded is
true). Ensure the panel element has a stable id (generate one using React's
useId or a prop fallback) so the button's aria-controls references that id. Keep
the existing onClick (setIsExpanded) behavior and only add these attributes to
the <button> and the corresponding panel element to improve screen-reader
clarity.
src/renderer/utils/toolRendering/index.ts (1)

7-13: Avoid expanding renderer utility barrel exports.

Adding hasBashContent here increases reliance on a renderer utils barrel; guideline direction is to import directly from specific files in renderer utilities.

As per coding guidelines: src/renderer/**/*.{ts,tsx} renderer utils/hooks/types should NOT use barrel exports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/toolRendering/index.ts` around lines 7 - 13, The barrel
export index.ts is exposing hasBashContent which violates the renderer utils
rule against using barrel exports; remove hasBashContent from the export list in
src/renderer/utils/toolRendering/index.ts and update any callers to import
hasBashContent directly from './toolContentChecks' (leave the other
exports—hasEditContent, hasReadContent, hasSkillInstructions, hasWriteContent—if
they remain allowed); ensure all references that previously imported
hasBashContent from the barrel now import it from the specific module to comply
with the renderer utils guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/components/chat/ChatHistory.tsx`:
- Around line 380-387: The session-refresh-scroll-bottom listener is global and
causes every mounted ChatHistory to scroll; update the handler in the useEffect
so it only acts for the intended tab/pane by checking an identifier on the
incoming event (e.g., event.detail?.chatId or event.detail?.paneId) against this
component's id/active prop (e.g., props.chatId, props.sessionId or isActive
flag) before calling scrollToBottom; if the app dispatching the event doesn't
include an identifier, change the emitter to include one and then compare in the
handler (identify the handler, useEffect, scrollToBottom, and the
'session-refresh-scroll-bottom' event).

In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx`:
- Around line 25-27: The code in BashToolViewer currently unsafely casts
linkedTool.input with "as string"; replace this with a runtime type guard to
validate the shape before using fields. Add a reusable guard function (e.g.,
isBashToolInput) that checks input is a non-null object and that input.command
is a string (and optionally input.description is string), import it into
BashToolViewer, call it to narrow linkedTool.input, and only then read command
and description; update other viewers (EditToolViewer, WriteToolViewer) to use
corresponding guards for consistent validation.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx`:
- Line 175: The LinkedToolItem component is passing an undeclared prop
variant={toolVariant} to BaseItem even though BaseItemProps does not define
variant; remove the prop from the JSX in LinkedToolItem (search for
LinkedToolItem and the BaseItem usage) so BaseItem is only given supported
props, or alternatively add variant to BaseItemProps and implement handling
inside BaseItem (update BaseItemProps and BaseItem component) if variant
behavior is required; prefer removing the prop here to resolve the TypeScript
error.

In `@src/renderer/hooks/useKeyboardShortcuts.ts`:
- Around line 263-268: The Promise.all call that runs
refreshSessionInPlace(selectedProjectId, selectedSessionId) and
fetchSessions(selectedProjectId) lacks error handling and can produce unhandled
promise rejections; wrap the Promise.all(...) chain with a .catch(...) to handle
errors (e.g., log with console.error or the app logger) and, if appropriate,
surface user feedback, while preserving the existing .then(...) that dispatches
the 'session-refresh-scroll-bottom' CustomEvent; reference the Promise.all call
and the functions refreshSessionInPlace, fetchSessions, and the dispatch of
'session-refresh-scroll-bottom' when making the change.

In `@src/renderer/store/index.ts`:
- Around line 278-283: The Promise.all call that invokes
state.refreshSessionInPlace and state.fetchSessions can reject and currently
leaves an unhandled rejection; update the code around the
Promise.all([...]).then(...) to append a .catch handler that explicitly handles
errors from refreshSessionInPlace and fetchSessions (e.g., log the error via
console.error or your app logger and/or surface a user-facing error) and ensure
post-refresh behavior is only triggered on success; reference the
refreshSessionInPlace and fetchSessions calls and the
'session-refresh-scroll-bottom' CustomEvent so you add the .catch after the
.then to avoid silent failures and unhandled rejections.

In `@src/renderer/utils/toolRendering/toolContentChecks.ts`:
- Around line 63-65: hasBashContent currently returns true for any truthy
linkedTool.input?.command (numbers, objects, booleans); change it into a proper
type guard by updating the function signature to a type predicate (e.g.,
hasBashContent(linkedTool: LinkedToolItem): linkedTool is LinkedToolItem & {
input: { command: string } }) and return only when typeof
linkedTool.input?.command === 'string' && linkedTool.input.command.trim().length
> 0 so callers can safely assume command is a non-empty string.

---

Nitpick comments:
In `@src/main/index.ts`:
- Around line 715-745: The memoryMonitorInterval is never cleared when the
window is closed; add cleanup to stop the interval to avoid background checks
against a destroyed window. In the component that creates memoryMonitorInterval
(the block using MEMORY_CHECK_INTERVAL_MS and calling
mainWindow.webContents.getOSProcessId()), ensure you store the interval id in
memoryMonitorInterval and call clearInterval(memoryMonitorInterval) from the
mainWindow 'closed' (or 'close') event handler (e.g., inside
mainWindow.on('closed', ...) or similar teardown logic) and remove/unregister
any references so the interval is not left running after the window is
destroyed.

In `@src/main/services/discovery/ProjectScanner.ts`:
- Around line 109-122: pruneCache currently evicts by Map insertion order (FIFO)
which is not true LRU; update the cache strategy used by ProjectScanner so reads
refresh entry recency or replace the Map with an LRU implementation: either (1)
on every read/update of the cache entry move it to the end of the Map (re-insert
the key/value) so pruneCache continues to work as LRU, or (2) swap the Map for a
standard LRU utility (e.g., the lru-cache package) and enforce MAX_CACHE_ENTRIES
there; modify places that access the cache to use the chosen approach and keep
pruneCache/ProjectScanner.MAX_CACHE_ENTRIES semantics consistent.

In `@src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx`:
- Around line 30-40: The toggle button in CollapsibleOutputSection lacks
explicit ARIA state; update the button rendered in the CollapsibleOutputSection
component to include aria-expanded tied to isExpanded and aria-controls pointing
to the id of the collapsible panel (the element rendered when isExpanded is
true). Ensure the panel element has a stable id (generate one using React's
useId or a prop fallback) so the button's aria-controls references that id. Keep
the existing onClick (setIsExpanded) behavior and only add these attributes to
the <button> and the corresponding panel element to improve screen-reader
clarity.

In `@src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx`:
- Around line 61-100: Extract the repeated markdown view toggle UI from
ReadToolViewer into a shared component (e.g., MarkdownModeToggle) and use it in
both ReadToolViewer and WriteToolViewer: create a new component that accepts
props {isMarkdownFile, viewMode, setViewMode} (or a hook that returns [viewMode,
setViewMode]) and moves the two buttons and styling there, then replace the
inline toggle block in ReadToolViewer (the fragment using isMarkdownFile,
viewMode, setViewMode) with <MarkdownModeToggle .../> and do the same in
WriteToolViewer so both reuse the same logic and visuals.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx`:
- Around line 44-45: Reorder the imports in LinkedToolItem.tsx to follow the
project guideline: place path-alias imports (e.g., "import type { StepVariant }
from '@renderer/constants/stepVariants';" and "import type { LinkedToolItem as
LinkedToolItemType } from '@renderer/types/groups';") before any relative
imports; update the import block so external packages remain first, then the
path-alias imports (StepVariant, LinkedToolItemType), and finally relative
imports to restore the prescribed grouping order.

In `@src/renderer/utils/toolRendering/index.ts`:
- Around line 7-13: The barrel export index.ts is exposing hasBashContent which
violates the renderer utils rule against using barrel exports; remove
hasBashContent from the export list in src/renderer/utils/toolRendering/index.ts
and update any callers to import hasBashContent directly from
'./toolContentChecks' (leave the other exports—hasEditContent, hasReadContent,
hasSkillInstructions, hasWriteContent—if they remain allowed); ensure all
references that previously imported hasBashContent from the barrel now import it
from the specific module to comply with the renderer utils guideline.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0be83daf-cb95-431a-858e-1a8b9929ab66

📥 Commits

Reviewing files that changed from the base of the PR and between c0708bb and 2f04d29.

📒 Files selected for processing (22)
  • src/main/index.ts
  • src/main/services/discovery/ProjectScanner.ts
  • src/main/services/infrastructure/FileWatcher.ts
  • src/main/services/infrastructure/ServiceContext.ts
  • src/preload/constants/ipcChannels.ts
  • src/preload/index.ts
  • src/renderer/api/httpClient.ts
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/items/LinkedToolItem.tsx
  • src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx
  • src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/index.ts
  • src/renderer/components/chat/viewers/syntaxHighlighter.ts
  • src/renderer/components/layout/TabBar.tsx
  • src/renderer/hooks/useKeyboardShortcuts.ts
  • src/renderer/store/index.ts
  • src/renderer/utils/toolRendering/index.ts
  • src/renderer/utils/toolRendering/toolContentChecks.ts
  • src/shared/types/api.ts

Comment on lines +380 to +387
// Listen for session-refresh-scroll-bottom events (from Ctrl+R / refresh button)
useEffect(() => {
const handler = (): void => {
scrollToBottom('smooth');
};
window.addEventListener('session-refresh-scroll-bottom', handler);
return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
}, [scrollToBottom]);
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

Scope refresh-scroll handling to the intended tab/pane.

This handler runs for every mounted ChatHistory, so one refresh event can scroll unrelated views to bottom and disrupt reading position.

💡 Proposed fix
   useEffect(() => {
     const handler = (): void => {
-      scrollToBottom('smooth');
+      if (!isThisTabActive) return;
+      scrollToBottom('smooth');
     };
     window.addEventListener('session-refresh-scroll-bottom', handler);
     return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
-  }, [scrollToBottom]);
+  }, [isThisTabActive, scrollToBottom]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/ChatHistory.tsx` around lines 380 - 387, The
session-refresh-scroll-bottom listener is global and causes every mounted
ChatHistory to scroll; update the handler in the useEffect so it only acts for
the intended tab/pane by checking an identifier on the incoming event (e.g.,
event.detail?.chatId or event.detail?.paneId) against this component's id/active
prop (e.g., props.chatId, props.sessionId or isActive flag) before calling
scrollToBottom; if the app dispatching the event doesn't include an identifier,
change the emitter to include one and then compare in the handler (identify the
handler, useEffect, scrollToBottom, and the 'session-refresh-scroll-bottom'
event).

Comment on lines +25 to +27
const command = linkedTool.input.command as string;
const description = linkedTool.input.description as string | undefined;

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

🧩 Analysis chain

🏁 Script executed:

cat -n src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx | head -35

Repository: matt1398/claude-devtools

Length of output: 1329


🏁 Script executed:

# Search for type guard utilities in the codebase
fd -t f "isXxx" --iglob "*.ts" --iglob "*.tsx" src/

Repository: matt1398/claude-devtools

Length of output: 270


🏁 Script executed:

# Look for existing type guard patterns in renderer utils
rg "^export const is[A-Z]" src/renderer/ -A 2 | head -30

Repository: matt1398/claude-devtools

Length of output: 276


🏁 Script executed:

# Check if there are validation utilities for linked tools
rg "linkedTool" src/renderer/ -t ts -t tsx -l | head -10

Repository: matt1398/claude-devtools

Length of output: 95


🏁 Script executed:

# Find LinkedToolItem type definition
rg "type LinkedToolItem" src/renderer/ -A 10

Repository: matt1398/claude-devtools

Length of output: 50


🏁 Script executed:

# Search for type guards with isXxx pattern
rg "export const is[A-Z]" src/ -t ts | head -20

Repository: matt1398/claude-devtools

Length of output: 160


🏁 Script executed:

# Check for tool input validation patterns
rg "linkedTool\.input" src/renderer/ -B 2 -A 2 | head -40

Repository: matt1398/claude-devtools

Length of output: 3361


🏁 Script executed:

# Check the types/groups file for LinkedToolItem
cat -n src/renderer/types/groups.ts | head -100

Repository: matt1398/claude-devtools

Length of output: 3494


🏁 Script executed:

# Search for LinkedToolItem interface
rg "interface LinkedToolItem" src/renderer/types/

Repository: matt1398/claude-devtools

Length of output: 130


🏁 Script executed:

# Get the full types/groups.ts file
wc -l src/renderer/types/groups.ts

Repository: matt1398/claude-devtools

Length of output: 100


🏁 Script executed:

# Search for LinkedToolItem definition in all types files
rg "LinkedToolItem" src/renderer/types/ -A 15 | head -60

Repository: matt1398/claude-devtools

Length of output: 3120


🏁 Script executed:

# Check other tool viewers for validation patterns
cat -n src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx | head -40

Repository: matt1398/claude-devtools

Length of output: 1909


🏁 Script executed:

# Check EditToolViewer
cat -n src/renderer/components/chat/items/linkedTool/EditToolViewer.tsx | head -40

Repository: matt1398/claude-devtools

Length of output: 1533


🏁 Script executed:

# Search for validation utilities or type guards related to tools
rg "function is" src/renderer/utils/ -A 3

Repository: matt1398/claude-devtools

Length of output: 1736


Create type guards for tool input validation instead of unsafe casts.

Lines 25-26 use unsafe as string casts on untyped input fields. Per coding guidelines, runtime validation should use isXxx type guards. This pattern appears across multiple tool viewers (BashToolViewer, EditToolViewer, WriteToolViewer).

Create reusable type guards like isBashToolInput(input): input is { command: string; description?: string } and use them consistently:

Suggested approach
-  const command = linkedTool.input.command as string;
-  const description = linkedTool.input.description as string | undefined;
+  if (!isBashToolInput(linkedTool.input)) {
+    return null;
+  }
+  const { command, description } = linkedTool.input;
+  if (command.trim().length === 0) return null;

Define in a shared validation module (e.g., utils/toolRendering/toolInputGuards.ts):

export function isBashToolInput(input: unknown): input is { command: string; description?: string } {
  return typeof input === 'object' && input !== null && 
         typeof (input as Record<string, unknown>).command === 'string';
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx` around
lines 25 - 27, The code in BashToolViewer currently unsafely casts
linkedTool.input with "as string"; replace this with a runtime type guard to
validate the shape before using fields. Add a reusable guard function (e.g.,
isBashToolInput) that checks input is a non-null object and that input.command
is a string (and optionally input.description is string), import it into
BashToolViewer, call it to narrow linkedTool.input, and only then read command
and description; update other viewers (EditToolViewer, WriteToolViewer) to use
corresponding guards for consistent validation.

highlightClasses={highlightClasses}
highlightStyle={highlightStyle}
notificationDotColor={notificationDotColor}
variant={toolVariant}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether BaseItemProps declares `variant` and where BaseItem is called with it.
rg -nP --type=tsx -C3 'interface\s+BaseItemProps|variant\??\s*:' src/renderer/components/chat/items/BaseItem.tsx
rg -nP --type=tsx -C2 '<BaseItem[^>]*\bvariant=' src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Search for BaseItemProps interface and variant usage without type restrictions
rg -nA5 'interface\s+BaseItemProps' src/renderer/components/chat/items/BaseItem.tsx

Repository: matt1398/claude-devtools

Length of output: 295


🏁 Script executed:

#!/bin/bash
# Read the LinkedToolItem.tsx file to see line 175 and surrounding context
wc -l src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 125


🏁 Script executed:

#!/bin/bash
# List files in the items directory to confirm they exist
ls -la src/renderer/components/chat/items/

Repository: matt1398/claude-devtools

Length of output: 913


🏁 Script executed:

#!/bin/bash
# Read the full BaseItemProps interface definition
sed -n '15,40p' src/renderer/components/chat/items/BaseItem.tsx

Repository: matt1398/claude-devtools

Length of output: 1012


🏁 Script executed:

#!/bin/bash
# Read line 175 and surrounding context from LinkedToolItem.tsx
sed -n '170,180p' src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 459


🏁 Script executed:

#!/bin/bash
# Continue reading BaseItemProps to see if variant is declared later
sed -n '40,60p' src/renderer/components/chat/items/BaseItem.tsx

Repository: matt1398/claude-devtools

Length of output: 691


🏁 Script executed:

#!/bin/bash
# Read more context around line 175 to see which component the variant prop is on
sed -n '160,180p' src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 808


🏁 Script executed:

#!/bin/bash
# Check BaseItem component render/export signature
sed -n '1,100p' src/renderer/components/chat/items/BaseItem.tsx | grep -A 30 'export\|function\|const.*BaseItem'

Repository: matt1398/claude-devtools

Length of output: 2455


🏁 Script executed:

#!/bin/bash
# Read exact lines around 175 to identify the opening tag
sed -n '165,178p' src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 595


🏁 Script executed:

#!/bin/bash
# Read earlier lines to see the opening tag of the component
sed -n '155,175p' src/renderer/components/chat/items/LinkedToolItem.tsx

Repository: matt1398/claude-devtools

Length of output: 709


Remove undeclared variant prop from BaseItem component.

Line 175 passes variant={toolVariant} to <BaseItem>, but BaseItemProps does not include this prop. The BaseItem component will not accept or use this prop, causing a TypeScript error.

Fix
-        variant={toolVariant}
📝 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
variant={toolVariant}
{/* variant prop removed - not supported by BaseItemProps */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx` at line 175, The
LinkedToolItem component is passing an undeclared prop variant={toolVariant} to
BaseItem even though BaseItemProps does not define variant; remove the prop from
the JSX in LinkedToolItem (search for LinkedToolItem and the BaseItem usage) so
BaseItem is only given supported props, or alternatively add variant to
BaseItemProps and implement handling inside BaseItem (update BaseItemProps and
BaseItem component) if variant behavior is required; prefer removing the prop
here to resolve the TypeScript error.

Comment on lines 263 to +268
void Promise.all([
fetchSessionDetail(selectedProjectId, selectedSessionId),
refreshSessionInPlace(selectedProjectId, selectedSessionId),
fetchSessions(selectedProjectId),
]);
]).then(() => {
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
});
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

Add error handling for the refresh shortcut async flow.

This Promise chain can reject without a handler, causing unhandled rejection noise and harder failure diagnosis.

💡 Proposed fix
           void Promise.all([
             refreshSessionInPlace(selectedProjectId, selectedSessionId),
             fetchSessions(selectedProjectId),
-          ]).then(() => {
-            window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
-          });
+          ])
+            .then(() => {
+              window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
+            })
+            .catch((error: unknown) => {
+              console.error('[KeyboardShortcuts] Session refresh failed:', error);
+            });
📝 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
void Promise.all([
fetchSessionDetail(selectedProjectId, selectedSessionId),
refreshSessionInPlace(selectedProjectId, selectedSessionId),
fetchSessions(selectedProjectId),
]);
]).then(() => {
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
});
void Promise.all([
refreshSessionInPlace(selectedProjectId, selectedSessionId),
fetchSessions(selectedProjectId),
])
.then(() => {
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
})
.catch((error: unknown) => {
console.error('[KeyboardShortcuts] Session refresh failed:', error);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useKeyboardShortcuts.ts` around lines 263 - 268, The
Promise.all call that runs refreshSessionInPlace(selectedProjectId,
selectedSessionId) and fetchSessions(selectedProjectId) lacks error handling and
can produce unhandled promise rejections; wrap the Promise.all(...) chain with a
.catch(...) to handle errors (e.g., log with console.error or the app logger)
and, if appropriate, surface user feedback, while preserving the existing
.then(...) that dispatches the 'session-refresh-scroll-bottom' CustomEvent;
reference the Promise.all call and the functions refreshSessionInPlace,
fetchSessions, and the dispatch of 'session-refresh-scroll-bottom' when making
the change.

Comment on lines +278 to +283
void Promise.all([
state.refreshSessionInPlace(activeTab.projectId, activeTab.sessionId),
state.fetchSessions(activeTab.projectId),
]).then(() => {
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
});
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

Handle refresh Promise rejections explicitly.

If either async call fails, this currently produces an unhandled rejection and silently skips post-refresh behavior.

💡 Proposed fix
-        void Promise.all([
-          state.refreshSessionInPlace(activeTab.projectId, activeTab.sessionId),
-          state.fetchSessions(activeTab.projectId),
-        ]).then(() => {
-          window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
-        });
+        void Promise.all([
+          state.refreshSessionInPlace(activeTab.projectId, activeTab.sessionId),
+          state.fetchSessions(activeTab.projectId),
+        ])
+          .then(() => {
+            window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
+          })
+          .catch((error: unknown) => {
+            console.error('[Store] Session refresh failed:', error);
+          });
📝 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
void Promise.all([
state.refreshSessionInPlace(activeTab.projectId, activeTab.sessionId),
state.fetchSessions(activeTab.projectId),
]).then(() => {
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
});
void Promise.all([
state.refreshSessionInPlace(activeTab.projectId, activeTab.sessionId),
state.fetchSessions(activeTab.projectId),
])
.then(() => {
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
})
.catch((error: unknown) => {
console.error('[Store] Session refresh failed:', error);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/store/index.ts` around lines 278 - 283, The Promise.all call
that invokes state.refreshSessionInPlace and state.fetchSessions can reject and
currently leaves an unhandled rejection; update the code around the
Promise.all([...]).then(...) to append a .catch handler that explicitly handles
errors from refreshSessionInPlace and fetchSessions (e.g., log the error via
console.error or your app logger and/or surface a user-facing error) and ensure
post-refresh behavior is only triggered on success; reference the
refreshSessionInPlace and fetchSessions calls and the
'session-refresh-scroll-bottom' CustomEvent so you add the .catch after the
.then to avoid silent failures and unhandled rejections.

Comment on lines +63 to +65
export function hasBashContent(linkedTool: LinkedToolItem): boolean {
return !!linkedTool.input?.command;
}
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

🧩 Analysis chain

🏁 Script executed:

cat src/renderer/utils/toolRendering/toolContentChecks.ts

Repository: matt1398/claude-devtools

Length of output: 2075


🏁 Script executed:

cd src/renderer && find . -name 'groups.ts' -o -name 'groups.tsx' | head -5

Repository: matt1398/claude-devtools

Length of output: 85


🏁 Script executed:

cat src/renderer/types/groups.ts | head -100

Repository: matt1398/claude-devtools

Length of output: 2794


🏁 Script executed:

rg "export (interface|type) LinkedToolItem" src/renderer/types/groups.ts -A 20

Repository: matt1398/claude-devtools

Length of output: 726


🏁 Script executed:

rg "!!.*\?" src/renderer/utils/toolRendering/toolContentChecks.ts

Repository: matt1398/claude-devtools

Length of output: 105


🏁 Script executed:

rg "!!.*\?" src/renderer/utils/ -t ts -t tsx --max-count 10

Repository: matt1398/claude-devtools

Length of output: 95


🏁 Script executed:

rg "!!.*\?" src/renderer/utils/ -t ts --max-count 10

Repository: matt1398/claude-devtools

Length of output: 159


Use a proper type guard for bash command validation.

The current implementation accepts any truthy command value (objects, numbers, booleans), which could incorrectly route to the Bash viewer and cause unsafe type casts downstream. Implement a type guard with proper string validation.

Suggested fix
+function isBashInput(input: Record<string, unknown>): input is { command: string } {
+  return typeof input.command === 'string' && input.command.trim().length > 0;
+}
+
 export function hasBashContent(linkedTool: LinkedToolItem): boolean {
-  return !!linkedTool.input?.command;
+  return isBashInput(linkedTool.input);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/toolRendering/toolContentChecks.ts` around lines 63 - 65,
hasBashContent currently returns true for any truthy linkedTool.input?.command
(numbers, objects, booleans); change it into a proper type guard by updating the
function signature to a type predicate (e.g., hasBashContent(linkedTool:
LinkedToolItem): linkedTool is LinkedToolItem & { input: { command: string } })
and return only when typeof linkedTool.input?.command === 'string' &&
linkedTool.input.command.trim().length > 0 so callers can safely assume command
is a non-empty string.

…tion

- Add LRU eviction (500 entries) for ProjectScanner caches to prevent
  unbounded growth over long sessions
- Add invalidateCachesForSession() for single-file cache invalidation
  instead of purging entire project on every file-change event
- Add renderer memory monitoring (5-min interval, 2GB warning threshold)
  to detect leaks before they crash the renderer
- Add crash recovery with retry caps, unresponsive watchdog, and crash
  logging to ~/.claude/claude-devtools-crash.log
- Add SubagentMessageCache (LRU, 10 entries, 10min TTL) for lazy-loaded
  subagent message bodies, distinct from the main DataCache
- Add IPC + HTTP handlers for on-demand subagent message loading so the
  renderer can fetch bodies only when a subagent is expanded
- Fix SubagentDetailBuilder path construction to use buildSubagentsPath()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@psypeal psypeal force-pushed the fix/crash-prevention-bounded-caches branch from 2f04d29 to 75e23ec Compare April 16, 2026 09:29
@psypeal psypeal changed the title fix: crash prevention with bounded caches and session-scoped invalidation fix: crash prevention with bounded caches and lazy subagent message loading Apr 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/main/services/discovery/ProjectScanner.ts (1)

63-64: Move cache size to shared cache constants.

MAX_CACHE_ENTRIES is hardcoded in ProjectScanner. Please centralize this in src/shared/constants/cache.ts so cache sizing stays consistent and tunable across services.

Based on learnings: Applies to src/shared/constants/cache.ts : Use cache.ts in constants for cache configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/discovery/ProjectScanner.ts` around lines 63 - 64, Replace
the hardcoded MAX_CACHE_ENTRIES in ProjectScanner with a shared constant: import
the cache size constant exported from src/shared/constants/cache.ts and use it
in place of ProjectScanner.MAX_CACHE_ENTRIES; remove the private static readonly
MAX_CACHE_ENTRIES declaration from the ProjectScanner class and update any
references (e.g., cache construction or LRU init inside ProjectScanner) to
reference the shared symbol (e.g., DEFAULT_CACHE_MAX_ENTRIES or
CACHE_MAX_ENTRIES) exported by cache.ts so cache sizing is centralized and
configurable across services.
src/main/http/index.ts (1)

31-31: Use barrel export for SubagentMessageCache type import.

Please import this type from ../services (and export it there if needed) instead of importing from ../services/infrastructure/... directly.

As per coding guidelines: src/main/**/*.{ts,tsx}: Services and domain subdirectories in src/main/services/ must export via barrel exports (index.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/http/index.ts` at line 31, The import of the SubagentMessageCache
type in index.ts should use the services barrel export: update the import to
pull SubagentMessageCache from '../services' (i.e., import type {
SubagentMessageCache } from '../services') and, if the type is not already
exported, add an export of SubagentMessageCache in the services barrel
(src/main/services/index.ts) re-exporting it from
'./infrastructure/SubagentMessageCache' so callers can rely on the top-level
services barrel.
src/main/services/infrastructure/ServiceContext.ts (1)

115-115: Extract subagent cache size to shared cache constants.

10 is hardcoded here. Please define and consume a named constant from src/shared/constants/cache.ts to keep cache knobs centralized.

Based on learnings: Applies to src/shared/constants/cache.ts : Use cache.ts in constants for cache configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/ServiceContext.ts` at line 115, Replace the
hardcoded 10 used when constructing SubagentMessageCache in ServiceContext with
a named constant: add a new exported constant (e.g. SUBAGENT_MESSAGE_CACHE_SIZE)
to your shared cache constants module and then change the constructor call in
ServiceContext (the line creating new SubagentMessageCache(...)) to use that
constant instead of 10; ensure the constant is exported from the constants file
and imported into ServiceContext so cache knobs remain centralized.
src/main/index.ts (1)

720-756: Consider clearing the memory monitor interval on window close.

The memoryMonitorInterval is created inside createWindow but not cleared when the window is closed or destroyed. While unref() prevents it from blocking app exit, the interval continues running and may log errors when mainWindow is null after the window closes but before the app quits (e.g., on macOS where closing the window doesn't quit the app).

💡 Suggested fix: Clear interval in 'closed' handler
+  let memoryMonitorInterval: ReturnType<typeof setInterval> | null = null;
+
   // Periodic memory monitoring via app.getAppMetrics().
   // Logs all-process memory every 5 minutes so we have data leading up to crashes.
   // Warns when the renderer exceeds 2 GB.
   const MEMORY_CHECK_INTERVAL_MS = 5 * 60_000;
   const RENDERER_MEMORY_WARNING_KB = 2048 * 1024; // 2 GB in KB
-  const memoryMonitorInterval = setInterval(() => {
+  memoryMonitorInterval = setInterval(() => {
     if (!mainWindow || mainWindow.isDestroyed()) return;
     // ... rest of implementation
   }, MEMORY_CHECK_INTERVAL_MS);
   memoryMonitorInterval.unref(); // Don't prevent app exit

Then in the mainWindow.on('closed') handler around line 568:

   mainWindow.on('closed', () => {
     mainWindow = null;
+    if (memoryMonitorInterval) {
+      clearInterval(memoryMonitorInterval);
+      memoryMonitorInterval = null;
+    }
     // Clear main window references
     if (notificationManager) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/index.ts` around lines 720 - 756, The memory monitor interval
created as memoryMonitorInterval inside createWindow is never cleared when the
window closes, so it keeps running and may log errors after mainWindow becomes
null; update the mainWindow.on('closed') / 'closed' handler to call
clearInterval(memoryMonitorInterval) (and optionally set memoryMonitorInterval
to undefined) so the interval stops when the window is closed, ensuring the
periodic check and writeCrashLog calls cease once the window is destroyed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/services/analysis/SubagentDetailBuilder.ts`:
- Line 46: The JSDoc for SubagentDetailBuilder still references an unused
`_sessionId` but the code now accepts and uses `sessionId`; update the docblock
for the constructor (or the function that declares `sessionId`) to describe the
`sessionId: string` parameter correctly, remove the outdated `_sessionId` note,
and ensure the param tag matches the exact symbol name `sessionId` used in the
method signature so documentation and code are in sync.

In `@src/main/services/infrastructure/SubagentMessageCache.ts`:
- Around line 29-33: The constructor of SubagentMessageCache must guard against
invalid inputs: clamp maxSize to at least 1 and clamp ttlMinutes to a sensible
positive minimum (e.g. 1 minute) before assigning this.maxSize and this.ttlMs so
set() behavior is predictable; update the constructor (constructor(...),
this.maxSize, this.ttlMs, enabled) to validate/clamp inputs (or throw on
invalid) and convert ttlMinutes to milliseconds after validation so downstream
methods like set() rely on safe values.

In `@test/main/services/infrastructure/SubagentMessageCache.test.ts`:
- Around line 75-83: The TTL test for SubagentMessageCache uses
vi.useFakeTimers() but restores with vi.useRealTimers() unconditionally, which
can leak fake timers if an assertion throws; update the 'expires entries past
their TTL' test to ensure timers are always reset by wrapping the test body in a
try/finally (call vi.useRealTimers() in finally) or move timer setup/teardown
into a shared beforeEach/afterEach, referencing the
vi.useFakeTimers()/vi.useRealTimers() calls and the test that constructs new
SubagentMessageCache(3, 1) so cleanup always runs even on failures.

---

Nitpick comments:
In `@src/main/http/index.ts`:
- Line 31: The import of the SubagentMessageCache type in index.ts should use
the services barrel export: update the import to pull SubagentMessageCache from
'../services' (i.e., import type { SubagentMessageCache } from '../services')
and, if the type is not already exported, add an export of SubagentMessageCache
in the services barrel (src/main/services/index.ts) re-exporting it from
'./infrastructure/SubagentMessageCache' so callers can rely on the top-level
services barrel.

In `@src/main/index.ts`:
- Around line 720-756: The memory monitor interval created as
memoryMonitorInterval inside createWindow is never cleared when the window
closes, so it keeps running and may log errors after mainWindow becomes null;
update the mainWindow.on('closed') / 'closed' handler to call
clearInterval(memoryMonitorInterval) (and optionally set memoryMonitorInterval
to undefined) so the interval stops when the window is closed, ensuring the
periodic check and writeCrashLog calls cease once the window is destroyed.

In `@src/main/services/discovery/ProjectScanner.ts`:
- Around line 63-64: Replace the hardcoded MAX_CACHE_ENTRIES in ProjectScanner
with a shared constant: import the cache size constant exported from
src/shared/constants/cache.ts and use it in place of
ProjectScanner.MAX_CACHE_ENTRIES; remove the private static readonly
MAX_CACHE_ENTRIES declaration from the ProjectScanner class and update any
references (e.g., cache construction or LRU init inside ProjectScanner) to
reference the shared symbol (e.g., DEFAULT_CACHE_MAX_ENTRIES or
CACHE_MAX_ENTRIES) exported by cache.ts so cache sizing is centralized and
configurable across services.

In `@src/main/services/infrastructure/ServiceContext.ts`:
- Line 115: Replace the hardcoded 10 used when constructing SubagentMessageCache
in ServiceContext with a named constant: add a new exported constant (e.g.
SUBAGENT_MESSAGE_CACHE_SIZE) to your shared cache constants module and then
change the constructor call in ServiceContext (the line creating new
SubagentMessageCache(...)) to use that constant instead of 10; ensure the
constant is exported from the constants file and imported into ServiceContext so
cache knobs remain centralized.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c30f1feb-36c6-4e29-bf6f-d3dc50973176

📥 Commits

Reviewing files that changed from the base of the PR and between 2f04d29 and 75e23ec.

📒 Files selected for processing (16)
  • src/main/http/index.ts
  • src/main/http/subagents.ts
  • src/main/index.ts
  • src/main/ipc/subagents.ts
  • src/main/services/analysis/SubagentDetailBuilder.ts
  • src/main/services/discovery/ProjectScanner.ts
  • src/main/services/infrastructure/FileWatcher.ts
  • src/main/services/infrastructure/ServiceContext.ts
  • src/main/services/infrastructure/SubagentMessageCache.ts
  • src/main/services/infrastructure/index.ts
  • src/main/standalone.ts
  • src/preload/constants/ipcChannels.ts
  • src/preload/index.ts
  • src/renderer/api/httpClient.ts
  • src/shared/types/api.ts
  • test/main/services/infrastructure/SubagentMessageCache.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/services/infrastructure/index.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/preload/constants/ipcChannels.ts
  • src/preload/index.ts
  • src/shared/types/api.ts
  • src/main/services/infrastructure/FileWatcher.ts

export async function buildSubagentDetail(
projectId: string,
_sessionId: string, // Unused but kept for API consistency
sessionId: string,
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 | 🟡 Minor

Update JSDoc to match the new sessionId usage.

The signature now uses sessionId, but the doc block still describes _sessionId as unused (see Line 35). Please sync the docs with actual behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/analysis/SubagentDetailBuilder.ts` at line 46, The JSDoc
for SubagentDetailBuilder still references an unused `_sessionId` but the code
now accepts and uses `sessionId`; update the docblock for the constructor (or
the function that declares `sessionId`) to describe the `sessionId: string`
parameter correctly, remove the outdated `_sessionId` note, and ensure the param
tag matches the exact symbol name `sessionId` used in the method signature so
documentation and code are in sync.

Comment on lines +29 to +33
constructor(maxSize: number = 10, ttlMinutes: number = 10, enabled: boolean = true) {
this.maxSize = maxSize;
this.ttlMs = ttlMinutes * 60 * 1000;
this.enabled = enabled;
}
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 | 🟡 Minor

Guard invalid cache config values in constructor.

If maxSize <= 0, set() can still retain one entry. Clamp or reject invalid maxSize/ttlMinutes to keep behavior predictable.

Suggested patch
  constructor(maxSize: number = 10, ttlMinutes: number = 10, enabled: boolean = true) {
-    this.maxSize = maxSize;
-    this.ttlMs = ttlMinutes * 60 * 1000;
+    this.maxSize = Math.max(1, maxSize);
+    this.ttlMs = Math.max(1, ttlMinutes) * 60 * 1000;
     this.enabled = enabled;
  }
📝 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
constructor(maxSize: number = 10, ttlMinutes: number = 10, enabled: boolean = true) {
this.maxSize = maxSize;
this.ttlMs = ttlMinutes * 60 * 1000;
this.enabled = enabled;
}
constructor(maxSize: number = 10, ttlMinutes: number = 10, enabled: boolean = true) {
this.maxSize = Math.max(1, maxSize);
this.ttlMs = Math.max(1, ttlMinutes) * 60 * 1000;
this.enabled = enabled;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/infrastructure/SubagentMessageCache.ts` around lines 29 -
33, The constructor of SubagentMessageCache must guard against invalid inputs:
clamp maxSize to at least 1 and clamp ttlMinutes to a sensible positive minimum
(e.g. 1 minute) before assigning this.maxSize and this.ttlMs so set() behavior
is predictable; update the constructor (constructor(...), this.maxSize,
this.ttlMs, enabled) to validate/clamp inputs (or throw on invalid) and convert
ttlMinutes to milliseconds after validation so downstream methods like set()
rely on safe values.

Comment on lines +75 to +83
it('expires entries past their TTL', () => {
vi.useFakeTimers();
const shortLived = new SubagentMessageCache(3, 1); // 1 minute TTL
shortLived.set('k', fakeMessages(1));
expect(shortLived.get('k')).toBeDefined();
vi.advanceTimersByTime(2 * 60 * 1000);
expect(shortLived.get('k')).toBeUndefined();
vi.useRealTimers();
});
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 | 🟡 Minor

Harden fake-timer cleanup in TTL test.

Wrap fake-timer usage in try/finally (or restore in afterEach) so timers are always reset even if an assertion throws.

Suggested patch
  it('expires entries past their TTL', () => {
-    vi.useFakeTimers();
-    const shortLived = new SubagentMessageCache(3, 1); // 1 minute TTL
-    shortLived.set('k', fakeMessages(1));
-    expect(shortLived.get('k')).toBeDefined();
-    vi.advanceTimersByTime(2 * 60 * 1000);
-    expect(shortLived.get('k')).toBeUndefined();
-    vi.useRealTimers();
+    vi.useFakeTimers();
+    try {
+      const shortLived = new SubagentMessageCache(3, 1); // 1 minute TTL
+      shortLived.set('k', fakeMessages(1));
+      expect(shortLived.get('k')).toBeDefined();
+      vi.advanceTimersByTime(2 * 60 * 1000);
+      expect(shortLived.get('k')).toBeUndefined();
+    } finally {
+      vi.useRealTimers();
+    }
  });
📝 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
it('expires entries past their TTL', () => {
vi.useFakeTimers();
const shortLived = new SubagentMessageCache(3, 1); // 1 minute TTL
shortLived.set('k', fakeMessages(1));
expect(shortLived.get('k')).toBeDefined();
vi.advanceTimersByTime(2 * 60 * 1000);
expect(shortLived.get('k')).toBeUndefined();
vi.useRealTimers();
});
it('expires entries past their TTL', () => {
vi.useFakeTimers();
try {
const shortLived = new SubagentMessageCache(3, 1); // 1 minute TTL
shortLived.set('k', fakeMessages(1));
expect(shortLived.get('k')).toBeDefined();
vi.advanceTimersByTime(2 * 60 * 1000);
expect(shortLived.get('k')).toBeUndefined();
} finally {
vi.useRealTimers();
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/services/infrastructure/SubagentMessageCache.test.ts` around lines
75 - 83, The TTL test for SubagentMessageCache uses vi.useFakeTimers() but
restores with vi.useRealTimers() unconditionally, which can leak fake timers if
an assertion throws; update the 'expires entries past their TTL' test to ensure
timers are always reset by wrapping the test body in a try/finally (call
vi.useRealTimers() in finally) or move timer setup/teardown into a shared
beforeEach/afterEach, referencing the vi.useFakeTimers()/vi.useRealTimers()
calls and the test that constructs new SubagentMessageCache(3, 1) so cleanup
always runs even on failures.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant