Skip to content

Adopt async file IO for history and stats#385

Open
openasocket wants to merge 22 commits intoRunMaestro:mainfrom
openasocket:symphony/issue-371-mlncz711
Open

Adopt async file IO for history and stats#385
openasocket wants to merge 22 commits intoRunMaestro:mainfrom
openasocket:symphony/issue-371-mlncz711

Conversation

@openasocket
Copy link
Contributor

@openasocket openasocket commented Feb 15, 2026

Closes #371

Summary

Systematic UX performance sweep targeting React rendering efficiency, main-process responsiveness, and database query performance. All changes are low-risk, behavior-preserving optimizations.

Changes

React Memoization (PERF-01 / PERF-02 / PERF-03)

  • React.memo wraps for high-churn components: SessionListItem, ExecutionQueueIndicator, Toast, ConfirmModal, RenameTabModal, QRCode, QuickActionsModal, HistoryHelpModal, ContextWarningSash, and all 14 UsageDashboard chart components
  • useMemo for expensive computations: session list sorts, AutoRun regex compilation, FilePreview regex/formatting, CsvTableRenderer regex, date tooltip formatting
  • Inline style extraction: memoized style constants in SessionListItem, SessionList, AICommandsPanel to prevent re-render allocation

Main Process Async I/O (PERF-04 / PERF-05)

  • history-manager.ts: converted all sync fs.* calls to fs.promises.*, added async helper utilities, updated all IPC/director-notes/web-server callers to await the async flows
  • stats-db.ts / singleton.ts: async initialization, backup, vacuum, and directory creation; full test suite updated
  • Debug collectors (group-chats.ts, storage.ts): async file reads
  • codex-output-parser.ts: async config caching
  • imageUtils.ts: async temp file writes
  • wslDetector.ts: async detection
  • useStats hook: reuse shared debounced callback to reduce redundant fetches

Database & Query Performance (PERF-06)

  • Compound query indexes on stats-db schema for common access patterns
  • Agent session discovery speed-up via optimized scan logic
  • Git handler parallelization: concurrent Promise.all for independent git queries in IPC handlers

VIBES Metadata

  • .ai-audit/ directory with annotations, config, and manifest for AI code provenance tracking

Test Plan

  • npx vitest run src/__tests__/main/history-manager.test.ts src/__tests__/main/ipc/handlers/history.test.ts src/__tests__/main/ipc/handlers/director-notes.test.ts src/__tests__/main/stats/*.test.ts
  • npx vitest run — full suite regression check
  • npm run build succeeds
  • Manual snappiness check: session list scrolling, tab switching, history panel, usage dashboard

Risk Assessment

All changes are behavior-preserving:

  • React.memo wraps cannot break behavior (only skip unnecessary re-renders)
  • useMemo caches pure computations
  • Style extraction is cosmetic refactoring
  • Async fs conversions follow established codebase patterns
  • DB indexes only accelerate reads
  • Git handler parallelization maintains same result semantics

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Performance Improvements

    • Enhanced rendering performance through component memoization and optimized re-render behavior across UI components.
    • Improved search efficiency with memoized regex patterns and optimized filtering.
    • Parallelized file operations and database queries for faster execution.
  • Bug Fixes & Refactoring

    • Converted file operations to async-based handling for better system stability.
    • Added database indexes to optimize query performance.
    • Improved asynchronous error handling and WSL environment detection.

@pedramamini
Copy link
Collaborator

@CodeRabbit review

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This PR systematically optimizes UX performance by converting synchronous filesystem operations to async throughout the main process, wrapping React components in React.memo and extracting expensive computations to memoized hooks, and adding database compound indexes for query performance.

Changes

Cohort / File(s) Summary
Audit & Configuration
.ai-audit/*
Added audit tracking metadata, configuration, manifest, and annotations for VIBES standard compliance.
Test Infrastructure - FS Mocking
`src/tests/main/(debug-package/collectors
history-manager
IPC Handler Tests
src/__tests__/main/ipc/handlers/*.test.ts
Updated history, stats, and director-notes handler test mocks to return resolved promises (mockResolvedValue) instead of immediate values.
Process Manager Tests
src/__tests__/main/process-manager/spawners/ChildProcessSpawner.test.ts
Converted spawn invocations to async/await; updated saveImageToTempFile mocks to return resolved promises.
Codex Parser Tests
src/__tests__/main/parsers/codex-output-parser.test.ts
Added new test suite for config caching behavior; introduced __codexConfigTestUtils export for test introspection.
History Manager
src/main/history-manager.ts
Major async conversion: migrated all fs operations from sync to fs.promises; 12 public methods now return Promises; added fileExists() helper for async path checks.
Stats Database
src/main/stats/stats-db.ts
Converted initialize(), getDatabaseSize(), vacuum(), and vacuumIfNeeded() to async; introduced pathExists() helper; all fs operations now use fs.promises.
Stats Initialization
`src/main/stats/(singleton
schema).ts`
Process Management
`src/main/process-manager/(ProcessManager
spawners/ChildProcessSpawner
IPC Handlers - History & Metadata
`src/main/ipc/handlers/(history
director-notes
IPC Handlers - Agent & Git
`src/main/ipc/handlers/(agentSessions
git).ts`
Utilities - WSL & Grooming
`src/main/utils/(wslDetector
context-groomer).ts`
Group Chat Collectors
`src/main/debug-package/collectors/(group-chats
storage).ts`
Web Server Callback Types
`src/main/web-server/(managers/CallbackRegistry
routes/apiRoutes
Codex Parser Config
src/main/parsers/codex-output-parser.ts
Added Codex config caching with TTL and invalidation; exposed __codexConfigTestUtils for test introspection; parser now applies cached config at runtime.
React Components - Memoization
`src/renderer/components/(ConfirmModal
ContextWarningSash
React Components - Style Extraction
`src/renderer/components/(AICommandsPanel
SessionList
React Components - Computation Memoization
`src/renderer/components/(AutoRun
CsvTableRenderer
React Components - Data Fetching
`src/renderer/components/UsageDashboard/(AutoRunStats
LongestAutoRunsTable
React Components - Utilities
`src/renderer/components/(AgentSessionsBrowser
UsageDashboard/EmptyState).tsx`
React Hooks
`src/renderer/hooks/(agent/useFilteredAndSortedSessions
stats/useStats).ts`

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.36% 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 'Adopt async file IO for history and stats' directly summarizes the main change in the changeset—converting synchronous file I/O operations to asynchronous patterns throughout the codebase.
Linked Issues check ✅ Passed The PR comprehensively implements all six coding-focused PERF objectives from issue #371: PERF-01 (React.memo wrapping), PERF-02 (memoizing computations), PERF-03 (extracting inline styles), PERF-04 (async history-manager), PERF-05 (async stats/collectors), and PERF-06 (DB indexes and IPC parallelization).
Out of Scope Changes check ✅ Passed All changes align with the performance optimization objectives in #371. The VIBES metadata audit files (.ai-audit/) are supporting artifacts for the performance work, and all code changes target React rendering, async I/O, or database query performance as specified.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

@pedramamini
Copy link
Collaborator

@greptile review
@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

@pedramamini I'll review the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps
Copy link

greptile-apps bot commented Feb 19, 2026

Greptile Summary

This PR implements a comprehensive performance optimization sweep across React rendering, main process I/O, and database queries. All changes are behavior-preserving and follow established patterns from CLAUDE-PERFORMANCE.md.

Key improvements:

  • React layer: Added React.memo to 20+ high-churn components, memoized expensive computations (session sorting, regex compilation, date parsing), and extracted inline styles to prevent re-render allocations
  • Main process: Converted all synchronous file operations in history-manager.ts and stats-db.ts to async fs.promises.* to prevent blocking the main thread
  • Git operations: Parallelized independent git queries using Promise.all, achieving 4x speed improvement for worktree detection
  • Agent session discovery: Optimized with batched parallelization (10 files at a time) and 30-second caching, significantly reducing filesystem scan overhead
  • Database: Added compound indexes (time+agent, time+project, time+source) for common query patterns in stats dashboard
  • Shared utilities: Replaced custom debounce in useStats with shared useDebouncedCallback to reduce redundant data fetches

Test coverage: All changes include comprehensive test updates. The async conversions required updating 15+ test files to use fs.promises mocks and await async methods.

The .ai-audit/ directory contains VIBES metadata for AI code provenance tracking (annotations, config, manifest).

Confidence Score: 5/5

  • Safe to merge - behavior-preserving performance optimizations with comprehensive test coverage
  • All changes follow documented patterns from CLAUDE-PERFORMANCE.md, maintain identical behavior, and include thorough test updates. React.memo wraps cannot break functionality, async I/O follows Node.js best practices, git parallelization maintains result semantics, and database indexes only accelerate reads.
  • No files require special attention

Important Files Changed

Filename Overview
src/main/history-manager.ts Converted all sync fs operations to async fs.promises for non-blocking I/O, added fileExists helper
src/main/stats/stats-db.ts Converted sync fs operations to async for initialization, backup, and vacuum operations
src/main/ipc/handlers/git.ts Parallelized independent git queries using Promise.all for 4x faster worktree detection
src/main/ipc/handlers/agentSessions.ts Optimized session discovery with batched parallelization and 30s caching for faster scans
src/main/stats/schema.ts Added compound indexes for common query patterns (time+agent, time+project, time+source)
src/renderer/components/SessionListItem.tsx Wrapped in React.memo, extracted inline styles to useMemo for stable references
src/renderer/components/AutoRun.tsx Pre-compiled regex at module level, memoized expensive regex matches and computations
src/renderer/hooks/stats/useStats.ts Replaced custom debounce with shared useDebouncedCallback to reduce redundant fetches

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Performance Optimizations] --> B[React Layer]
    A --> C[Main Process]
    A --> D[Database Layer]
    
    B --> B1[React.memo wraps]
    B --> B2[useMemo for computations]
    B --> B3[Style extraction]
    B1 --> B1a[SessionListItem]
    B1 --> B1b[ExecutionQueueIndicator]
    B1 --> B1c[14 UsageDashboard charts]
    B2 --> B2a[Session list sorting]
    B2 --> B2b[Regex pre-compilation]
    B2 --> B2c[Date parsing cache]
    B3 --> B3a[Inline styles → useMemo]
    
    C --> C1[Async File I/O]
    C --> C2[Query Parallelization]
    C --> C3[Session Discovery]
    C1 --> C1a[history-manager.ts]
    C1 --> C1b[stats-db.ts]
    C1 --> C1c[imageUtils.ts]
    C2 --> C2a[Git handlers - Promise.all]
    C3 --> C3a[Batched scanning]
    C3 --> C3b[30s cache TTL]
    
    D --> D1[Compound Indexes]
    D --> D2[Async Init/Vacuum]
    D1 --> D1a[time+agent]
    D1 --> D1b[time+project]
    D1 --> D1c[time+source]
Loading

Last reviewed commit: f1e54db

Copy link

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx (1)

128-138: ⚠️ Potential issue | 🟠 Major

Error handling swallows exceptions instead of bubbling to Sentry.

The catch block logs to console.error and then continues, which violates the coding guideline requiring unexpected errors to bubble up to Sentry for tracking. For a data-fetching failure that the user cannot recover from directly, either let it bubble or explicitly report it via Sentry utilities.

Proposed fix using Sentry utilities
+import { captureException } from '../../../utils/sentry';
+
 const fetchData = useCallback(async () => {
   setLoading(true);
   try {
     const autoRunSessions = await window.maestro.stats.getAutoRunSessions(timeRange);
     setSessions(autoRunSessions);
   } catch (err) {
-    console.error('Failed to fetch Auto Run sessions for table:', err);
+    captureException(err, { extra: { context: 'LongestAutoRunsTable fetchData', timeRange } });
+    // Optionally set an error state to show user feedback
   } finally {
     setLoading(false);
   }
 }, [timeRange]);

As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking."

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

In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx` around lines
128 - 138, The fetchData function currently swallows errors in its catch by only
calling console.error; replace that with error reporting to Sentry or rethrowing
so exceptions are not silently ignored. Update the catch in fetchData (wrapping
window.maestro.stats.getAutoRunSessions) to call the project's Sentry utility
(e.g., Sentry.captureException or the app's captureError helper) with the caught
err and a helpful message, then rethrow or allow the error to bubble instead of
solely logging; ensure setLoading(false) still runs in finally and preserve
existing setSessions behavior.
src/renderer/components/SessionList.tsx (1)

1799-1813: ⚠️ Potential issue | 🟡 Minor

Inconsistent indentation in button element.

The changed lines (1799-1800, 1804-1805) have different indentation than the unchanged lines within the same element (1801-1803, 1806). The onClick handler body and title attribute are more deeply indented than other attributes.

As per coding guidelines, src/**/*.{ts,tsx,js,jsx}: Use tabs for indentation, not spaces. Always match existing file indentation when editing.

