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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user