Repository navigation
Description
Description
Summary
When opencode runs in a large git worktree (here: $HOME, ~1.3M files / 47 GB), the server spawns git status commands (ls-files --others, diff-files, diff --name-status/--numstat/--unified=2147483647, check-ignore) in a tight loop every few seconds. Each scan walks the whole tree, so the spawned git processes spike above 100% CPU (and sit in D state on saturated disk I/O), and the opencode process itself also runs hot, making the TUI feel frozen. The snapshot directory (~/.local/share/opencode/snapshot/, 31 GB here) lives inside the watched worktree, so every snapshot write triggers the file watcher → new git scan → storm (feedback loop).
Environment
- opencode version:
opencode v2.2.6 - OS:
Linux 7.0.0-42-generic #42-Ubuntu SMP PREEMPT_DYNAMIC x86_64 GNU/Linux - Terminal:
TERM=xterm-256color,COLORTERM=yes,TERM_PROGRAMunset - Shell:
/bin/zsh - Install/channel: latest (binary at
~/.opencode/bin/opencode) - Active plugins:
opencode-models-discovery@latest,./koboldcpp-plugin(from~/.config/opencode/opencode.jsonc)
Reproduction
- Use a directory that is a git repo and contains a very large tree as the opencode project — here the user's
$HOME( 47 GB, ~1.25M files). Note:opencode serve --servicechdirs to$HOME, so the managed service always watches it. - Start opencode (TUI or
opencode serve --service) in that directory. - Run
watch -n1 'ps aux --sort=-%cpu | head -15'and inspect~/.local/share/opencode/log/opencode.log. - Observe the server spawning, every few seconds (in bursts, sometimes several per second):
git --git-dir ~/.local/share/opencode/snapshot/<hash>/<id> --work-tree <project> diff-files --name-only -z -- :(literal).git --git-dir ~/.local/share/opencode/snapshot/<hash>/<id> --work-tree <project> ls-files --others --exclude-from=<project>/.git/info/exclude --exclude-standard -z -- :(literal).git ... diff --name-status -z --no-renames <a> <b>,diff --numstat -z ..., anddiff --no-ext-diff --unified=2147483647 ...(full-file diff with unlimited context)git --git-dir <project>/.git --work-tree <project> check-ignore --no-index --stdin -z
- Each
gitprocess spikes >100% CPU and the opencode server process also stays busy; the TUI is unresponsive during scans.
Expected Behavior
- Status/diff polling should be debounced and cheap on large trees (e.g.
git status --untracked-files=no, respect ignore rules, prune data dirs, skip the snapshot directory). - The snapshot store should live outside the watched worktree (or be excluded from the watcher) to avoid a write→watch→scan feedback loop.
- Full diffs (
--unified=2147483647) should not be computed for large files just to refresh status.
Actual Behavior
- Log contains 22,370
ls-filesspawns (2,459 against work-tree/home/$USER, 647diff-files), in bursts every ~5–10 s since days (log file itself is 42 MB). psshows e.g.git --git-dir .../snapshot/... --work-tree /home/lwa ls-files --others ...in stateDat high CPU, whileopencode serve --servicekeeps ~13%+ CPU with 1.1 GB RSS continuously.- The snapshot directory grew to 31 GB inside the worktree, amplifying the loop and disk I/O.
Additional Context
- Worktree:
/home/$USER= 47 GB, ~1.25M files;.gititself is tiny (57 objects), so the cost is purely tree walking + the snapshot repo's huge index. find /home/$USER -type f | wc -l→ 1,246,855;.local/alone holds ~1.32M files.- Workarounds that helped locally: add heavy data dirs to
.gitignore(.local/,.hermes/,snap/, ...), move the snapshot dir out ofsnapshot/, run opencode from a dedicated project directory instead of$HOME, andOPENCODE_DISABLE_FILEWATCHER=1disables the watcher entirely (found in the binary). - Representative log lines:
timestamp=2026-11-09T15:09:20.447Z level=INFO run=101934b4 message="spawning process" command=git args="[\"--git-dir\",\"/home/lwa/.local/share/opencode/snapshot/b9353382cd43ef07a85f329fe8f003d41b481b3b/9e902342ba8d28931c2c86a299d556c66b137bf9\",\"--work-tree\",\"/home/docker\",\"diff-files\",\"--name-only\",\"-z\",\"--\",\":(literal).\"]"
timestamp=2026-11-09T15:09:22.003Z level=INFO ... args="[...,\"--literal-pathspecs\",\"-c\",\"core.quotepath=false\",\"diff\",\"--no-ext-diff\",\"--unified=2147483647\",\"--no-renames\",\"cff625...\",\"69f5d...\",\"--\"]"
- Reproducibility: consistent, as long as a session/server watches a huge worktree; intermittent bursts correlate with file-watcher events (snapshot writes, log writes).
Plugins
No response
OpenCode version
2.2.6
Steps to reproduce
- Use a directory that is a git repo and contains a very large tree as the opencode project — here the user's
$HOME(47 GB, ~1.25M files). Note:opencode serve --servicechdirs to$HOME, so the managed service always watches it. - Start opencode (TUI or
opencode serve --service) in that directory. - Run
watch -n1 'ps aux --sort=-%cpu | head -15'and inspect~/.local/share/opencode/log/opencode.log. - Observe the server spawning, every few seconds (in bursts, sometimes several per second):
git --git-dir ~/.local/share/opencode/snapshot/<hash>/<id> --work-tree <project> diff-files --name-only -z -- :(literal).git --git-dir ~/.local/share/opencode/snapshot/<hash>/<id> --work-tree <project> ls-files --others --exclude-from=<project>/.git/info/exclude --exclude-standard -z -- :(literal).git ... diff --name-status -z --no-renames <a> <b>,diff --numstat -z ..., anddiff --no-ext-diff --unified=2147483647 ...(full-file diff with unlimited context)git --git-dir <project>/.git --work-tree <project> check-ignore --no-index --stdin -z
- Each
gitprocess spikes >100% CPU and the opencode server process also stays busy; the TUI is unresponsive during scans.
Screenshot
No response
Operating System
Linux 7.0.0-42-generic #42-Ubuntu SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
Terminal
Windows Terminal
Activity
opencodejr commentedon Nov 9, 2026
Thanks for the detailed report and the log excerpts. I tried this on
2.2.6(Linux) but couldn't reproduce the polling loop.What I tried:
HOMEthat is a git repo with 150k untracked, un-ignored files, with OpenCode's data folder (~/.local/share/opencode) inside it, and a mock model that streams each reply slowly over about 60 seconds.opencode serveleft idle: nogitprocesses at all in 45 seconds.opencode run: one snapshot scan when the turn started and one when it ended (diff-files+ls-files+update-index+write-tree, then onediff). Nothing ran afterwards while idle.So in my runs each scan came from a turn starting or ending, not from a timer or from file-watcher events. I could confirm the other part of your report: when
$HOMEis the git worktree, the snapshot store records OpenCode's own data folder. That is already tracked in #55010, and the open PR #56209 would exclude that folder.To track down where your repeated scans come from, it would help to know:
opencode-models-discoveryand./koboldcpp-plugin), since a plugin that runs prompts or writes files could start new turns.ls-filesspawns), so the lines that trigger it are visible, not just thegitcommands. Lines that mention a session or step start would be especially useful.Related issues:
$HOMEis a git repo (the 31 GB growth you saw). PR fix(core): keep OpenCode data out of filesystem snapshots #56209 would fix it.Until then, a
.gitignorein$HOMEthat covers.local/and other large data folders, or running OpenCode from a project folder instead of$HOME, avoids the cost of these scans.Lapin-myope commentedon Nov 9, 2026
Thanks for looking into this — here are answers to your three questions, all from
~/.local/share/opencode/log/opencode.log(v2.2.6, log spans 2026-10-29 → 2026-11-09, 124,846 lines) plus a controlled A/B test I ran.1. Do bursts happen with no session running?
Mostly no — an idle server does not poll. Controlled test: I started two fresh
opencode serveinstances (ports 4997/4998) in fresh 3000-file git repos under/tmp/opencode/repro{1,2}— one withOPENCODE_CONFIG={}(no plugins), one with the full user config (plugins enabled) — and left them with no client connected for ~90 s. Result: each spawned git exactly once at startup (workspace init), then silence (ls-files=1each, no further spawns).Caveats from the production log:
opencode serve --service, whichchdirs to$HOME) does an initial scan right at startup: today's firstls-filesspawn was at14:41:20.592Z, ~17 s after the service process started and before the first HTTP event-stream connection (14:41:37.825Z GET /api/event).ls-filesspawns between 02:00–06:00 on 2026-11-06), but they were allcwd=/home/docker/projectand there werecli startingevents at02:44:49and03:01:10— i.e. clients were connecting at those hours. So bursts correlate with client connections / workspace (re)initialization, not with a blind timer.2. Do bursts stop with plugins disabled?
Plugins are a strong trigger/amplifier while the server is active, but not the only one.
provider.updated/model.updatedevents (emitted byopencode-models-discovery@latestpolling the configured providers —koboldcpp-pluginhasrefreshMs: 60000,modelsDiscoveryenabled on the provider-a/provider-b providers) are immediately interleaved with git spawns (see excerpt below: event at28.943Z→ git spawn at28.947Z). Each event batch seems to trigger a full git workspace re-discovery (symbolic-ref HEAD,remote,for-each-ref refs/heads,config init.defaultBranch,rev-parse) plusls-files/diff-filesstatus scans.OPENCODE_CONFIG={}): no difference when idle (1 spawn each) — so plugins alone don't explain the loop. The second trigger is the file watcher:watcher subscribe path=~ type=entries ignores=0— the entire$HOME(47 GB, ~1.25M files) is watched withignores=0(no ignore rules applied), and opencode itself writes constantly inside the watched work-tree:~/.local/share/opencode/opencode.db-wal,snapshot/*/objects/*,shell/*/*.out,.git/objects/*, plus the log itself. Each of those writes is a watcher event → rescan. This feedback loop is independent of plugins and sessions.3. Log excerpt around one burst
Window
2026-11-09T19:49:28.805Z → 19:49:29.186Z(28 lines, 16 plugin events + 6 git spawns interleaved; full excerpt also saved atburst-excerpt-anon.logon the reporter's machine):Adjacent lines in the same burst also contain the heavy scans against the snapshot repo:
git --git-dir ~/.local/share/opencode/snapshot/<hash>/<id> --work-tree <project> ls-files --others --exclude-standard -z,diff-files --name-only -z,diff --name-status/--numstat/--unified=2147483647,check-ignore --stdin -z.Other relevant counts in the log: 22,370
ls-filesspawns total; dense clusters e.g. 277 spawns within ~1,300 log lines;watcher subscribe path=~ type=entries ignores=0(whole home tree, no ignores); 276failed to capture snapshotand 196failed to diff session turn(snapshot capture keeps failing/retrying against the huge tree).Summary of the mechanism: client connect or plugin event → workspace (re)discovery + status scan via git; on a 1.25M-file work-tree each scan takes seconds of >100% CPU and saturated disk; opencode's own data dir (
~/.local/share/opencode: db, snapshots, shell output, logs) lives inside the watched tree withignores=0, so its own writes re-trigger scans → self-sustaining storm while any client is connected.burst-excerpt-anon.log
opencodejr commentedon Nov 9, 2026
Thanks a lot for the detailed follow-up and the A/B test, it narrowed things down. I tried both triggers you pointed to on
2.2.6, but couldn't get the scans to repeat:$HOMEwas a git repo with 150k untracked files and OpenCode's data folder inside it, with a client connected. The watcher did subscribe to$HOMEwithignores=0, as in your log. But writing files into$HOMEand into~/.local/share/opencodefor 20 seconds started nogitprocesses at all.provider.updated/model.updatedevents (14–17 of each), both with only an event stream and with the TUI open on a session. Neither started anygitprocesses: no rediscovery, and nols-files/diff-files.In your excerpt, the rediscovery lines (
symbolic-ref,remote,for-each-ref,rev-parse, watcher on~/.git/HEAD) look like the location being opened, which in my runs happened once, when a client first connected. So what keeps re-triggering it, and the heavy snapshot scans, on your side is still unclear. Two things would help:burst-excerpt-anon.log, the nearest lines before the firstls-files/diff-filesof a burst that mention a session, step, prompt orlocation, e.g.session,step,prompt,instanceorlocationmessages, or an HTTP request line. That would show whether each burst comes from a turn (perhaps one started by a plugin or another client), from a session diff request, or from the location being opened again.failed to capture snapshotandfailed to diff session turnlines with theircause=text. If captures keep failing on the huge tree, something may be retrying them, and that would be a concrete lead.Related: #55010 tracks the snapshot store recording its own folder when
$HOMEis a git repo (PR #56209 would exclude it), and #60370 covers how slow one scan is on a huge home folder (PR #59423 would make it cheaper).