Repository navigation
Description
Description
Summary
The background service (opencode serve --service) continuously tears down and rebuilds the inotify watch for a compatibility skills directory (<project>/.agents/skills), about 1–2 times per second. It never settles while otherwise idle, pinning the service at ~35–40% CPU and flooding the log.
Environment
- opencode version: v2.0.26 (channel
latest) - OS: Linux 6.18.40.1-microsoft-standard-WSL2 (x86_64) — Docker Desktop
- Terminal:
TERM_PROGRAM=vscode,TERM=xterm-256color,COLORTERM=truecolor - Shell: fish
- Install/channel: latest
- Active plugins: one local plugin (
worktree-plugin.jsunder~/.config/opencode/plugins/). The looping code is in the built-inopencode.config.*plugins, not this plugin.
Reproduction
- Use a project that exposes compatibility skills via
./.agents/skills/<name>/SKILL.md(the project also has a./.opencode/directory). - Start the background service:
opencode service start(or launch the app / TUI). - Watch the service log:
tail -f ~/.config/opencode/log/opencode.log. - Observe
watcher stopped→watcher subscribe→watcher startedrepeating forpath=<project>/.agents/skills, ~1–2×/s, indefinitely — even though no file in that directory is changing. - Count
watcher subscribeper path in the log:.agents/skillsaccumulates thousands of occurrences; every other path subscribes once or twice and stays stable.
Expected Behavior
The skills/compatibility watcher subscribes to the directory once and stays idle when nothing changes — the same way ~/.config/opencode/skills and <project>/.opencode/skills behave.
Actual Behavior
The watch is cleared and rebuilt continuously. In opencode.config.compatibility, refresh() calls iQ(l) (closes all watch resources) and then re-subscribes join(dir, "skills") for each compatibility directory. The watch rebuild appears to emit events that are fed back through the queue → 100 ms debounce → refresh() again, forming a self-sustaining loop. opencode.config.skill and Config.reconcileWatches (which publishes config.updated) participate in the same feedback.
Evidence — watcher subscribe occurrences per path over one service run:
<N> path=<project>/.agents/skills <- loops, grows without bound
2 path=~/.config/opencode/skills
2 path=<project>/.opencode/skills
2 path=<project>/.opencode
2 path=<project>/AGENTS.md
... (every other path: 1–2 occurrences)
- No file under
.agents/was modified during the observation window (find . -newermt '-2 minutes'returned nothing); no external process held the directory open. So real filesystem changes are not the trigger. - The only log lines produced in the window were
watcher subscribe/watcher started/watcher stopped(noskills rescannedorconfig.updated), i.e. the loop is at the low-level watcher layer. - Measured steady state while otherwise idle: ~37 watcher events per 20 s, service CPU ~37%.
Additional Context
- Impact:
opencode serve --servicesits at ~35–40% CPU while otherwise idle, andopencode.loggrows to tens of MB/day from watcher spam alone. - Workaround that works — disable the file watcher via service env:
After restart:
opencode service set env OPENCODE_FILEWATCHER_DISABLE 1 opencode service start.agents/skillswatcher events drop to 0 per 20 s and service CPU falls to ~14%. Downside: config/skills/AGENTS.md hot-reload is disabled. opencode debug configdoes not list.agentsas a config directory; the subscription comes from the compatibility plugin ([...claude, ...agents].map(z => join(z, "skills"))).- Minor related issue:
opencode service set env <KEY> <VALUE>stops the service before writing config and does not restart it, so it must be run outside the agent/session.
Activity
opencode-agent commentedon Oct 9, 2026
Thanks for the detailed report. I tried to reproduce this on 2.0.26 on native Linux and couldn't trigger the loop.
What I tried:
.agents/skills/<name>/SKILL.md, a.opencode/directory and AGENTS.md, plus a local plugin in~/.config/opencode/plugins/.opencode serve --service, with the project loaded through the API, then left idle for a few minutes.<project>/.agents/skillssubscribed once and stayed idle, and the service sat at about 3–4% CPU. Writing to the skill file,.opencode/or~/.config/opencode/caused a few re-subscribes, which then settled.Your environment (Docker Desktop on WSL2) is the main thing I couldn't match. These details would help narrow it down:
/mnt/c/..., or a Docker volume from the Windows side)? Does the loop also happen if you copy the project to a native Linux path such as~/proj?worktree-plugin.jswrite any files, such as state, logs or worktree metadata? If so, where? Writes anywhere under~/.config/opencode/trigger a compatibility reload (see core: any file write inside the config directory triggers an un-debounced compatibility reload — ~/.claude/skills watcher resubscribe loop (v2.0.23) #53393). Does the loop stop if you temporarily move the plugin out ofplugins/?opencode.logcovering a few loop cycles, from a run started with--log-level debugif you can. Please include the lines just before eachwatcher stopped.Related reports, most likely first:
s524797336 commentedon Oct 9, 2026
Thanks — I think this lines up with #53393, and I found the differentiator.
Root cause in my environment: the config directory and the data directory are the same directory.
opencode debug pathsreports:config ~/.config/opencode
data ~/.local/share/opencode
db ~/.local/share/opencode/opencode.db
log ~/.local/share/opencode/log
But here both paths resolve to the same directory (identical inodes) — this
devcontainer bind-mounts the same host folder at both locations. As a result,
opencode's own data files — opencode.db, opencode.db-wal, opencode.db-shm and
log/opencode.log — all live inside the config directory. So every DB/WAL/log
write is a write under the config dir, which per #53393 triggers the
un-debounced compatibility reload; that calls iQ() to clear and re-subscribe the
skill watchers (opencode.config.compatibility.refresh()), and the subscribe/log
activity produces more writes → self-sustaining loop. On a normal install these
two dirs are separate, which likely explains why you can't reproduce on native
Linux.
While idle, the only files touched under the config dir are opencode.db,
opencode.db-wal, opencode.db-shm and log/opencode.log; /.agents/skills
itself is never modified.
Suggested repro on native Linux: force the data dir to equal the config dir —
e.g. run
XDG_DATA_HOME=$HOME/.config opencode serve --service(or symlink~/.local/share/opencode → ~/.config/opencode) — with a project that has
.agents/skills//SKILL.md. I'd expect the loop to reproduce with no bind
mount involved.
Answers to your questions:
/mnt/c path). The difference vs your setup is the merged config/data dir
above, not the project mount.
(~/.config/opencode/worktrees/repos//{worktree-sessions.json,.repo-id}
and worktrees/.migrated-v4), but only during setup and worktree
create/delete — not while idle. The idle writers are the DB WAL and the log,
so the plugin is probably not the trigger. Can confirm by moving it out.
opencode-agent commentedon Oct 9, 2026
Thanks for finding the trigger. Making the data directory the same as the config directory was enough to reproduce this on native Linux, no bind mount needed.
Steps:
.agents/skills/demo/SKILL.mdand a.opencode/directory.XDG_DATA_HOMEset to the same directory asXDG_CONFIG_HOME, soopencode.dband the log end up in~/.config/opencode.opencode serve, loaded the project through the API, then left it idle.Result:
<project>/.agents/skillswas torn down and re-subscribed about once per second (about 30 times in 30 s) with no file changes. Every other path subscribed once. With separate data and config directories it subscribes once and stays put. It still happens on the latest v2 (2f3ba04, run from source). The script below reproduces it.This looks like the same un-debounced compatibility reload as #53393, with OpenCode's own database and log writes in the config directory as the trigger. Open PR #53764 for that issue may also fix this case. Until there is a fix, the workarounds are to keep the data directory separate from the config directory (e.g. point
XDG_DATA_HOMEsomewhere outside~/.config), or theOPENCODE_FILEWATCHER_DISABLE=1setting you already use.Reproduction script (repro.sh)
Run it:
bash repro.shargszero commentedon Oct 11, 2026
I'd like to work on this.
The cause is visible in
packages/core/src/config/plugin/compatibility.ts:refresh()starts withFiberMap.clear(watches)and then re-subscribes every directory. That blanket clear is what defeats theonlyIfMissing: trueon theFiberMap.runinsidewatch()— so every event that reaches the 100 ms debounce tears down and rebuilds all compatibility watches, and each rebuild publishes again. Nothing has to change on disk for that to keep going.The sibling paths in this codebase already reconcile instead of clearing —
Config.reconcileWatchesinpackages/core/src/config.tsandConfigSkillPlugin.watchDirectoryboth compute the desired key set,FiberMap.removethe keys that are gone, and letonlyIfMissingkeep the ones that remain. I'll bring the compatibility watcher in line with that, so an unchanged directory is never re-subscribed and the loop has no event to feed on.That also makes the failure testable with the existing
Watcher.Testlayer, which records everysubscribecall: emit one update, let the debounce settle, and assert the compatibility directory was not subscribed a second time.Opening a PR against
dev.argszero commentedon Oct 11, 2026
Opened a PR implementing this: #54450 — reconciles the compatibility watches instead of clearing and rebuilding them on every event.
larspage commentedon Oct 26, 2026
Reproducing on Linux (Fedora), opencode v2.1.3 and v2.1.4, with the same symptom and a measurable per-path leak.
Setup. Two skill roots, both real and non-empty, different inodes:
OpenCode watches both conventions by design, so each agent session subscribing to both is expected. The problem is that the subscriptions are never released.
What I measure. The log records two relevant messages, and only two:
There is no
watcher unsubscribemessage in this build. If you grep for one you get 0 and conclude nothing is ever released. The release event iswatcher stopped.Net registry per server instance, over a single ~4-day server lifetime:
Releases do happen, roughly 1 per 11 subscribes. The net is what grows without bound.
Per path, same instance:
~/.claude/skills~/.agents/skills~/.claude/skills/unslop/SKILL.md~/.config/opencode/skillThe two skill roots account for 83,407 of the 89,756 held subscriptions.
Rate. A fresh server accumulates roughly 1,240 net subscriptions in its first 5 minutes, about 13,600/hour, so the registry reaches the 4-day level again in under 7 hours.
The registry is per-process. Restarting the
opencode serve --serviceprocess resets the count to zero and it immediately starts climbing again. Restarting the TUI client does nothing, which is what made this look like it was not release-related.Two details that may help:
~/.claude/skillsand~/.agents/skillscounts are near-identical (45,394 vs 45,389), which looks like each session subscribes to both roots.SKILL.mdfiles also accumulate, at 273 subscribes against 1 stop each.Also note the version situation: v2.1.4 does not fix this. I upgraded to it specifically to test that, and the 2.1.3 to 2.1.4 diff (72 commits, 300 files) touches no watcher or file-watching source. The post-upgrade rate is the same.