mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-21 21:31:12 +02:00
fix(workspace): whitelist watched roots to stop EMFILE crash
The workspace watcher watched all of ~/.rowboat recursively. Chokidar v4 (no fsevents) holds one OS watch handle per file, and internal state dirs (runs-archive, storage/turns, engines, ...) grow unboundedly with usage — ~20k open fds on a 1.1G workdir, crashing the app with EMFILE once the fd limit is hit. Only watch the roots whose change events actually have consumers: knowledge, bases, inbox_lists, gmail_sync, calendar_sync, bg-tasks and config/agent-schedule.json — with .git ignored (knowledge/ is a git repo).
This commit is contained in:
parent
4182794b76
commit
4db72e1fe4
2 changed files with 29 additions and 12 deletions
|
|
@ -583,11 +583,12 @@ function handleWorkspaceChange(event: z.infer<typeof workspaceShared.WorkspaceCh
|
|||
|
||||
/**
|
||||
* Start workspace watcher
|
||||
* Watches the configured workspace root recursively and emits change events to renderer
|
||||
* Watches the user-facing workspace roots recursively and emits change events to renderer
|
||||
*
|
||||
* This should be called once when the app starts (from main.ts).
|
||||
* The watcher runs as a main-process service and catches ALL filesystem changes
|
||||
* (both from IPC handlers and external changes like terminal/git).
|
||||
* The watcher runs as a main-process service and catches all filesystem changes
|
||||
* under the watched roots (both from IPC handlers and external changes like
|
||||
* terminal/git).
|
||||
*
|
||||
* Safe to call multiple times - guards against duplicate watchers.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,10 +9,28 @@ import { Stats } from 'node:fs';
|
|||
|
||||
export type WorkspaceChangeCallback = (event: z.infer<typeof WorkspaceChangeEvent>) => 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<FSWatcher> {
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue