Fix: Pulse crash-loops when any unwatchable node exists under LIFEOS/ - #1683
Open
lmbagley wants to merge 1 commit into
Open
Fix: Pulse crash-loops when any unwatchable node exists under LIFEOS/#1683lmbagley wants to merge 1 commit into
lmbagley wants to merge 1 commit into
Conversation
`startWatchers()` in the wiki module watches `LIFEOS_DIR` recursively. A recursive
watch walks every node beneath its root, and some cannot be opened — unix sockets,
FIFOs, dead symlinks, unreadable directories. Those surface as an asynchronous
'error' event on the watcher. The surrounding `try/catch` only sees synchronous
throws, so the event goes unhandled and terminates the process.
Under a supervisor Pulse then crash-loops for as long as the node exists. Because
the walk happens during startup, it presents as "Pulse won't start" rather than
"one file is unwatchable", and the only clue is an ENXIO naming a file that has
nothing to do with the wiki:
error: ENXIO: no such device or address, open '.../MEMORY/<tool>/<name>.sock'
at emitError (node:events:51:13)
at #onEvent (node:fs:47:16)
It is easy to miss because it is latent: a long-running Pulse is unaffected when
the socket appears, since the walk already happened. It only bites on the next
restart, which can be weeks later and looks unrelated to whatever created the file.
Any tool that puts a socket under LIFEOS/MEMORY/ — a bot, an IPC helper, an editor
server — is enough to trigger it.
Two changes:
1. Narrow the watch roots to what the reindexer actually consumes
(DOCUMENTATION, KNOWLEDGE, ALGORITHM). Watching all of LIFEOS_DIR was both too
broad and too narrow — too broad because MEMORY/ is the busiest subtree in an
install and the place where non-regular files appear, too narrow because
skills/ and hooks/ live under ~/.claude rather than LIFEOS/, so changes there
were never watched anyway. This removes the cause and cuts a large amount of
pointless wakeup churn from .jsonl appends.
2. Attach an 'error' handler to the watcher so any future unopenable node degrades
to "no live reindex" instead of taking Pulse down. The comment in the existing
catch already says watching is best-effort; this makes that true.
The same async-error gap exists in `user-index.ts`, which watches USER_DIR with an
identical `try/catch`, so it gets the same handler. Its initial scan has already
built the index, so losing live updates is a soft degradation.
Verified on Linux (bun 1.3.13): before, Pulse crash-looped with a socket present
and `/healthz` never came up; after, it starts clean, logs the watcher warning once
when applicable, and serves normally. No new typecheck errors in either file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
startWatchers()in the wiki module watchesLIFEOS_DIRrecursively. A recursive watch walks every node beneath its root, and some cannot be opened — unix sockets, FIFOs, dead symlinks, unreadable directories. Those surface as an asynchronous'error'event on the watcher. The surroundingtry/catchonly sees synchronous throws, so the event goes unhandled and terminates the process.Under a supervisor, Pulse then crash-loops for as long as the node exists. Because the walk happens during startup, it presents as "Pulse won't start" rather than "one file is unwatchable", and the only clue is an ENXIO naming a file with nothing to do with the wiki:
It is easy to miss because it is latent. A long-running Pulse is unaffected when the socket appears — the walk already happened. It only bites on the next restart, which can be weeks later and looks unrelated to whatever created the file. Any tool that puts a socket under
LIFEOS/MEMORY/(a bot, an IPC helper, an editor server) is enough to trigger it.Changes
1. Narrow the watch roots to what the reindexer actually consumes —
DOCUMENTATION_DIR,KNOWLEDGE_DIR,ALGORITHM_DIR.Watching all of
LIFEOS_DIRwas simultaneously too broad and too narrow:MEMORY/is the busiest subtree in an install (constant.jsonlappends) and the place where non-regular files appear.skills/andhooks/live under~/.claude, not underLIFEOS/, so changes there were never watched anyway.This removes the cause and cuts a lot of pointless wakeup churn.
2. Attach an
'error'handler so any future unopenable node degrades to "no live reindex" instead of taking Pulse down. The comment in the existingcatchalready says watching is best-effort; this makes that true.The same async-error gap exists in
user-index.ts, which watchesUSER_DIRwith an identicaltry/catch, so it gets the same handler. Its initial scan has already built the index, so losing live updates is a soft degradation.Verification
Linux, bun 1.3.13:
MEMORY/, Pulse crash-looped and/healthznever came up.Notes
Both changes are independent — the narrowing prevents the crash, the handler survives it. If you would rather take only one, the handler alone is the minimal fix, though the broad watch root is the underlying cause.