Repository navigation
server: file watcher restarts in a loop on unmodified skill directories, dropping SSE and respawning the CLI #53416
Description
Summary
The file watcher enters a restart loop on skill directories that are not being modified, tearing down and re-subscribing thousands of times per day. The resulting load appears to drop SSE event-stream connections, which makes the desktop app conclude the background service is dead and respawn the ~206 MB CLI process. Frequent respawns then collide on session_message sequence allocation, corrupting conversation ordering.
Environment
- opencode version: 2.0.23 (bundled CLI
opencode v2.0.23, desktop2.0.23.0) - OS: Windows 11,
10.0.26200build 26200, x64 - Terminal: Desktop app (
OPENCODE_CLIENT=desktop); no TTY terminal variables - Shell:
C:\WINDOWS\system32\cmd.exe(spawned tools run underpwsh) - Install/channel:
latest(release channel, packaged desktop build) - Active plugins: none configured
Reproduction
- Launch the OpenCode desktop app on Windows with at least two skill directories registered, one of which is idle.
- Let the service run for an hour or so.
- Inspect
opencode.logand countwatcher startedevents.
Expected Behavior
A watcher on an unmodified directory should stay started. Watcher restarts should track actual filesystem change volume.
Actual Behavior
An idle directory is repeatedly torn down and re-subscribed.
Measured on one log file:
watcher startedevents total: 3039- Directory A (a skills directory on a secondary drive): 3073 restart events
- Directory B (
<USERPROFILE>\.claude\skills): 2574 restart events
Directory B is the decisive data point: its most recent file modification was 2026-08-22, roughly six weeks before the measurement window, yet its watcher was rebuilt 2574 times. Zero file activity, thousands of rebuilds, so the loop is not driven by filesystem changes. Every event is logged as backend=windows.
The churn is continuous, not a startup burst — 121 watcher events were present in the final 400 lines of the log.
Downstream observable effects:
- Renderer log:
[server-request-queue] server thrashing detectedappears 17 times - CLI process grew to 407 MB working set / 660 MB private memory / 328.5 s CPU
- System memory was not the constraint (16 GB total, 3.1 GB free), so this is not OOM
- Both the thrashing warnings and the load clear when the service is freshly restarted
Additional Context
Observed consequence chain
Service respawns are always preceded by an SSE disconnect, with a stable 15–20 s gap (one health-check timeout):
renderer.log 13:19:59.954 [global-sdk] event stream disconnected
main.log 13:20:06.088 v2 CLI background service starting { reason: 'missing' }
renderer.log 23:34:37.879 [global-sdk] event stream disconnected
main.log 23:34:45.497 v2 CLI background service ready <- found alive, no respawn
renderer.log 01:01:00.844 [global-sdk] event stream disconnected
main.log 01:01:20.423 v2 CLI background service starting { reason: 'missing' }
Note the 23:34 case: the stream also dropped, but the service was judged alive and was not respawned. This suggests the respawn follows a failed liveness probe rather than the disconnect alone.
One respawn took 50 seconds to reach ready.
Impact: session data corruption
About three minutes after one respawn completed, the log shows:
ERROR run=... message="Failed to drain Session"
cause=EffectDrizzleQueryError: ... UNIQUE constraint failed: session_message.session_id, session_message.seq
ERROR run=... message="Failed to publish Session execution lifecycle" (same transaction)
14 such pairs appear across two service runs. When the insert loses the sequence-number race, subsequent rows land with seq values that do not agree with their time_created. The runtime reads conversation context ordered by seq, so older messages are presented as the most recent ones. Observed effect: the model answers a question never asked in the current turn, referencing material from roughly 18 hours earlier.
Reported here because the respawn frequency appears to be the driver. The sequence-allocation race itself may warrant its own issue.
The real gate is undocumented, and the desktop app sets a variable that does nothing
The CLI gates the watcher like this (verbatim from the shipped binary):
fs: {
filewatcher: !y(process.env.OPENCODE_FILEWATCHER_DISABLE ?? process.env.OPENCODE_DISABLE_FILEWATCHER),
...
}Neither name appears anywhere in the published configuration schema, and opencode --help does not list them.
Separately, the desktop app forcibly injects into the service environment:
Object.assign(process.env, {
...t,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: `true`,
OPENCODE_EXPERIMENTAL_FILEWATCHER: `true`,
OPENCODE_CLIENT: `desktop`
})But OPENCODE_EXPERIMENTAL_FILEWATCHER does not appear anywhere in the 2.0.23 CLI binary — zero occurrences, verified with a string search whose detector was confirmed working against other known strings in the same file. The CLI reads OPENCODE_DISABLE_FILEWATCHER, not OPENCODE_EXPERIMENTAL_FILEWATCHER. The desktop app therefore sets a dead variable, which makes the watcher look experimental and configurable when neither is true.
Note also that the watcher is on by default: with neither disable variable set, filewatcher evaluates to true. There is no experimental code path to speak of.
Net effect for users: the only working control is an undocumented environment variable whose name suggests it disables something else, while a similarly-named variable that the desktop app does set is silently ignored.
Ruled out
watcher.ignoreinopencode.jsondoes not apply to these paths;ignores=0is logged on every subscribe for both directories. The key exists in the schema but has no effect on globally-registered skill directories.- Not disk-related
- Not a crash (Application event log clean, crashpad reported nothing)
- Not memory pressure
Workaround
Setting OPENCODE_DISABLE_FILEWATCHER in the user environment and restarting the desktop app stops the watcher churn. This is the only mitigation found; note it disables the watcher outright rather than excluding specific directories, so watcher.ignore would be the better long-term shape once it works for skill paths.
Confidence
Not proven that the watcher loop causes the SSE disconnects. Timing is consistent (churn is heaviest right before disconnects) and the six-week-idle-directory evidence proves the loop exists independent of file activity, but no direct causal measurement was taken. CPU saturation and watcher churn are both candidates.
Activity
github-actions commentedon Oct 5, 2026
This issue might be a duplicate of existing issues. Please check:
~/.claude/skillswatcher on v2.0.23, triggered by writes elsewhere in the config directory.~/.claude/skillsthat coincides with managed-service termination during bulk skill writes.UNIQUE constraint failed: session_message.session_id, session_message.seqcollision, attributed to concurrent server processes.OPENCODE_EXPERIMENTAL_FILEWATCHER, which the CLI does not read.