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 <noreply@anthropic.com>

---------

Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-02-06 22:37:39 +01:00
committed by GitHub
parent 09f059ca3b
commit 2db36982fb
5 changed files with 67 additions and 20 deletions
@@ -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) {
@@ -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();
+38 -11
View File
@@ -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);
});
}
+4 -1
View File
@@ -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() {
<Roadmap projectId={activeProjectId || selectedProjectId!} onGoToTask={handleGoToTask} />
)}
{activeView === 'context' && (activeProjectId || selectedProjectId) && (
<Context projectId={activeProjectId || selectedProjectId!} />
<ErrorBoundary>
<Context projectId={activeProjectId || selectedProjectId!} />
</ErrorBoundary>
)}
{activeView === 'ideation' && (activeProjectId || selectedProjectId) && (
<Ideation projectId={activeProjectId || selectedProjectId!} onGoToTask={handleGoToTask} />
@@ -276,7 +276,9 @@ export function MemoryCard({ memory }: MemoryCardProps) {
<SectionHeader icon={Sparkles} title="Patterns" count={parsed.discoveries.patterns_discovered.length} />
<div className="flex flex-wrap gap-2 pl-4">
{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 ? (
<Badge key={idx} variant="secondary" className="text-xs">
{text}
@@ -293,7 +295,9 @@ export function MemoryCard({ memory }: MemoryCardProps) {
<SectionHeader icon={AlertTriangle} title="Gotchas" count={parsed.discoveries.gotchas_discovered.length} />
<ul className="space-y-0.5">
{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 ? (
<ListItem key={idx} variant="error">{text}</ListItem>
) : null;