diff --git a/.jules/bolt.md b/.jules/bolt.md index 6763a17..7c2932d 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -70,3 +70,6 @@ ## 2023-10-27 - O(N) Re-renders in List Components **Learning:** React list components mapped from arrays (e.g. `sessions.map(...)`) will experience severe O(N) re-renders when a single element changes state if the evaluation is done inline inside the map without memoization. Passing the active check (e.g., `isActive={activeSessionId === s.id}`) down to a `React.memo` wrapper component is necessary to convert this into an O(1) re-render (only the two affected components will update). **Action:** When creating high-frequency updating list elements with individual active states in React, extract the items into `React.memo` components and pass simple boolean flags to prevent massive VDOM updates. +## 2024-05-24 - [Remove Synchronous File Operations] +**Learning:** Checking for file existence using `fs.existsSync` introduces blocking I/O on the Node.js event loop, creating micro-stutters and reducing application concurrency. +**Action:** Always prefer asynchronous file access (e.g., `fs.promises.readFile` or `fs.promises.access`) enclosed in a `try...catch` block. This approach avoids blocking and eliminates Time-of-Check to Time-of-Use (TOCTOU) race conditions. diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index f8125c7..cc51d94 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -1,7 +1,6 @@ import Anthropic from '@anthropic-ai/sdk'; import type { MessageParam, Tool, ToolUseBlock, TextBlock, ContentBlock } from '@anthropic-ai/sdk/resources/messages'; import fetch from 'node-fetch'; -import { existsSync, readFileSync } from 'fs'; import { readFile } from 'fs/promises'; import { join } from 'path'; import { homedir } from 'os'; @@ -759,12 +758,10 @@ export class EnhancedAgent extends EventEmitter { } else { // Fallback: read .xibecode/memory.md directly (cheap single read) const fallbackMd = join(process.cwd(), '.xibecode', 'memory.md'); - if (existsSync(fallbackMd)) { - try { - const content = await readFile(fallbackMd, 'utf-8'); - this.autoMemoryMarkdownSection = `\n\n## Project Memory\n\n${content.trim()}`; - } catch { /* ignore */ } - } + try { + const content = await readFile(fallbackMd, 'utf-8'); + this.autoMemoryMarkdownSection = `\n\n## Project Memory\n\n${content.trim()}`; + } catch { /* ignore if not exist */ } } if (newMem.trim() && !this.autoMemoryMarkdownSection.includes(newMem.trim())) { diff --git a/packages/core/src/code-graph.ts b/packages/core/src/code-graph.ts index 5d17d2c..0c069c0 100644 --- a/packages/core/src/code-graph.ts +++ b/packages/core/src/code-graph.ts @@ -24,7 +24,15 @@ export class CodeGraph { if (this.isInitialized) return; const tsConfigPath = path.join(this.workingDir, 'tsconfig.json'); - if (fs.existsSync(tsConfigPath)) { + let hasTsConfig = false; + try { + await fs.promises.access(tsConfigPath); + hasTsConfig = true; + } catch { + hasTsConfig = false; + } + + if (hasTsConfig) { this.project = new Project({ tsConfigFilePath: tsConfigPath, skipAddingFilesFromTsConfig: false, diff --git a/packages/core/src/memory.ts b/packages/core/src/memory.ts index 56cbc0d..1a12561 100644 --- a/packages/core/src/memory.ts +++ b/packages/core/src/memory.ts @@ -1,5 +1,4 @@ import * as fs from 'fs/promises'; -import { existsSync } from 'fs'; import * as path from 'path'; import { createHash } from 'crypto'; import { MemoryPromotions } from './memory-promotions.js'; @@ -36,13 +35,15 @@ export class NeuralMemory { try { const xibeDir = path.dirname(this.memoryFile); - if (!existsSync(xibeDir)) { - await fs.mkdir(xibeDir, { recursive: true }); - } + await fs.mkdir(xibeDir, { recursive: true }); - if (existsSync(this.memoryFile)) { + try { const content = await fs.readFile(this.memoryFile, 'utf-8'); this.memory = JSON.parse(content); + } catch (readError: any) { + if (readError.code !== 'ENOENT') { + throw readError; + } } this.initialized = true; } catch (error) { diff --git a/packages/core/src/permission-store.ts b/packages/core/src/permission-store.ts index 951ae85..1c59480 100644 --- a/packages/core/src/permission-store.ts +++ b/packages/core/src/permission-store.ts @@ -1,5 +1,4 @@ import * as fs from 'fs/promises'; -import { existsSync } from 'fs'; import * as path from 'path'; import type { PermissionMode } from './permissions.js'; @@ -18,10 +17,6 @@ export class PermissionStore { } async load(): Promise { - if (!existsSync(this.storePath)) { - return null; - } - try { const raw = await fs.readFile(this.storePath, 'utf8'); const parsed = JSON.parse(raw) as PersistedPermissionState; diff --git a/packages/core/src/utils/auto-memory.ts b/packages/core/src/utils/auto-memory.ts index 1749a45..fc5f41a 100644 --- a/packages/core/src/utils/auto-memory.ts +++ b/packages/core/src/utils/auto-memory.ts @@ -3,8 +3,7 @@ * Keyword relevance scoring; optional env to disable. */ -import { existsSync } from 'fs'; -import { readFile, readdir } from 'fs/promises'; +import { readFile, readdir, access } from 'fs/promises'; import { homedir } from 'os'; import { join } from 'path'; @@ -75,9 +74,6 @@ async function loadMemoryFile( type: LoadedMemory['type'], ): Promise { try { - if (!existsSync(filePath)) { - return null; - } const content = await readFile(filePath, 'utf-8'); return { path: filePath, @@ -117,11 +113,9 @@ async function loadProjectMemories(cwd: string): Promise { } const autoDir = join(cwd, '.xibecode', 'memories'); - if (!existsSync(autoDir)) { - return memories; - } try { + await access(autoDir); const names = await readdir(autoDir, { withFileTypes: true }); const mdFiles = names.filter((d) => d.isFile() && d.name.endsWith('.md')).map((d) => join(autoDir, d.name)); for (const fp of mdFiles.slice(0, MAX_AUTO_LOAD_MEMORIES)) {