Suggested fix to align indentation
 						{/* Drawer handle at bottom - click to collapse */}
-					<button
-						onClick={(e) => {
-								e.stopPropagation();
-								onToggleWorktreeExpanded(session.id);
-							}}
-						className="w-full flex items-center justify-center gap-1.5 py-0.5 text-[9px] font-medium hover:opacity-80 transition-opacity cursor-pointer"
-						style={sessionListStyles.worktreeCollapseButton}
-							title="Click to collapse worktrees"
-						>
+						<button
+							onClick={(e) => {
+								e.stopPropagation();
+								onToggleWorktreeExpanded(session.id);
+							}}
+							className="w-full flex items-center justify-center gap-1.5 py-0.5 text-[9px] font-medium hover:opacity-80 transition-opacity cursor-pointer"
+							style={sessionListStyles.worktreeCollapseButton}
+							title="Click to collapse worktrees"
+						>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 1799 - 1813, Fix the
inconsistent indentation in the button JSX inside SessionList component:
re-indent the onClick handler body, title attribute, and surrounding attributes
to match the file's existing tab-based indentation style (use tabs, not spaces)
so all attributes and children lines align with the rest of the button element;
specifically update the JSX for the button that calls
onToggleWorktreeExpanded(session.id) and uses
sessionListStyles.worktreeCollapseButton, GitBranch, ChevronUp, and
worktreeChildren so the attribute lines and inner lines have uniform tab
indentation consistent with the file.
src/renderer/components/AICommandsPanel.tsx (1)

209-259: ⚠️ Potential issue | 🟡 Minor

Add tabIndex/outline-none to interactive controls.
The updated buttons still omit the focus requirements in renderer components. Please apply tabIndex and outline-none (and replicate to other buttons in this panel).

Suggested patch
 				<button
+					tabIndex={0}
 					onClick={() => setVariablesExpanded(!variablesExpanded)}
-					className="w-full px-3 py-2 flex items-center justify-between hover:bg-white/5 transition-colors"
+					className="w-full px-3 py-2 flex items-center justify-between hover:bg-white/5 transition-colors outline-none"
 				>
@@
 					<button
+						tabIndex={0}
 						onClick={() => setIsCreating(true)}
-						className="flex items-center gap-2 px-4 py-2 rounded text-sm font-medium transition-all"
+						className="flex items-center gap-2 px-4 py-2 rounded text-sm font-medium transition-all outline-none"
 					>

As per coding guidelines: src/renderer/**/*.{tsx,jsx}: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.

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

In `@src/renderer/components/AICommandsPanel.tsx` around lines 209 - 259, The
interactive controls (the toggle button that calls setVariablesExpanded and the
"Add Command" button that calls setIsCreating) are missing keyboard focus
attributes; add tabIndex={0} and the Tailwind class "outline-none" to these
button elements (and other buttons in this panel, including the variable items
if they should be interactive) so they are focusable, and where a control should
auto-focus (e.g., when opening creation UI setIsCreating true) use a ref
callback like ref={(el) => el?.focus()} on that element; ensure you update the
JSX around the variables toggle (variablesExpanded, setVariablesExpanded),
TEMPLATE_VARIABLES_GENERAL items, and the Add Command button to include these
changes.
src/renderer/components/UsageDashboard/ChartSkeletons.tsx (1)

47-77: ⚠️ Potential issue | 🟡 Minor

Add focus attributes to all skeleton root containers.
These skeleton components render top-level containers missing tabIndex, outline-none, and autofocus refs required by the renderer component guidelines. Apply focus attributes to each of the following skeleton containers:

  • SummaryCardsSkeleton (lines 52-57)
  • AgentComparisonChartSkeleton (lines 86-89)
  • SourceDistributionChartSkeleton (lines 133-136)
  • ActivityHeatmapSkeleton (lines 183-186)
  • DurationTrendsChartSkeleton (lines 264-267)
  • AutoRunStatsSkeleton (lines 332-336)
  • DashboardSkeleton (line 370)
🔧 Example update
-		<div
-			className="grid gap-4"
+		<div
+			className="grid gap-4 outline-none"
+			tabIndex={-1}
+			ref={(el) => el?.focus()}
 			style={{
 				gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
 			}}
 			data-testid="summary-cards-skeleton"

Per coding guidelines: "src/renderer/**/*.{tsx,jsx}: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components."

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

In `@src/renderer/components/UsageDashboard/ChartSkeletons.tsx` around lines 47 -
77, The root containers of several skeleton components (SummaryCardsSkeleton,
AgentComparisonChartSkeleton, SourceDistributionChartSkeleton,
ActivityHeatmapSkeleton, DurationTrendsChartSkeleton, AutoRunStatsSkeleton,
DashboardSkeleton) lack focus attributes; update each root <div> to include a
focusable/tabbable setup by adding tabIndex={0} or tabIndex={-1} as appropriate,
the "outline-none" class, and a ref that auto-focuses (ref={el => el?.focus()})
per renderer guidelines so keyboard/focus handling works; modify the root
container in the listed components (e.g., the outer div in SummaryCardsSkeleton)
to include these three changes.
src/renderer/components/ExecutionQueueIndicator.tsx (1)

137-146: ⚠️ Potential issue | 🟡 Minor

Add explicit focus affordances to comply with renderer accessibility rules.

The indicator button is interactive but lacks the required explicit focus attributes/classes. Please add a tabIndex and outline-none class here.

🔧 Proposed fix
 		<button
 			ref={containerRef}
 			onClick={onClick}
+			tabIndex={0}
 			className="w-full mb-2 px-3 py-2 rounded-lg border flex items-center gap-2 text-sm transition-all hover:opacity-90 outline-none"
 			style={{
 				backgroundColor: theme.colors.bgActivity,
 				borderColor: theme.colors.border,
 				color: theme.colors.textMain,
 			}}
 		>

As per coding guidelines, all renderer components must include tabIndex and outline-none to ensure focus works correctly.

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

In `@src/renderer/components/ExecutionQueueIndicator.tsx` around lines 137 - 146,
The ExecutionQueueIndicator button lacks explicit focus affordances; update the
JSX for the button element (the one using ref={containerRef} and
onClick={onClick} inside the ExecutionQueueIndicator component) to include
tabIndex={0} and add the "outline-none" class to its className so keyboard focus
is enabled while preserving existing styles and classes.
src/main/stats/stats-db.ts (1)

