From 2db36982fb8d1072efadc3ea55400e2824a0985a Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:37:39 +0100 Subject: [PATCH] auto-claude: 190-bug-context-page-crash-multiple-root-causes-when-v (#1724) * auto-claude: subtask-1-1 - Add ErrorBoundary wrapper around Context component * auto-claude: subtask-1-2 - Wrap statSync() calls in try-catch in memory-statu * auto-claude: subtask-1-3 - Wrap statSync() calls in try-catch in memory-data-handlers.ts * auto-claude: subtask-1-4 - Add safe property access with optional chaining in MemoryCard.tsx - Add optional chaining to pattern.pattern with fallback to pattern.applies_to - Add optional chaining to gotcha.gotcha with JSON.stringify fallback - Prevents crashes when memory data has malformed discoveries structures - Fixes Root Cause #3: Unsafe Property Access * auto-claude: subtask-1-5 - Add Promise guard flags in memory-service.ts executeQuery() * auto-claude: subtask-1-5 - Add Promise guard flags in memory-service.ts executeQuery() - Store timeoutId to enable cleanup - Add clearTimeout() in close and error handlers - Prevents race condition where timeout fires after Promise resolved - Follows pattern from memory-handlers.ts:262-312 * auto-claude: subtask-1-5 - Add Promise guard flags in memory-service.ts executeQuery() Fix Promise race condition by reordering timeout setup before event handlers. Follows pattern from memory-handlers.ts:262-312 with resolved guard flag, timeout cleanup in close/error handlers, preventing double-resolution crashes. * fix: apply timeout cleanup pattern to executeSemanticQuery matching executeQuery Store the setTimeout ID and clear it in both close and error handlers to prevent timeout resource leaks. Also move resolved flag immediately after the guard check for consistency with executeQuery. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Test User Co-authored-by: Claude Opus 4.6 --- .../context/memory-data-handlers.ts | 16 ++++-- .../context/memory-status-handlers.ts | 9 +++- apps/frontend/src/main/memory-service.ts | 49 ++++++++++++++----- apps/frontend/src/renderer/App.tsx | 5 +- .../components/context/MemoryCard.tsx | 8 ++- 5 files changed, 67 insertions(+), 20 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/context/memory-data-handlers.ts b/apps/frontend/src/main/ipc-handlers/context/memory-data-handlers.ts index 302c5671..153bbeb0 100644 --- a/apps/frontend/src/main/ipc-handlers/context/memory-data-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/context/memory-data-handlers.ts @@ -31,8 +31,12 @@ export function loadFileBasedMemories( const recentSpecDirs = readdirSync(specsDir) .filter((f: string) => { - const specPath = path.join(specsDir, f); - return statSync(specPath).isDirectory(); + try { + const specPath = path.join(specsDir, f); + return statSync(specPath).isDirectory(); + } catch { + return false; + } }) .sort() .reverse() @@ -118,8 +122,12 @@ export function searchFileBasedMemories( const allSpecDirs = readdirSync(specsDir) .filter((f: string) => { - const specPath = path.join(specsDir, f); - return statSync(specPath).isDirectory(); + try { + const specPath = path.join(specsDir, f); + return statSync(specPath).isDirectory(); + } catch { + return false; + } }); for (const specDir of allSpecDirs) { diff --git a/apps/frontend/src/main/ipc-handlers/context/memory-status-handlers.ts b/apps/frontend/src/main/ipc-handlers/context/memory-status-handlers.ts index 85c2f83c..019afbf9 100644 --- a/apps/frontend/src/main/ipc-handlers/context/memory-status-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/context/memory-status-handlers.ts @@ -34,8 +34,13 @@ export function loadGraphitiStateFromSpecs( const specDirs = readdirSync(specsDir) .filter((f: string) => { - const specPath = path.join(specsDir, f); - return statSync(specPath).isDirectory(); + try { + const specPath = path.join(specsDir, f); + return statSync(specPath).isDirectory(); + } catch { + // Directory was deleted or inaccessible - skip it + return false; + } }) .sort() .reverse(); diff --git a/apps/frontend/src/main/memory-service.ts b/apps/frontend/src/main/memory-service.ts index 00e88936..cde18fb4 100644 --- a/apps/frontend/src/main/memory-service.ts +++ b/apps/frontend/src/main/memory-service.ts @@ -203,6 +203,9 @@ async function executeQuery( const [pythonExe, baseArgs] = parsePythonCommand(pythonCmd); return new Promise((resolve) => { + // Promise guard flag to prevent double resolution + let resolved = false; + const fullArgs = [...baseArgs, scriptPath, command, ...args]; // Get Python environment (includes PYTHONPATH for bundled/venv packages) @@ -227,7 +230,20 @@ async function executeQuery( stderr += data.toString('utf-8'); }); + // Single timeout mechanism to avoid race condition + const timeoutId = setTimeout(() => { + if (!resolved) { + resolved = true; + proc.kill(); + resolve({ success: false, error: 'Query timed out' }); + } + }, timeout); + proc.on('close', (code) => { + if (resolved) return; + resolved = true; + clearTimeout(timeoutId); + // The Python script outputs JSON to stdout (even for errors) // Always try to parse stdout first to get the actual error message if (stdout) { @@ -254,14 +270,11 @@ async function executeQuery( }); proc.on('error', (err) => { + if (resolved) return; + resolved = true; + clearTimeout(timeoutId); resolve({ success: false, error: err.message }); }); - - // Handle timeout - setTimeout(() => { - proc.kill(); - resolve({ success: false, error: 'Query timed out' }); - }, timeout); }); } @@ -351,6 +364,9 @@ async function executeSemanticQuery( } return new Promise((resolve) => { + // Promise guard flag to prevent double resolution + let resolved = false; + const fullArgs = [...baseArgs, scriptPath, 'semantic-search', ...args]; const proc = spawn(pythonExe, fullArgs, { stdio: ['ignore', 'pipe', 'pipe'], @@ -369,7 +385,20 @@ async function executeSemanticQuery( stderr += data.toString('utf-8'); }); + // Single timeout mechanism to avoid race condition + const timeoutId = setTimeout(() => { + if (!resolved) { + resolved = true; + proc.kill(); + resolve({ success: false, error: 'Semantic search timed out' }); + } + }, timeout); + proc.on('close', (code) => { + if (resolved) return; + resolved = true; + clearTimeout(timeoutId); + // The Python script outputs JSON to stdout (even for errors) if (stdout) { try { @@ -393,13 +422,11 @@ async function executeSemanticQuery( }); proc.on('error', (err) => { + if (resolved) return; + resolved = true; + clearTimeout(timeoutId); resolve({ success: false, error: err.message }); }); - - setTimeout(() => { - proc.kill(); - resolve({ success: false, error: 'Semantic search timed out' }); - }, timeout); }); } diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index e21922b1..76b110aa 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -38,6 +38,7 @@ import { Roadmap } from './components/Roadmap'; import { Context } from './components/Context'; import { Ideation } from './components/Ideation'; import { Insights } from './components/Insights'; +import { ErrorBoundary } from './components/ui/error-boundary'; import { GitHubIssues } from './components/GitHubIssues'; import { GitLabIssues } from './components/GitLabIssues'; import { GitHubPRs } from './components/github-prs'; @@ -895,7 +896,9 @@ export function App() { )} {activeView === 'context' && (activeProjectId || selectedProjectId) && ( - + + + )} {activeView === 'ideation' && (activeProjectId || selectedProjectId) && ( diff --git a/apps/frontend/src/renderer/components/context/MemoryCard.tsx b/apps/frontend/src/renderer/components/context/MemoryCard.tsx index 4a9d0268..46260083 100644 --- a/apps/frontend/src/renderer/components/context/MemoryCard.tsx +++ b/apps/frontend/src/renderer/components/context/MemoryCard.tsx @@ -276,7 +276,9 @@ export function MemoryCard({ memory }: MemoryCardProps) {
{parsed.discoveries.patterns_discovered.map((pattern, idx) => { - const text = typeof pattern === 'string' ? pattern : pattern.pattern; + const text = typeof pattern === 'string' + ? pattern + : (pattern?.pattern || pattern?.applies_to || JSON.stringify(pattern)); return text ? ( {text} @@ -293,7 +295,9 @@ export function MemoryCard({ memory }: MemoryCardProps) {
    {parsed.discoveries.gotchas_discovered.map((gotcha, idx) => { - const text = typeof gotcha === 'string' ? gotcha : gotcha.gotcha; + const text = typeof gotcha === 'string' + ? gotcha + : (gotcha?.gotcha || JSON.stringify(gotcha)); return text ? ( {text} ) : null;