diff --git a/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts b/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts index b1430ad610..7e482f5236 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/user-index.ts @@ -570,6 +570,13 @@ export async function start(): Promise { if (!filename || !filename.endsWith(".md")) return reindexDebounced(`${event}:${filename}`) }) + // Same async-error gap as the wiki watcher: an unopenable node under USER_DIR + // (socket, FIFO, dead symlink) emits 'error' asynchronously, which the catch + // below cannot see, and unhandled it takes the process down. The initial scan + // has already built the index, so log and carry on without live updates. + state.watcher.on("error", (err: unknown) => { + console.warn(`[${MODULE_NAME}] Watcher error (continuing without it):`, err) + }) console.log(`[${MODULE_NAME}] Watching ${USER_DIR}`) } catch (err) { console.warn(`[${MODULE_NAME}] Watch failed, polling disabled:`, err) diff --git a/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts b/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts index fbf3ec2bd6..3662f7259a 100644 --- a/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts +++ b/LifeOS/install/LIFEOS/PULSE/modules/wiki.ts @@ -485,7 +485,13 @@ const pendingPaths: Set = new Set() function startWatchers(): void { stopWatchers() - const watchPaths = [LIFEOS_DIR] + // Watch only the roots the reindexer consumes. Watching all of LIFEOS_DIR is + // both too broad and too narrow: too broad because it pulls in MEMORY/, the + // busiest subtree in an install (constant .jsonl appends) and the place where + // non-regular files appear — a unix socket there kills a recursive watcher + // outright; too narrow because skills/ and hooks/ live under ~/.claude, not + // under LIFEOS/, so changes there were never actually watched. + const watchPaths = [DOCUMENTATION_DIR, KNOWLEDGE_DIR, ALGORITHM_DIR] for (const watchPath of watchPaths) { if (!existsSync(watchPath)) continue @@ -499,6 +505,18 @@ function startWatchers(): void { } }) + // A recursive watch walks every node beneath its root, including ones that + // cannot be opened: unix sockets, FIFOs, dead symlinks, unreadable dirs. + // Those surface as an ASYNC 'error' event on the watcher, which the catch + // below cannot see — unhandled, it terminates the process. Under a + // supervisor Pulse then crash-loops for as long as the node exists, and + // because the walk happens during startup it presents as "Pulse won't + // start" rather than "one file is unwatchable". Watching is best-effort by + // design, so degrade to that instead of dying. + watcher.on("error", (err: unknown) => { + console.warn(`[wiki] watcher error on ${watchPath} (continuing without it): ${String(err)}`) + }) + watchers.push(watcher) } catch { // File watching is best-effort; manual refresh still rebuilds on restart.