175-182: ⚠️ Potential issue | 🟡 Minor

Handle unexpected fs errors explicitly in size/path checks.

getDatabaseSize and pathExists currently treat any error as “missing,” which can mask permission/IO failures and skip maintenance. Consider only suppressing ENOENT and rethrowing others.

🛠️ Suggested guard for ENOENT-only handling
-	async getDatabaseSize(): Promise<number> {
-		try {
-			const stats = await StatsDB.fsPromises.stat(this.dbPath);
-			return stats.size;
-		} catch {
-			return 0;
-		}
-	}
+	async getDatabaseSize(): Promise<number> {
+		try {
+			const stats = await StatsDB.fsPromises.stat(this.dbPath);
+			return stats.size;
+		} catch (error) {
+			if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+				return 0;
+			}
+			throw error;
+		}
+	}
@@
-	private async pathExists(targetPath: string): Promise<boolean> {
-		try {
-			await StatsDB.fsPromises.access(targetPath, fs.constants.F_OK);
-			return true;
-		} catch {
-			return false;
-		}
-	}
+	private async pathExists(targetPath: string): Promise<boolean> {
+		try {
+			await StatsDB.fsPromises.access(targetPath, fs.constants.F_OK);
+			return true;
+		} catch (error) {
+			if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+				return false;
+			}
+			throw error;
+		}
+	}

As per coding guidelines, handle only expected/recoverable errors explicitly with specific error codes and let unexpected errors bubble up to Sentry.

Also applies to: 832-838

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

In `@src/main/stats/stats-db.ts` around lines 175 - 182, The current
getDatabaseSize (using StatsDB.fsPromises.stat on this.dbPath) and pathExists
swallow all errors; change both to catch errors explicitly and only suppress
ENOENT: in getDatabaseSize, after awaiting StatsDB.fsPromises.stat(this.dbPath)
catch the error, if error.code === 'ENOENT' return 0, otherwise rethrow the
error; similarly in pathExists (the function that checks this.dbPath via
fsPromises.access/stat) catch and return false only for error.code === 'ENOENT'
and rethrow other errors so unexpected permission/IO failures surface to Sentry.
src/main/history-manager.ts (1)

209-330: ⚠️ Potential issue | 🟠 Major

Serialize async read‑modify‑write operations to avoid lost history entries.

Concurrent IPC calls to addEntry, deleteEntry, updateEntry, clearSession, and updateSessionNameByClaudeSessionId can interleave on the same session, causing last-write-wins data loss. Implement per-session write serialization using a Promise queue (as suggested in the comment) to preserve operation ordering.

🧵 Suggested per-session write queue (example with addEntry)
@@
 export class HistoryManager {
 	private historyDir: string;
 	private legacyFilePath: string;
 	private migrationMarkerPath: string;
 	private configDir: string;
 	private watcher: fs.FSWatcher | null = null;
+	private writeQueues = new Map<string, Promise<unknown>>();
@@
+	private enqueueWrite<T>(sessionId: string, task: () => Promise<T>): Promise<T> {
+		const prev = this.writeQueues.get(sessionId) ?? Promise.resolve();
+		const next = prev.then(task, task);
+		const cleanup = next.finally(() => {
+			if (this.writeQueues.get(sessionId) === cleanup) {
+				this.writeQueues.delete(sessionId);
+			}
+		});
+		this.writeQueues.set(sessionId, cleanup);
+		return next;
+	}
@@
-	async addEntry(sessionId: string, projectPath: string, entry: HistoryEntry): Promise<void> {
-		const filePath = this.getSessionFilePath(sessionId);
-		let data: HistoryFileData;
-
-		if (await this.fileExists(filePath)) {
-			try {
-				data = JSON.parse(await fsPromises.readFile(filePath, 'utf-8'));
-			} catch {
-				data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] };
-			}
-		} else {
-			data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] };
-		}
-
-		// Add to beginning (most recent first)
-		data.entries.unshift(entry);
-
-		// Trim to max entries
-		if (data.entries.length > MAX_ENTRIES_PER_SESSION) {
-			data.entries = data.entries.slice(0, MAX_ENTRIES_PER_SESSION);
-		}
-
-		// Update projectPath if it changed
-		data.projectPath = projectPath;
-
-		try {
-			await fsPromises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
-			logger.debug(`Added history entry for session ${sessionId}`, LOG_CONTEXT);
-		} catch (error) {
-			logger.error(`Failed to write history for session ${sessionId}: ${error}`, LOG_CONTEXT);
-			captureException(error, { operation: 'history:write', sessionId });
-		}
-	}
+	async addEntry(sessionId: string, projectPath: string, entry: HistoryEntry): Promise<void> {
+		return this.enqueueWrite(sessionId, async () => {
+			const filePath = this.getSessionFilePath(sessionId);
+			let data: HistoryFileData;
+
+			if (await this.fileExists(filePath)) {
+				try {
+					data = JSON.parse(await fsPromises.readFile(filePath, 'utf-8'));
+				} catch {
+					data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] };
+				}
+			} else {
+				data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] };
+			}
+
+			// Add to beginning (most recent first)
+			data.entries.unshift(entry);
+
+			// Trim to max entries
+			if (data.entries.length > MAX_ENTRIES_PER_SESSION) {
+				data.entries = data.entries.slice(0, MAX_ENTRIES_PER_SESSION);
+			}
+
+			// Update projectPath if it changed
+			data.projectPath = projectPath;
+
+			try {
+				await fsPromises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
+				logger.debug(`Added history entry for session ${sessionId}`, LOG_CONTEXT);
+			} catch (error) {
+				logger.error(`Failed to write history for session ${sessionId}: ${error}`, LOG_CONTEXT);
+				captureException(error, { operation: 'history:write', sessionId });
+			}
+		});
+	}

Apply the same queuing pattern to deleteEntry, updateEntry, clearSession, and updateSessionNameByClaudeSessionId.

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

In `@src/main/history-manager.ts` around lines 209 - 330, Concurrent
read-modify-write operations on the same session can interleave and lose
entries; implement a per-session Promise queue to serialize operations by adding
a Map<string, Promise<any>> (e.g., sessionQueues) and, in each mutating method
(addEntry, deleteEntry, updateEntry, clearSession and
updateSessionNameByClaudeSessionId), replace the direct body with an enqueued
task: set sessionQueues[sessionId] = (sessionQueues[sessionId] ||
Promise.resolve()).then(() => { /* original logic */ }); ensure the enqueued
callback returns the final Promise so callers await completion, catch/log errors
inside the task, and finally clear the map entry when the chain settles to avoid
memory leaks; keep the existing read/write/error handling but run it inside the
queued task to preserve ordering.
src/main/ipc/handlers/tabNaming.ts (1)

197-263: ⚠️ Potential issue | 🟡 Minor

Clean up listeners/timeouts on spawn failure to avoid leaks.
If spawn rejects, the timeout and listeners remain registered and may fire after the Promise is already rejected.

🧹 Proposed fix
-						(async () => {
-							try {
-								await processManager.spawn({
-									sessionId,
-									toolType: config.agentType,
-									cwd,
-									command,
-									args: finalArgs,
-									prompt: fullPrompt,
-									customEnvVars,
-									sendPromptViaStdin: shouldSendPromptViaStdin,
-								});
-							} catch (error) {
-								reject(error);
-							}
-						})();
+						(async () => {
+							try {
+								await processManager.spawn({
+									sessionId,
+									toolType: config.agentType,
+									cwd,
+									command,
+									args: finalArgs,
+									prompt: fullPrompt,
+									customEnvVars,
+									sendPromptViaStdin: shouldSendPromptViaStdin,
+								});
+							} catch (error) {
+								if (!resolved) {
+									resolved = true;
+									clearTimeout(timeoutId);
+									processManager.off('data', onData);
+									processManager.off('exit', onExit);
+								}
+								reject(error);
+							}
+						})();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 197 - 263, If
processManager.spawn rejects, the timeout and listeners (onData/onExit) are left
registered causing leaks; update the spawn error handler inside the async IIFE
so it clears the timeout (timeoutId), removes listeners via
processManager.off('data', onData) and processManager.off('exit', onExit), and
ensures the promise is settled exactly once by checking/setting the resolved
flag before rejecting (or resolving) with the error; reference
processManager.spawn, onData, onExit, timeoutId, resolved and sessionId to
locate and modify the handler.
src/main/web-server/routes/apiRoutes.ts (1)

326-338: ⚠️ Potential issue | 🟠 Major

Capture history failures via Sentry instead of swallowing the error.

The new async getHistory can reject; the current catch returns a 500 without Sentry reporting. Please capture (or rethrow) unexpected errors so they aren’t lost.

Suggested fix
+import { captureException } from '../../utils/sentry';
...
				} catch (error: any) {
+					captureException(error, {
+						tags: { context: 'WebServer:API /history' },
+						extra: { projectPath, sessionId },
+					});
					return reply.code(500).send({
						error: 'Internal Server Error',
						message: `Failed to fetch history: ${error.message}`,
						timestamp: Date.now(),
					});
				}

As per coding guidelines, “Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.”

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

In `@src/main/web-server/routes/apiRoutes.ts` around lines 326 - 338, The catch
block around this.callbacks.getHistory in the route handler currently returns a
500 without reporting the error; update the handler to call the Sentry utilities
(import captureException and/or captureMessage from src/utils/sentry) when an
unexpected error occurs in getHistory (or rethrow after reporting) so failures
are reported to Sentry; specifically, in the catch for the getHistory call
(references: getHistory, this.callbacks.getHistory) call captureException(error,
{ extra: { projectPath, sessionId } }) or captureMessage with context before
sending the reply (or rethrow) to ensure the error is captured by Sentry.
src/main/process-manager/utils/imageUtils.ts (2)

33-45: ⚠️ Potential issue | 🟠 Major

Report write failures to Sentry instead of only logging.

The catch block logs and returns null, which can mask unexpected IO errors without Sentry visibility. Please capture exceptions (or rethrow unexpected ones) with context.

Suggested fix
+import { captureException } from '../../utils/sentry';
...
	} catch (error) {
+		captureException(error, {
+			tags: { context: 'ProcessManager:saveImageToTempFile' },
+			extra: { tempPath },
+		});
		logger.error('[ProcessManager] Failed to save image to temp file', 'ProcessManager', {
			error: String(error),
		});
		return null;
	}

As per coding guidelines, “Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.”

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

In `@src/main/process-manager/utils/imageUtils.ts` around lines 33 - 45, The catch
block around Buffer.from(parsed.base64) / fsPromises.writeFile currently only
logs and returns null; change it to capture the exception with the Sentry helper
and let it bubble instead of silently swallowing it: import captureException
from 'src/utils/sentry', call captureException(error, { extra: { tempPath,
parsedBase64Preview: parsed.base64?.slice(0,100), attemptedSize: buffer?.length
} }) (or similar contextual fields) inside the catch, keep or augment the
logger.error, and then rethrow the caught error (throw error) so it surfaces to
calling code/Sentry rather than returning null. Ensure you reference
parsed.base64, tempPath and the write operation (fsPromises.writeFile) when
adding context.

22-36: ⚠️ Potential issue | 🟠 Major

Error handling must use Sentry for explicit error reporting.

The function correctly awaits at its call site (ChildProcessSpawner.ts:112), but the catch block (lines 41-46) silently swallows the exception using only logger.error. Per coding guidelines, use Sentry utilities from src/utils/sentry to explicitly report file write failures with context, then return null or rethrow as appropriate.

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

In `@src/main/process-manager/utils/imageUtils.ts` around lines 22 - 36, The catch
block in saveImageToTempFile currently only logs the error; update it to import
the Sentry utilities from src/utils/sentry and call the appropriate reporting
function (e.g., captureException or reportError) inside the catch that surrounds
fsPromises.writeFile, passing the caught exception plus context (function name
saveImageToTempFile, index, tempPath, ext, and parsed.mediaType or a safe subset
of dataUrl info) so failures are explicitly reported to Sentry, then return null
(preserving current behavior) instead of silently swallowing the error.
🧹 Nitpick comments (7)
src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx (1)

229-235: Inline style objects recreated on each row render.

The onMouseEnter/onMouseLeave handlers set backgroundColor directly on the element, which is fine for hover state. However, the initial style object on lines 225-228 is recreated every render for every row. For a table capped at 25 rows this is minor, but if you want to squeeze out more perf, consider CSS :nth-child(even) or :hover pseudo-classes instead of JS-driven hover and alternating styles.

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

In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx` around lines
229 - 235, The per-row inline style and the JS hover handlers
(onMouseEnter/onMouseLeave) in LongestAutoRunsTable recreate style objects on
every render; replace the JS-driven alternating-row and hover styling with CSS
rules instead (use a class on the row element and define :nth-child(even) for
alternating background and :hover for hover state using theme colors or CSS
variables derived from theme.colors), or if you must keep inline styles, memoize
the base style per index (e.g., compute once using a stable helper) and remove
the onMouseEnter/onMouseLeave backgroundColor mutations so the row style is
static and not recreated on each render.
src/renderer/components/UsageDashboard/EmptyState.tsx (1)

36-36: Consider extracting inline styles to constants.

The inline style objects ({ color: theme.colors.textDim }, { opacity: 0.3 }, etc.) create new references on each render, which can cause unnecessary child re-renders even when values haven't changed. For full memoization benefit, consider extracting static styles and using useMemo for theme-dependent ones.

♻️ Optional: Extract inline styles
+import { memo, useMemo } from 'react';
-import { memo } from 'react';
 import { BarChart3 } from 'lucide-react';
 import type { Theme } from '../../types';

+const illustrationStyle = { opacity: 0.3 };
+const decorativeSvgStyle = { opacity: 0.5 };
+
 export const EmptyState = memo(function EmptyState({
 	theme,
 	title = 'No usage data yet',
 	message = 'Start using Maestro to see your stats!',
 }: EmptyStateProps) {
+	const textDimStyle = useMemo(() => ({ color: theme.colors.textDim }), [theme.colors.textDim]);
+	const textMainStyle = useMemo(() => ({ color: theme.colors.textMain }), [theme.colors.textMain]);
+
 	return (
 		<div
 			className="h-full flex flex-col items-center justify-center gap-4"
-			style={{ color: theme.colors.textDim }}
+			style={textDimStyle}
 			data-testid="usage-dashboard-empty"
 		>
 			{/* Subtle chart illustration */}
-			<div className="relative" style={{ opacity: 0.3 }}>
+			<div className="relative" style={illustrationStyle}>

Also applies to: 40-40, 49-49, 59-59

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

In `@src/renderer/components/UsageDashboard/EmptyState.tsx` at line 36, The inline
style objects in the EmptyState component (e.g., style={{ color:
theme.colors.textDim }}, style={{ opacity: 0.3 }}, and other similar inline
styles) should be extracted so they don't get re-created on every render; move
static styles to module-level constants and wrap theme-dependent style objects
in a useMemo inside the EmptyState function (referencing theme) and replace the
inline style props on the relevant JSX elements with those memoized constants
(look for the style props in EmptyState.tsx around the color/opacity usages).
src/renderer/components/QRCode.tsx (1)

57-61: Consider reporting error to Sentry instead of only logging to console.

The error is caught and displayed to the user, which is good. However, per coding guidelines, consider using Sentry utilities from src/utils/sentry for error reporting with context, rather than only logging to console. This would help track QR generation failures in production.

Suggested change
 			.catch((err) => {
-				console.error('Failed to generate QR code:', err);
+				// QR generation failure is recoverable - show error UI
 				setError('Failed to generate QR code');
 				setDataUrl(null);
 			});

Or if tracking is desired:

import { captureException } from '@/utils/sentry';
// ...
.catch((err) => {
	captureException(err, { extra: { value, size } });
	setError('Failed to generate QR code');
	setDataUrl(null);
});

As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks" and "Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context."

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

In `@src/renderer/components/QRCode.tsx` around lines 57 - 61, Replace the
console.error-only catch in QRCode.tsx with a Sentry report: import
captureException from src/utils/sentry and call captureException(err, { extra: {
value, size } }) (or appropriate local variables used for QR generation) inside
the .catch block, then keep the existing setError('Failed to generate QR code')
and setDataUrl(null) so the UI behavior is unchanged; ensure the import and the
use of captureException reference the same names used elsewhere
(captureException) and include relevant contextual fields.
src/renderer/components/UsageDashboard/AutoRunStats.tsx (1)

90-130: Consider memoizing MetricCard for additional gains.

MetricCard is rendered 6 times within this component. Since the parent is now memoized, wrapping MetricCard with React.memo could prevent unnecessary re-renders when only individual card props change.

♻️ Optional: Memoize MetricCard
+const MetricCard = memo(function MetricCard({ icon, label, value, subValue, theme }: MetricCardProps) {
-function MetricCard({ icon, label, value, subValue, theme }: MetricCardProps) {
 	return (
 		<div
 			className="p-4 rounded-lg flex items-start gap-3"
 			// ... rest unchanged
 		</div>
 	);
-}
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/UsageDashboard/AutoRunStats.tsx` around lines 90 -
130, MetricCard is a plain functional component rendered multiple times so wrap
it with React.memo to avoid unnecessary re-renders; update the MetricCard
export/definition to use React.memo(MetricCard) (or assign const MemoMetricCard
= React.memo(MetricCard) and use that) and ensure any props used (icon, label,
value, subValue, theme) are stable or provide a custom comparison function to
React.memo if needed to control re-rendering.
src/renderer/components/SessionList.tsx (1)

2002-2003: Misleading comment about effect dependencies.

The comment on lines 2002-2003 states "Keep dependencies limited to sessionFilter and sessions" but the dependency array at line 2040 only includes [sessionFilter]. The sessions variable is used in the effect body (line 2008) but intentionally excluded from dependencies.

The current behavior is likely intentional (only react to filter changes, using latest sessions from that render), but the comment should be updated to accurately reflect this design choice.

