diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index ef530ff0..2ada48a9 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -583,11 +583,12 @@ function handleWorkspaceChange(event: z.infer) => void; +// The only WorkDir paths whose change events have a consumer (knowledge index +// invalidation in main, and the tree/editor, live-notes, email, sidebar and +// meetings views in the renderer). Everything else under WorkDir — runs-archive, +// storage/turns, engines, logs, code-mode, ... — is internal state that grows +// unboundedly with usage; chokidar v4 holds one OS watch handle per file, so +// watching all of WorkDir exhausts the process fd limit (EMFILE crash) once +// the workdir gets big enough. +const WATCHED_ROOTS = [ + 'knowledge', + 'bases', + 'inbox_lists', + 'gmail_sync', + 'calendar_sync', + 'bg-tasks', + 'config/agent-schedule.json', +]; + /** * Create a workspace watcher - * Watches the configured workspace root recursively and emits change events via callback - * + * Watches the user-facing workspace roots (WATCHED_ROOTS) recursively and + * emits WorkDir-relative change events via callback. + * * Returns a watcher instance that can be closed. * The watcher emits events immediately without debouncing. * Debouncing and lifecycle management should be handled by the caller. @@ -22,15 +40,13 @@ export async function createWorkspaceWatcher( ): Promise { await ensureWorkspaceRoot(); - // Code-section session worktrees are full repo checkouts (thousands of files, - // possibly node_modules) living under WorkDir — watching them would flood the - // event stream and burn file handles, and nothing in the app renders them - // from workspace events. - const codeModeDir = path.join(WorkDir, 'code-mode'); - const watcher = chokidar.watch(WorkDir, { + const roots = WATCHED_ROOTS.map((rel) => path.join(WorkDir, rel)); + const watcher = chokidar.watch(roots, { ignoreInitial: true, + // knowledge/ is a git repo (version history) — its .git object store is + // thousands of files nothing renders, so keep it out of the watch set. ignored: (watchedPath: string) => - watchedPath === codeModeDir || watchedPath.startsWith(codeModeDir + path.sep), + path.relative(WorkDir, watchedPath).split(path.sep).includes('.git'), awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50,