Suggested comment fix
-	// Note: Keep dependencies limited to sessionFilter and sessions to avoid unnecessary effect churn
+	// Note: Intentionally depends only on sessionFilter - sessions is accessed from the render closure
+	// when the filter changes, ensuring fresh session data without re-running on every session update
 	useEffect(() => {

Also applies to: 2040-2040

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

In `@src/renderer/components/SessionList.tsx` around lines 2002 - 2003, The
existing comment above the useEffect in SessionList is misleading: it says "Keep
dependencies limited to sessionFilter and sessions" while the dependency array
only includes [sessionFilter] and sessions is intentionally omitted; update the
comment to clearly state that the effect only runs when sessionFilter changes
and that sessions is intentionally read from the current render (i.e., excluded
from dependencies on purpose) so future readers won't assume sessions should be
in the array; reference the useEffect block, the sessionFilter variable, and the
sessions variable when making this clarification.
src/renderer/components/AutoRun.tsx (1)

690-699: Minor: searchQuery is redundant in searchRegex dependencies.

escapedSearchQuery already depends on searchQuery, so including both in the dependency array is unnecessary. This doesn't cause bugs, just a small clarity nit.

♻️ Optional cleanup
 	const searchRegex = useMemo(() => {
 		if (!searchQuery.trim()) {
 			return null;
 		}
 		return new RegExp(escapedSearchQuery, 'gi');
-	}, [escapedSearchQuery, searchQuery]);
+	}, [escapedSearchQuery]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AutoRun.tsx` around lines 690 - 699, The useMemo for
searchRegex redundantly lists searchQuery in its dependency array even though
escapedSearchQuery already updates from searchQuery; update the useMemo for
searchRegex (the function named searchRegex created with useMemo) to depend only
on escapedSearchQuery (remove searchQuery from the dependency array) so the memo
is correctly tied to the derived value.
src/main/debug-package/collectors/group-chats.ts (1)

54-86: Consider parallelizing file processing for improved collection speed.

The current implementation processes group chat files sequentially. For debug package collection with many files, parallel processing could speed this up:

♻️ Optional: Parallel file processing
-	for (const file of files) {
-		if (!file.endsWith('.json') || file.endsWith('.log.json')) {
-			continue;
-		}
-
-		const filePath = path.join(groupChatsPath, file);
-
-		try {
-			const content = await fsPromises.readFile(filePath, 'utf-8');
-			// ... rest of processing
-		} catch {
-			// Skip files that can't be parsed
-		}
-	}
+	const chatFiles = files.filter(
+		(file) => file.endsWith('.json') && !file.endsWith('.log.json')
+	);
+
+	const results = await Promise.all(
+		chatFiles.map(async (file) => {
+			const filePath = path.join(groupChatsPath, file);
+			try {
+				const content = await fsPromises.readFile(filePath, 'utf-8');
+				const chat = JSON.parse(content);
+				const logPath = path.join(groupChatsPath, `${path.basename(file, '.json')}.log.json`);
+				const messageCount = await countMessages(logPath);
+				return {
+					id: chat.id || path.basename(file, '.json'),
+					moderatorAgentId: chat.moderatorAgentId || chat.moderator?.agentId || 'unknown',
+					participantCount: Array.isArray(chat.participants) ? chat.participants.length : 0,
+					participants: Array.isArray(chat.participants)
+						? chat.participants.map((p: any) => ({ agentId: p.agentId || 'unknown' }))
+						: [],
+					messageCount,
+					createdAt: chat.createdAt || 0,
+					updatedAt: chat.updatedAt || 0,
+				} as GroupChatInfo;
+			} catch {
+				return null;
+			}
+		})
+	);
+
+	return results.filter((r): r is GroupChatInfo => r !== null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/debug-package/collectors/group-chats.ts` around lines 54 - 86, The
current for-loop in the group chat collector processes files sequentially
(iterating over files, reading with fsPromises.readFile, parsing JSON, calling
countMessages, and pushing into groupChats), which is slow for many files;
refactor collect logic (the loop that builds chatInfo) to run in parallel by
mapping files to async tasks and using Promise.all (or a bounded concurrency
utility like p-limit) to run readFile/JSON.parse/countMessages concurrently,
ensure each task catches/parses its own errors so a single failure doesn't
reject the entire Promise.all, and finally aggregate successful chatInfo results
into the groupChats array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.ai-audit/annotations.jsonl:
- Line 11: The entry with session_id "1a945b20-4249-4534-a69b-91262e1d9afa" is a
start event without a corresponding end; add a matching JSONL end event object
for that session (type "session", event "end", same session_id and
environment_hash, a later timestamp and same assurance_level/description as
appropriate) so the start/end pair is complete, or remove the incomplete start
line if the session should not be recorded.

In `@src/main/history-manager.ts`:
- Around line 179-185: The fileExists method currently treats any
fsPromises.access error as "missing"; update fileExists to only return false
when the caught error has code 'ENOENT' and rethrow all other errors so
permission/IO errors surface to Sentry. Specifically, inside fileExists (which
calls fsPromises.access with fs.constants.F_OK), inspect the caught error's code
and return false for 'ENOENT' only—otherwise throw the error. Keep the async
signature and behavior unchanged for the ENOENT case.

In `@src/main/ipc/handlers/agentSessions.ts`:
- Around line 241-278: The catch blocks in the session discovery flow (around
fs.access, fs.readdir and inside processProjectDir) are swallowing all errors;
update each catch to only handle expected filesystem error codes (e.g., ENOENT,
ENOTDIR, EACCES) and rethrow any other errors so they bubble up (use the same
pattern for the other catches noted around lines 327-395). In places where you
intentionally handle a recoverable error, log/report it using the Sentry helpers
(captureException or captureMessage from src/utils/sentry) with context (include
claudeProjectsDir, projectDir or filePath and the operation like
fs.stat/fs.readdir), and for all other unknown error.code values rethrow the
error instead of returning empty results (affecting functions/processes
including processProjectDir, the initial fs.access block, and the per-file
fs.stat handling).

In `@src/main/parsers/codex-output-parser.ts`:
- Around line 246-250: The empty .catch on loadCodexConfigCached() swallows all
errors; update the catch in the promise chain that calls
loadCodexConfigCached().then(...).catch(...) to explicitly ignore only expected
"config not found" errors (check error.code === 'ENOENT' or error.message
contains 'not found' or similar), and for any other error call your logger (or
console.error) with context (e.g., "Failed to load Codex config" and the error)
before rethrowing or returning; reference the loadCodexConfigCached and
this.applyCodexConfig call sites to locate where to add the error discrimination
and logging.

In `@src/main/utils/context-groomer.ts`:
- Around line 320-342: The GroomingProcessManager.spawn signature is currently
synchronous but the code calls await processManager.spawn(...); update the
GroomingProcessManager interface method spawn to return Promise<{ pid: number;
success?: boolean } | null> (i.e., make it async) to match the usage in
context-groomer where processManager.spawn is awaited; then adjust any
implementing classes/types to return a Promise as well so implementations
conform to the new async signature (refer to the spawn method on
GroomingProcessManager and the usage processManager.spawn in the spawning
block).

In `@src/main/web-server/types.ts`:
- Around line 294-297: Update the test callback implementations to satisfy the
GetHistoryCallback signature which returns Promise<HistoryEntry[]>: locate uses
in CallbackRegistry.test.ts (the functions passed as history callbacks) and
change their returns from raw arrays (e.g., () => []) to promise-returning ones
(e.g., async () => [] or () => Promise.resolve([])); ensure any helper mocks
used by CallbackRegistry.getHistory() are updated similarly so the type matches
GetHistoryCallback and avoids returning non-Promise values.

In `@src/renderer/components/FilePreview.tsx`:
- Around line 673-680: The regex is built from the raw searchQuery while
hasSearchQuery uses trimmedSearchQuery, causing mismatches; update the useMemo
for searchRegex to construct the RegExp from trimmedSearchQuery (not
searchQuery) so the gating (hasSearchQuery) and pattern creation align—adjust
the dependency list to include trimmedSearchQuery and keep
escapeRegexCharacters, useMemo, trimmedSearchQuery, and hasSearchQuery as the
referenced symbols.
- Around line 289-297: The CODE_FENCE_BOUNDARY_REGEX and HEADING_CAPTURE_REGEX
don't accept the up-to-3 leading spaces allowed by CommonMark, so update
CODE_FENCE_BOUNDARY_REGEX (used in FilePreview) to allow optional 0–3 leading
spaces before the fence marker (e.g., prefix with a non-capturing group like (?:
{0,3})) and update HEADING_CAPTURE_REGEX to allow 0–3 leading spaces before the
ATX hashes (e.g., ^(?: {0,3})(#{1,6})\s+(.+)$) while preserving multiline
behavior; keep other regexes (MARKDOWN_OPEN_TASK_REGEX,
MARKDOWN_CLOSED_TASK_REGEX, etc.) as-is.

---

Outside diff comments:
In `@src/main/history-manager.ts`:
- Around line 209-330: Concurrent read-modify-write operations on the same
session can interleave and lose entries; implement a per-session Promise queue
to serialize operations by adding a Map<string, Promise<any>> (e.g.,
sessionQueues) and, in each mutating method (addEntry, deleteEntry, updateEntry,
clearSession and updateSessionNameByClaudeSessionId), replace the direct body
with an enqueued task: set sessionQueues[sessionId] = (sessionQueues[sessionId]
|| Promise.resolve()).then(() => { /* original logic */ }); ensure the enqueued
callback returns the final Promise so callers await completion, catch/log errors
inside the task, and finally clear the map entry when the chain settles to avoid
memory leaks; keep the existing read/write/error handling but run it inside the
queued task to preserve ordering.

In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 197-263: If processManager.spawn rejects, the timeout and
listeners (onData/onExit) are left registered causing leaks; update the spawn
error handler inside the async IIFE so it clears the timeout (timeoutId),
removes listeners via processManager.off('data', onData) and
processManager.off('exit', onExit), and ensures the promise is settled exactly
once by checking/setting the resolved flag before rejecting (or resolving) with
the error; reference processManager.spawn, onData, onExit, timeoutId, resolved
and sessionId to locate and modify the handler.

In `@src/main/process-manager/utils/imageUtils.ts`:
- Around line 33-45: The catch block around Buffer.from(parsed.base64) /
fsPromises.writeFile currently only logs and returns null; change it to capture
the exception with the Sentry helper and let it bubble instead of silently
swallowing it: import captureException from 'src/utils/sentry', call
captureException(error, { extra: { tempPath, parsedBase64Preview:
parsed.base64?.slice(0,100), attemptedSize: buffer?.length } }) (or similar
contextual fields) inside the catch, keep or augment the logger.error, and then
rethrow the caught error (throw error) so it surfaces to calling code/Sentry
rather than returning null. Ensure you reference parsed.base64, tempPath and the
write operation (fsPromises.writeFile) when adding context.
- Around line 22-36: The catch block in saveImageToTempFile currently only logs
the error; update it to import the Sentry utilities from src/utils/sentry and
call the appropriate reporting function (e.g., captureException or reportError)
inside the catch that surrounds fsPromises.writeFile, passing the caught
exception plus context (function name saveImageToTempFile, index, tempPath, ext,
and parsed.mediaType or a safe subset of dataUrl info) so failures are
explicitly reported to Sentry, then return null (preserving current behavior)
instead of silently swallowing the error.

In `@src/main/stats/stats-db.ts`:
- Around line 175-182: The current getDatabaseSize (using
StatsDB.fsPromises.stat on this.dbPath) and pathExists swallow all errors;
change both to catch errors explicitly and only suppress ENOENT: in
getDatabaseSize, after awaiting StatsDB.fsPromises.stat(this.dbPath) catch the
error, if error.code === 'ENOENT' return 0, otherwise rethrow the error;
similarly in pathExists (the function that checks this.dbPath via
fsPromises.access/stat) catch and return false only for error.code === 'ENOENT'
and rethrow other errors so unexpected permission/IO failures surface to Sentry.

In `@src/main/web-server/routes/apiRoutes.ts`:
- Around line 326-338: The catch block around this.callbacks.getHistory in the
route handler currently returns a 500 without reporting the error; update the
handler to call the Sentry utilities (import captureException and/or
captureMessage from src/utils/sentry) when an unexpected error occurs in
getHistory (or rethrow after reporting) so failures are reported to Sentry;
specifically, in the catch for the getHistory call (references: getHistory,
this.callbacks.getHistory) call captureException(error, { extra: { projectPath,
sessionId } }) or captureMessage with context before sending the reply (or
rethrow) to ensure the error is captured by Sentry.

In `@src/renderer/components/AICommandsPanel.tsx`:
- Around line 209-259: The interactive controls (the toggle button that calls
setVariablesExpanded and the "Add Command" button that calls setIsCreating) are
missing keyboard focus attributes; add tabIndex={0} and the Tailwind class
"outline-none" to these button elements (and other buttons in this panel,
including the variable items if they should be interactive) so they are
focusable, and where a control should auto-focus (e.g., when opening creation UI
setIsCreating true) use a ref callback like ref={(el) => el?.focus()} on that
element; ensure you update the JSX around the variables toggle
(variablesExpanded, setVariablesExpanded), TEMPLATE_VARIABLES_GENERAL items, and
the Add Command button to include these changes.

In `@src/renderer/components/ExecutionQueueIndicator.tsx`:
- Around line 137-146: The ExecutionQueueIndicator button lacks explicit focus
affordances; update the JSX for the button element (the one using
ref={containerRef} and onClick={onClick} inside the ExecutionQueueIndicator
component) to include tabIndex={0} and add the "outline-none" class to its
className so keyboard focus is enabled while preserving existing styles and
classes.

In `@src/renderer/components/SessionList.tsx`:
- Around line 1799-1813: Fix the inconsistent indentation in the button JSX
inside SessionList component: re-indent the onClick handler body, title
attribute, and surrounding attributes to match the file's existing tab-based
indentation style (use tabs, not spaces) so all attributes and children lines
align with the rest of the button element; specifically update the JSX for the
button that calls onToggleWorktreeExpanded(session.id) and uses
sessionListStyles.worktreeCollapseButton, GitBranch, ChevronUp, and
worktreeChildren so the attribute lines and inner lines have uniform tab
indentation consistent with the file.

In `@src/renderer/components/UsageDashboard/ChartSkeletons.tsx`:
- Around line 47-77: The root containers of several skeleton components
(SummaryCardsSkeleton, AgentComparisonChartSkeleton,
SourceDistributionChartSkeleton, ActivityHeatmapSkeleton,
DurationTrendsChartSkeleton, AutoRunStatsSkeleton, DashboardSkeleton) lack focus
attributes; update each root <div> to include a focusable/tabbable setup by
adding tabIndex={0} or tabIndex={-1} as appropriate, the "outline-none" class,
and a ref that auto-focuses (ref={el => el?.focus()}) per renderer guidelines so
keyboard/focus handling works; modify the root container in the listed
components (e.g., the outer div in SummaryCardsSkeleton) to include these three
changes.

In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx`:
- Around line 128-138: The fetchData function currently swallows errors in its
catch by only calling console.error; replace that with error reporting to Sentry
or rethrowing so exceptions are not silently ignored. Update the catch in
fetchData (wrapping window.maestro.stats.getAutoRunSessions) to call the
project's Sentry utility (e.g., Sentry.captureException or the app's
captureError helper) with the caught err and a helpful message, then rethrow or
allow the error to bubble instead of solely logging; ensure setLoading(false)
still runs in finally and preserve existing setSessions behavior.

---

Nitpick comments:
In `@src/main/debug-package/collectors/group-chats.ts`:
- Around line 54-86: The current for-loop in the group chat collector processes
files sequentially (iterating over files, reading with fsPromises.readFile,
parsing JSON, calling countMessages, and pushing into groupChats), which is slow
for many files; refactor collect logic (the loop that builds chatInfo) to run in
parallel by mapping files to async tasks and using Promise.all (or a bounded
concurrency utility like p-limit) to run readFile/JSON.parse/countMessages
concurrently, ensure each task catches/parses its own errors so a single failure
doesn't reject the entire Promise.all, and finally aggregate successful chatInfo
results into the groupChats array.

In `@src/renderer/components/AutoRun.tsx`:
- Around line 690-699: The useMemo for searchRegex redundantly lists searchQuery
in its dependency array even though escapedSearchQuery already updates from
searchQuery; update the useMemo for searchRegex (the function named searchRegex
created with useMemo) to depend only on escapedSearchQuery (remove searchQuery
from the dependency array) so the memo is correctly tied to the derived value.

In `@src/renderer/components/QRCode.tsx`:
- Around line 57-61: Replace the console.error-only catch in QRCode.tsx with a
Sentry report: import captureException from src/utils/sentry and call
captureException(err, { extra: { value, size } }) (or appropriate local
variables used for QR generation) inside the .catch block, then keep the
existing setError('Failed to generate QR code') and setDataUrl(null) so the UI
behavior is unchanged; ensure the import and the use of captureException
reference the same names used elsewhere (captureException) and include relevant
contextual fields.

In `@src/renderer/components/SessionList.tsx`:
- Around line 2002-2003: The existing comment above the useEffect in SessionList
is misleading: it says "Keep dependencies limited to sessionFilter and sessions"
while the dependency array only includes [sessionFilter] and sessions is
intentionally omitted; update the comment to clearly state that the effect only
runs when sessionFilter changes and that sessions is intentionally read from the
current render (i.e., excluded from dependencies on purpose) so future readers
won't assume sessions should be in the array; reference the useEffect block, the
sessionFilter variable, and the sessions variable when making this
clarification.

In `@src/renderer/components/UsageDashboard/AutoRunStats.tsx`:
- Around line 90-130: MetricCard is a plain functional component rendered
multiple times so wrap it with React.memo to avoid unnecessary re-renders;
update the MetricCard export/definition to use React.memo(MetricCard) (or assign
const MemoMetricCard = React.memo(MetricCard) and use that) and ensure any props
used (icon, label, value, subValue, theme) are stable or provide a custom
comparison function to React.memo if needed to control re-rendering.

In `@src/renderer/components/UsageDashboard/EmptyState.tsx`:
- Line 36: The inline style objects in the EmptyState component (e.g., style={{
color: theme.colors.textDim }}, style={{ opacity: 0.3 }}, and other similar
inline styles) should be extracted so they don't get re-created on every render;
move static styles to module-level constants and wrap theme-dependent style
objects in a useMemo inside the EmptyState function (referencing theme) and
replace the inline style props on the relevant JSX elements with those memoized
constants (look for the style props in EmptyState.tsx around the color/opacity
usages).

In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx`:
- Around line 229-235: The per-row inline style and the JS hover handlers
(onMouseEnter/onMouseLeave) in LongestAutoRunsTable recreate style objects on
every render; replace the JS-driven alternating-row and hover styling with CSS
rules instead (use a class on the row element and define :nth-child(even) for
alternating background and :hover for hover state using theme colors or CSS
variables derived from theme.colors), or if you must keep inline styles, memoize
the base style per index (e.g., compute once using a stable helper) and remove
the onMouseEnter/onMouseLeave backgroundColor mutations so the row style is
static and not recreated on each render.

{"type":"session","event":"end","session_id":"f6904b0f-9eda-443e-bbef-dca3328d9838","timestamp":"2026-02-15T06:29:39.768Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session ended"}
{"type":"session","event":"start","session_id":"79b55eb5-d4c1-4af9-9a74-8e1d89ec7b11","timestamp":"2026-02-15T06:29:40.019Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session"}
{"type":"session","event":"end","session_id":"79b55eb5-d4c1-4af9-9a74-8e1d89ec7b11","timestamp":"2026-02-15T06:33:11.783Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session ended"}
{"type":"session","event":"start","session_id":"1a945b20-4249-4534-a69b-91262e1d9afa","timestamp":"2026-02-15T06:33:12.370Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session"}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Possible missing session end event (Line 11).
Line 11 records a start event for session 1a945b20-4249-4534-a69b-91262e1d9afa without a matching end event in this file. If this JSONL is meant to represent completed sessions, consider adding the corresponding end event or trimming the incomplete start.

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

In @.ai-audit/annotations.jsonl at line 11, The entry with session_id
"1a945b20-4249-4534-a69b-91262e1d9afa" is a start event without a corresponding
end; add a matching JSONL end event object for that session (type "session",
event "end", same session_id and environment_hash, a later timestamp and same
assurance_level/description as appropriate) so the start/end pair is complete,
or remove the incomplete start line if the session should not be recorded.

Comment on lines +179 to +185
private async fileExists(filePath: string): Promise<boolean> {
try {
await fsPromises.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t treat non-ENOENT access errors as “missing.”

fileExists currently returns false for any access error; this can hide permission/IO failures. Consider only suppressing ENOENT and rethrowing others.

🛠️ Suggested ENOENT-only handling
-	private async fileExists(filePath: string): Promise<boolean> {
-		try {
-			await fsPromises.access(filePath, fs.constants.F_OK);
-			return true;
-		} catch {
-			return false;
-		}
-	}
+	private async fileExists(filePath: string): Promise<boolean> {
+		try {
+			await fsPromises.access(filePath, fs.constants.F_OK);
+			return true;
+		} catch (error) {
+			if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+				return false;
+			}
+			throw error;
+		}
+	}

As per coding guidelines, handle only expected/recoverable errors explicitly with specific error codes and let unexpected errors bubble up to Sentry.

📝 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
private async fileExists(filePath: string): Promise<boolean> {
try {
await fsPromises.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
private async fileExists(filePath: string): Promise<boolean> {
try {
await fsPromises.access(filePath, fs.constants.F_OK);
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return false;
}
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/history-manager.ts` around lines 179 - 185, The fileExists method
currently treats any fsPromises.access error as "missing"; update fileExists to
only return false when the caught error has code 'ENOENT' and rethrow all other
errors so permission/IO errors surface to Sentry. Specifically, inside
fileExists (which calls fsPromises.access with fs.constants.F_OK), inspect the
caught error's code and return false for 'ENOENT' only—otherwise throw the
error. Keep the async signature and behavior unchanged for the ENOENT case.

Comment on lines 241 to 278
try {
await fs.access(claudeProjectsDir);
} catch {
setCachedSessionFiles('claude-code', files);
return files;
}

const projectDirs = await fs.readdir(claudeProjectsDir);

for (const projectDir of projectDirs) {
const processProjectDir = async (projectDir: string): Promise<SessionFileInfo[]> => {
const collected: SessionFileInfo[] = [];
const projectPath = path.join(claudeProjectsDir, projectDir);
try {
const stat = await fs.stat(projectPath);
if (!stat.isDirectory()) continue;
if (!stat.isDirectory()) return collected;
} catch {
return collected;
}

const dirFiles = await fs.readdir(projectPath);
const sessionFiles = dirFiles.filter((f) => f.endsWith('.jsonl'));
let dirFiles: string[];
try {
dirFiles = await fs.readdir(projectPath);
} catch {
return collected;
}

for (const filename of sessionFiles) {
const filePath = path.join(projectPath, filename);
try {
const fileStat = await fs.stat(filePath);
// Skip 0-byte sessions (created but abandoned before any content was written)
if (fileStat.size === 0) continue;
const sessionKey = `${projectDir}/${filename.replace('.jsonl', '')}`;
files.push({ filePath, sessionKey, mtimeMs: fileStat.mtimeMs });
} catch {
// Skip files we can't stat
}
for (const filename of dirFiles) {
if (!filename.endsWith('.jsonl')) continue;
const filePath = path.join(projectPath, filename);
try {
const fileStat = await fs.stat(filePath);
// Skip 0-byte sessions (created but abandoned before any content was written)
if (fileStat.size === 0) continue;
const sessionKey = `${projectDir}/${filename.replace('.jsonl', '')}`;
collected.push({ filePath, sessionKey, mtimeMs: fileStat.mtimeMs });
} catch {
// Skip files we can't stat
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid swallowing filesystem errors during session discovery.

The new broad catch { ... } blocks return empty results for all errors, which can mask unexpected failures and violates the error-handling policy. Please filter for expected/recoverable error codes (e.g., ENOENT, ENOTDIR, EACCES) and rethrow anything else so it can be captured upstream.

🐛 Suggested pattern (apply similarly to other catch blocks)
-		} catch {
-			return collected;
-		}
+		} catch (error) {
+			const code = (error as NodeJS.ErrnoException).code;
+			if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EACCES') {
+				return collected;
+			}
+			throw error;
+		}

As per coding guidelines: Do NOT silently swallow exceptions with try-catch-console.error blocks. Handle only expected/recoverable errors explicitly with specific error codes. Let unexpected errors bubble up to Sentry. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.

Also applies to: 327-395

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

In `@src/main/ipc/handlers/agentSessions.ts` around lines 241 - 278, The catch
blocks in the session discovery flow (around fs.access, fs.readdir and inside
processProjectDir) are swallowing all errors; update each catch to only handle
expected filesystem error codes (e.g., ENOENT, ENOTDIR, EACCES) and rethrow any
other errors so they bubble up (use the same pattern for the other catches noted
around lines 327-395). In places where you intentionally handle a recoverable
error, log/report it using the Sentry helpers (captureException or
captureMessage from src/utils/sentry) with context (include claudeProjectsDir,
projectDir or filePath and the operation like fs.stat/fs.readdir), and for all
other unknown error.code values rethrow the error instead of returning empty
results (affecting functions/processes including processProjectDir, the initial
fs.access block, and the per-file fs.stat handling).

Comment on lines +246 to +250
void loadCodexConfigCached()
.then((config) => {
this.applyCodexConfig(config);
})
.catch(() => {});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent error swallowing may hide unexpected failures.

The empty .catch(() => {}) at line 250 silently discards all errors from config loading. While missing config files are expected, unexpected errors (permissions, disk I/O) should be logged for debugging. Per coding guidelines, unexpected errors should not be silently swallowed.

🛠️ Suggested fix
 		void loadCodexConfigCached()
 			.then((config) => {
 				this.applyCodexConfig(config);
 			})
-			.catch(() => {});
+			.catch((err) => {
+				// Config loading is optional - log for debugging but don't fail
+				if (process.env.NODE_ENV !== 'test') {
+					console.debug('[CodexOutputParser] Config load failed:', err.message);
+				}
+			});
📝 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 loadCodexConfigCached()
.then((config) => {
this.applyCodexConfig(config);
})
.catch(() => {});
void loadCodexConfigCached()
.then((config) => {
this.applyCodexConfig(config);
})
.catch((err) => {
// Config loading is optional - log for debugging but don't fail
if (process.env.NODE_ENV !== 'test') {
console.debug('[CodexOutputParser] Config load failed:', err.message);
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/parsers/codex-output-parser.ts` around lines 246 - 250, The empty
.catch on loadCodexConfigCached() swallows all errors; update the catch in the
promise chain that calls loadCodexConfigCached().then(...).catch(...) to
explicitly ignore only expected "config not found" errors (check error.code ===
'ENOENT' or error.message contains 'not found' or similar), and for any other
error call your logger (or console.error) with context (e.g., "Failed to load
Codex config" and the error) before rethrowing or returning; reference the
loadCodexConfigCached and this.applyCodexConfig call sites to locate where to
add the error discrimination and logging.

Comment on lines +320 to +342
const spawnProcess = async () => {
const spawnResult = await processManager.spawn({
sessionId: groomerSessionId,
toolType: agentType,
cwd: projectRoot,
command: agent.command,
args: finalArgs,
prompt: prompt, // Triggers batch mode (no PTY)
promptArgs: agent.promptArgs, // For agents using flag-based prompt (e.g., OpenCode -p)
noPromptSeparator: agent.noPromptSeparator,
// Pass SSH config for remote execution support
sessionSshRemoteConfig,
sessionCustomPath,
sessionCustomArgs,
sessionCustomEnvVars,
});

if (!spawnResult || spawnResult.pid <= 0) {
cleanup();
reject(new Error(`Failed to spawn grooming process for ${agentType}`));
return;
}
if (!spawnResult || spawnResult.pid <= 0) {
cleanup();
resolved = true;
reject(new Error(`Failed to spawn grooming process for ${agentType}`));
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for ProcessManager and spawn method definitions
rg -n "interface.*ProcessManager|class.*ProcessManager" --type ts -A 5 | head -50

# Check the spawn method signature specifically
rg -n "spawn\s*\(" --type ts -B 2 -A 3 | grep -A 3 -B 2 "interface\|spawn"

# Look for the specific interface at line 46 in context-groomer.ts
head -50 src/main/utils/context-groomer.ts | tail -10

Repository: RunMaestro/Maestro

Length of output: 50376


🏁 Script executed:

# Find and check the ProcessManager interface/class definition
fd -e ts | xargs rg -l "ProcessManager" | head -10

Repository: RunMaestro/Maestro

Length of output: 477


🏁 Script executed:

# Check context-groomer.ts to see the interface definition around line 46
cat -n src/main/utils/context-groomer.ts | sed -n '40,55p'

Repository: RunMaestro/Maestro

Length of output: 679


🏁 Script executed:

# Check if spawn is async in the actual ProcessManager implementation
rg -n "async spawn" --type ts -B 2 -A 10

Repository: RunMaestro/Maestro

Length of output: 3372


Update GroomingProcessManager interface to reflect async spawn signature. The interface declares spawn() returning { pid: number; success?: boolean } | null, but the actual implementation (lines 321+) uses await, indicating spawn is async. The interface should return Promise<{ pid: number; success?: boolean } | null> to match the ProcessManager implementation.

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

In `@src/main/utils/context-groomer.ts` around lines 320 - 342, The
GroomingProcessManager.spawn signature is currently synchronous but the code
calls await processManager.spawn(...); update the GroomingProcessManager
interface method spawn to return Promise<{ pid: number; success?: boolean } |
null> (i.e., make it async) to match the usage in context-groomer where
processManager.spawn is awaited; then adjust any implementing classes/types to
return a Promise as well so implementations conform to the new async signature
(refer to the spawn method on GroomingProcessManager and the usage
processManager.spawn in the spawning block).

Comment on lines 294 to +297
export type GetHistoryCallback = (
projectPath?: string,
sessionId?: string
) => import('../../shared/types').HistoryEntry[];
) => Promise<import('../../shared/types').HistoryEntry[]>;
Copy link

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
# Find GetHistoryCallback implementers/usages and verify async return shapes.
rg -n --type=ts "GetHistoryCallback" src
rg -n --type=ts "getHistory\s*:" src/main/web-server
rg -n --type=ts "getHistory\s*=" src/main/web-server

Repository: RunMaestro/Maestro

Length of output: 2778


🏁 Script executed:

#!/bin/bash
# Check the actual implementations in test files
sed -n '205,215p' src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
sed -n '560,600p' src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
sed -n '370,385p' src/main/web-server/WebServer.ts

Repository: RunMaestro/Maestro

Length of output: 2612


🏁 Script executed:

#!/bin/bash
# Find getHistory method in CallbackRegistry
sed -n '1,300p' src/main/web-server/managers/CallbackRegistry.ts | grep -A 20 "getHistory\s*("

Repository: RunMaestro/Maestro

Length of output: 768


Fix type mismatch: GetHistoryCallback implementations don't return Promises as required by the type.

The type now requires Promise<HistoryEntry[]> return, but implementations in tests (e.g., () => [] at CallbackRegistry.test.ts:210, 567, 572, etc.) return arrays directly instead of Promises. While CallbackRegistry.getHistory() is async and wraps results at runtime, this creates a type safety violation. Update test implementations to return Promises (e.g., async () => [] or () => Promise.resolve([])).

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

In `@src/main/web-server/types.ts` around lines 294 - 297, Update the test
callback implementations to satisfy the GetHistoryCallback signature which
returns Promise<HistoryEntry[]>: locate uses in CallbackRegistry.test.ts (the
functions passed as history callbacks) and change their returns from raw arrays
(e.g., () => []) to promise-returning ones (e.g., async () => [] or () =>
Promise.resolve([])); ensure any helper mocks used by
CallbackRegistry.getHistory() are updated similarly so the type matches
GetHistoryCallback and avoids returning non-Promise values.

Comment on lines +289 to +297
const MARKDOWN_OPEN_TASK_REGEX = /^[\s]*[-*]\s*\[\s*\]/gm;
const MARKDOWN_CLOSED_TASK_REGEX = /^[\s]*[-*]\s*\[[xX]\]/gm;
const CODE_FENCE_BOUNDARY_REGEX = /^(`{3,}|~{3,})/;
const HEADING_CAPTURE_REGEX = /^(#{1,6})\s+(.+)$/;
const MARKDOWN_HIGHLIGHT_REGEX = /==([^=]+)==/g;
const INLINE_CODE_LANGUAGE_REGEX = /language-(\w+)/;
const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;

const escapeRegexCharacters = (value: string): string => value.replace(REGEX_SPECIAL_CHARS, '\\$&');
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check the exact lines mentioned
head -n 300 src/renderer/components/FilePreview.tsx | tail -n 15

Repository: RunMaestro/Maestro

Length of output: 614


🌐 Web query:

CommonMark specification indented code fences headings leading spaces

💡 Result:

In CommonMark, “leading spaces” matter because most block constructs allow up to 3 spaces of indentation, but 4 spaces generally switches interpretation to an indented code block.

  • ATX headings (#): the opening # may be preceded by up to 3 spaces; with 4 spaces, it’s not a heading (it becomes indented code / literal text). [1][2]
  • Setext headings:
    • The content line must not have more than 3 leading spaces. [1]
    • The underline (--- / ===) may be preceded by up to 3 spaces; 4 spaces is too many. [1][2]
  • Indented code blocks: any non-blank line with 4+ leading spaces is an indented code “chunk” (and the content is taken literally, not parsed as Markdown). [3]
  • Fenced code blocks (``` / ~~~): the opening fence must be preceded by up to 3 spaces; 4 spaces is too many (the fence line is then just literal text inside an indented code block). Closing fences also only allow up to 3 spaces. [4][5]

Sources

  1. CommonMark Spec 0.30 – ATX + Setext heading indentation rules: (spec.commonmark.org)
  2. CommonMark Spec 0.30 – examples showing “four spaces is too many” for headings: (spec.commonmark.org)
  3. CommonMark Spec 0.30 – indented code block definition (4+ spaces): (spec.commonmark.org)
  4. CommonMark Spec 0.30 – fenced code blocks begin with fence indented up to 3 spaces: (spec.commonmark.org)
  5. CommonMark Spec 0.30 – examples showing 4-space-indented fences aren’t fences (and closing fence can’t be indented 4): (spec.commonmark.org)

Citations:


🏁 Script executed:

# Find where CODE_FENCE_BOUNDARY_REGEX and HEADING_CAPTURE_REGEX are used
rg -n "CODE_FENCE_BOUNDARY_REGEX|HEADING_CAPTURE_REGEX" src/renderer/components/FilePreview.tsx

Repository: RunMaestro/Maestro

Length of output: 277


🏁 Script executed:

# Get the function context where these regexes are used
sed -n '310,335p' src/renderer/components/FilePreview.tsx

Repository: RunMaestro/Maestro

Length of output: 749


🏁 Script executed:

# Get more context - find the function signature and how 'lines' is created
sed -n '290,315p' src/renderer/components/FilePreview.tsx | head -n 30

Repository: RunMaestro/Maestro

Length of output: 1029


🏁 Script executed:

# Check how extractHeadings is called - find callers of this function
rg -n "extractHeadings" src/renderer/components/FilePreview.tsx -A 2 -B 2

Repository: RunMaestro/Maestro

Length of output: 461


🏁 Script executed:

# Check if there's any preprocessing of 'line' before regex testing
sed -n '307,335p' src/renderer/components/FilePreview.tsx

Repository: RunMaestro/Maestro

Length of output: 878


Handle indented fences and headings in TOC parsing.

CommonMark allows up to 3 leading spaces before fenced code blocks and ATX headings. With the current regexes, lines with 1–3 leading spaces won't be detected, causing indented fences to go untracked and headings inside code fences to leak into the TOC. Update the regexes to allow optional leading whitespace.

🔧 Suggested regex fix
-const CODE_FENCE_BOUNDARY_REGEX = /^(`{3,}|~{3,})/;
-const HEADING_CAPTURE_REGEX = /^(#{1,6})\s+(.+)$/;
+const CODE_FENCE_BOUNDARY_REGEX = /^\s{0,3}(`{3,}|~{3,})/;
+const HEADING_CAPTURE_REGEX = /^\s{0,3}(#{1,6})\s+(.+)$/;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/FilePreview.tsx` around lines 289 - 297, The
CODE_FENCE_BOUNDARY_REGEX and HEADING_CAPTURE_REGEX don't accept the up-to-3
leading spaces allowed by CommonMark, so update CODE_FENCE_BOUNDARY_REGEX (used
in FilePreview) to allow optional 0–3 leading spaces before the fence marker
(e.g., prefix with a non-capturing group like (?: {0,3})) and update
HEADING_CAPTURE_REGEX to allow 0–3 leading spaces before the ATX hashes (e.g.,
^(?: {0,3})(#{1,6})\s+(.+)$) while preserving multiline behavior; keep other
regexes (MARKDOWN_OPEN_TASK_REGEX, MARKDOWN_CLOSED_TASK_REGEX, etc.) as-is.

Comment on lines +673 to +680
const trimmedSearchQuery = searchQuery.trim();
const hasSearchQuery = trimmedSearchQuery.length > 0;
const searchRegex = useMemo(() => {
if (!hasSearchQuery) {
return null;
}
return new RegExp(escapeRegexCharacters(searchQuery), 'gi');
}, [hasSearchQuery, searchQuery]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Align searchRegex with the trimmed query.

hasSearchQuery is derived from the trimmed value, but the regex is built from the raw query. Leading/trailing spaces can make search look “active” while never matching. Consider building the regex from trimmedSearchQuery to keep gating and matching aligned.

🔧 Suggested tweak
-const searchRegex = useMemo(() => {
-	if (!hasSearchQuery) {
-		return null;
-	}
-	return new RegExp(escapeRegexCharacters(searchQuery), 'gi');
-}, [hasSearchQuery, searchQuery]);
+const searchRegex = useMemo(() => {
+	if (!hasSearchQuery) {
+		return null;
+	}
+	return new RegExp(escapeRegexCharacters(trimmedSearchQuery), 'gi');
+}, [hasSearchQuery, trimmedSearchQuery]);

Also applies to: 2186-2213

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

In `@src/renderer/components/FilePreview.tsx` around lines 673 - 680, The regex is
built from the raw searchQuery while hasSearchQuery uses trimmedSearchQuery,
causing mismatches; update the useMemo for searchRegex to construct the RegExp
from trimmedSearchQuery (not searchQuery) so the gating (hasSearchQuery) and
pattern creation align—adjust the dependency list to include trimmedSearchQuery
and keep escapeRegexCharacters, useMemo, trimmedSearchQuery, and hasSearchQuery
as the referenced symbols.

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.

UX Performance Sweep: Low-Risk High-Reward Snappiness Optimizations

2 participants

Comments