Repository navigation
core: session never drains when Snapshot.capture() stalls on huge untracked tree — git add hangs, execution claim never released #48657
Description
Description
Summary
Sending a prompt in a project with a very large untracked (and not gitignored)
file tree leaves the session stuck forever with no response. SessionStep.attempt
runs Snapshot.capture() before llm.stream(), and the capture's
git add --all --pathspec-from-file=- burns 100% CPU in pathspec matching for
10+ minutes (167k untracked files). No model call is ever made, and the
time_suspended execution claim is never released, so the session never drains.
Expected Behavior
The step either completes the snapshot in reasonable time, skips/times out a
pathological capture and still calls the model, or surfaces a snapshot error —
and the execution claim is released so the session stays usable.
Actual Behavior
- The runner claims the session (
session_v2.time_suspendedset 64ms after
creation) and blocks inSnapshot.capture():
packages/core/src/session/runner/step.ts:72runs beforellm.stream()
atstep.ts:102, so the provider is never contacted. Git.index.refresh(packages/core/src/git.ts:381) enumerates all untracked
files vials-files --othersand feeds ~167k paths to a single
git add --all --sparse --pathspec-from-file=- --pathspec-file-nul
in the snapshot repo (~/.local/share/opencode/snapshot/<project>/<hash>/).- That
git add(child of the server process) ran 11+ minutes at ~99% CPU;
sampleshows 100% of time inread_directory_recursive→
treat_path→match_pathspec_with_flags→strncmp— i.e. pathspec
matching cost, not I/O. It also holdsindex.lockin the snapshot repo. time_suspendedis never cleared (time_updatedfrozen), no
Failed to drain Sessionerror is logged, the UI just shows nothing.
Side symptoms: a transientGET /api/session/<id> 404(request raced
session-row insert by ~74ms) and SSE reconnect churn, both harmless.
Additional Context
- Frequency: deterministic given the tree shape; every step start re-captures,
so restarting the server re-triggers it (startup recovery resumes the
claimed session). - Workaround: kill the stuck
git add, add the directory to the project's
.gitignore, then restart the server / resend. - Related (same feature area, different mechanisms, not duplicates):
bug: Snapshot.diffFull serialises full patch text into message rows, causing unbounded DB growth and web UI hang #46138 (Snapshot.diffFull serializes full patch text into message rows),
EventV2 cold history duplicates large snapshot payloads and amplifies SQLite storage/reads #47854 (EventV2 duplicates large snapshot payloads),
OpenCode database grew to ~72 GB in roughly two weeks of normal use #47022 (~72GB DB growth),
Missing Content-Type bypasses model response body timeout and leaves sessions busy indefinitely #47605 (sessions stuck busy via model-response timeout),
beta: cancelled task remains running and blocks resume/amend #47988 (cancelled task blocks resume/amend). - Possible fix directions (suggestion only): cap/skip capture when the
untracked candidate set exceeds a threshold (log + continue with
snapshot: undefined, whichcapture()already supports); add a timeout to
Git.tree.capture; avoid feeding hundreds of thousands of explicit
pathspecs to onegit add(e.g. rely on directory-level adds or
status --porcelaincone/sparse scoping).
Plugins
nothing globally; project-local .opencode/plugins/use-vp.ts present
OpenCode version
local (dev install, bun run src/index.ts serve) checkout of v2.0.2
Steps to reproduce
- Have a git project with a huge untracked, not-ignored directory
(in my caseshadcn_ui_components_2026/: 167,696 files, 685MB;
167,697 untracked vs 502 tracked; the project.gitignoredid not cover it). - Open the web UI, create/select a session in that project, send any prompt.
- Observe: user message is persisted (
session_messagehas the user part),
but no assistant message ever appears and no model request is logged.
Screenshot and/or share link
No response
Operating System
Darwin 24.6.0 arm64
Terminal
web UI in browser (TERM_PROGRAM not set in shell env). Shell: /bin/zsh
Activity
piknockyou commentedon Sep 30, 2026
Your analysis of the snapshot step matches what I hit, so I'm adding the second trigger in case it helps others landing here with the same symptom (no reply and/or the
server restarted while you were workingloop,event stream disconnected / stalled).In my case the huge tree was not a real project: a stray empty
.gitin a parent folder (my home folder) made opencode treat everything underneath it as one giant project. Same result you describe - the per-message scan takes ~26s for me (541k files), the watchdog gives up, the service gets restarted, and it loops.For anyone with this symptom: run
git rev-parse --show-toplevelin the folder where you start opencode..gitis the culprit - delete it (usually empty, nothing lost; rename if unsure) and start opencode inside a real project folder. That was the fix for me.node_modules/,target/,build/, caches) into.gitignore, or set"snapshot": false.cdhcrs commentedon Oct 9, 2026
Field data: 2.37M small files → I/O-bound stall, disk-fill risk, and no client-side recovery
Hitting the same failure at a much larger scale, with several details I don't think are covered above.
Environment
@openviking/opencode-pluginScale
.gitignoreis the stock Python template; the dataset directory was never ignored.1. This instance is I/O-bound, not CPU-bound
Unlike the
git addpathspec matching described above (100% CPU), the failing command here isupdate-index, and it blocks in the kernel:D= uninterruptible disk sleep. It had been running 19 min 43 s when I killed it.2. Disk-fill risk
update-index --addwrites a loose object for every untracked file it ingests. Measured growth of~/.local/share/opencode/snapshot/<project>/<hash>/:At ~11 MB/s it would have tried to copy 808 GB into the snapshot object DB. Root filesystem had 637 GB free → roughly 17 hours to a full disk. The snapshot dir grew 18 GB → 24.5 GB during this session alone.
3. The
>2 MiBcapture cap doesn't protect against many small filesThe snapshots doc says "Individual untracked files larger than 2 MiB → No". Every one of these 2.37M files is ~350 KB, so all are eligible. There is no file-count or total-size guard at all.
4. No backoff on retry
I
kill -9'd the stalled process; OpenCode re-spawned the identical command within 60 seconds:No failure threshold, no backoff, no "the previous attempt ran 20 minutes" check — a deterministic retry loop that keeps re-creating the condition.
5.
POST /interruptreturnsinterrupted: false, so there is no client-side recovery pathWhile wedged:
GET /api/session/active{"type":"running"}POST /api/session/{id}/interrupt{"interrupted": false}— no interruptible fiberPOST /api/session/{id}/revert/stageInterruptErroratSessionRevert.stage)DELETE /api/session/{id}InterruptErroratSession.remove)POST /api/experimental/session/{id}/waitInterruptErroratSession.wait)So the recovery path recommended in #44807 (
POST /interrupt) does not work in this case: the execution claim is held by a live but blocked fiber, not a crashed run. All three escape hatches (interrupt / delete / revert) fail, and the sessions stayrunning.Both stuck sessions left
runningon their own only after I killed the git process and removed the staleindex.lock— one transitioned tosucceeded, the other tofailed.POST /interruptdid not achieve this.6. Contradicts the documented "best effort" contract
From the snapshots doc:
Here the unavailable snapshot doesn't degrade — it blocks every session operation in that project indefinitely.
Reproduction
process/streamnever appear in the log), the session sits inactivewith{"type":"running"}indefinitely, andinterruptreturnsinterrupted: false.Workaround verification
After adding the dataset directory to the project
.gitignore:Suggestion
Beyond the capture timeout / claim release already discussed here: a pre-flight guard that skips capture with a loud warning above N files or M bytes total, and treats a capture exceeding some duration as failed instead of letting it hold
index.lock. Also worth revisiting whether a per-file size cap is the right axis when the pathological case is file count, not file size.cyanyux commentedon Oct 19, 2026
Additional observation on OpenCode 2.0.32, Linux x64, accessed through T3 Code desktop connected to a Linux T3 server.
A prompt asking only for progress spent several minutes in snapshot capture before its shell tool ran. Capture then failed on an unreadable file, and subsequent capture attempts caused further delays.
Selected timestamps from October 19, 2026, UTC:
The failing file was owned by
root:rootwith mode0600. Its path and contents are omitted. The active capture command was:During inspection, the Git process alternated between running and
Dstate withfolio_wait_bit_common; it had read approximately 4.1 GB from storage after about 65 seconds in that capture attempt. These process counters are cumulative for that attempt, not a measurement of the whole turn.At the time of inspection, the shell query had already returned its result, but T3 continued displaying the turn as working without a final response. This observation demonstrates repeated long capture delays; it does not establish a permanent stall or an infinite retry loop.
This adds an unreadable-file failure case on 2.0.32 to the large-tree/update-index observations above. Early detection or exclusion of unreadable paths, bounded capture time, and a visible capture-failure status could help.
No snapshot configuration, file permissions, or provider processes were changed during this investigation. Disabling snapshots has not been tested here.
Vijayapardhu commentedon Oct 21, 2026
Adding Windows evidence for this same root cause — #57841.
Three differences from the report here, in case they change the fix or the triage:
1. Windows, and the legacy shell junctions are the amplifier. The working directory is
$HOME, which is a git work-tree.$HOMEalso contains the legacy Windows junctions (Application Data,Local Settings,Cookies,My Documents,SendTo,Start Menu,Templates,Web,WebComponents), each of which re-exposesAppDataas its own top-level tree. git therefore walks the sameAppDatacontent nine more times, and the untracked set that reaches the indexer is 1,126,526 paths — 6.7x the 167k in this report.AppDataalone accounts for 1,039,918 files.2. The command changed between 2.0.2 and 2.1.2. On 2.1.2 the blocking call is no longer
git add --all --sparse --pathspec-from-file=-:The untracked paths are still enumerated in full and streamed to a single indexer invocation, so the unbounded-pathset problem is unchanged in 2.1.2 — but the fix surface is
update-index --stdinnow, notgit add --pathspec-from-file.3. The stall happens before any session row exists. With
opencode runfrom$HOMEon 2.1.2, no row is written to thesession/session_v2tables at all and the CLI emits zero stdout — so it is not obvious from the outside that the snapshot indexer is the thing blocking. In this report the user message is persisted but no assistant message follows.Quantified on the affected machine:
git ls-files --others --exclude-standard.gitignorecoveringAppData/, the junction dirs, tool state and cachesopencode runbefore the.gitignoregit.exeat ~99% CPU, >207s CPU observed,index.lockheld)opencode runafterThe
.gitignoreworkaround described in this issue is confirmed to work, andsnapshot: falseavoids the path entirely.Env: OpenCode 2.1.2 (channel
latest), Windows 11 build 26200 x64, PowerShell 7.6.7, git for Windows 2.x. No plugins.One suggestion in case it is cheap: on Windows, treat the legacy junction directories as excluded, and/or skip the snapshot index when the working directory is
$HOME. A bound or timeout on the untracked-path enumeration would also let a pathological tree degrade tosnapshot: undefined(whichcapture()already supports) instead of blocking session startup.Vijayapardhu commentedon Oct 21, 2026
Correcting a detail in my comment above, and adding a portable reproduction with a threshold measurement.
Correction. I wrote that the legacy Windows junctions in
$HOME(Application Data,Local Settings,Cookies, …) re-exposeAppDataand inflate the path set ~9x. That is wrong. They are NTFS junctions, and git does not traverse them —git ls-files --others --exclude-standard -- "Application Data"returns 0 paths, same forLocal Settings.AppDataitself is 1,039,918 of the 1,126,526 paths (92%); the junctions add nothing. So please disregard the "9x" framing, and disregard the suggestion to treat them as excluded — that would not change the path count at all.Portable minimal reproduction (no large home directory required). Scratch git repo, 200,000 untracked empty files, OpenCode 2.1.2, Windows 11, stdin closed, same command and same provider for both runs:
opencode runwall clockbulk/presentbulk/to.gitignoreA 52x difference with nothing else changed, and the provider is not contacted until the capture returns. This reproduces outside
$HOMEentirely, so it is independent of the home directory, of the junctions, and of the provider — it is a plain "large untracked tree in a git project" case, and it is a much cheaper repro than a 1.1M-file tree.This is a threshold datapoint for the cap/skip suggestion in this issue: ~200k untracked paths is enough to add ~4 minutes to every session start, and the cost scales roughly linearly with the untracked count. The machine in my original report reaches 1,126,526 paths (84.67s for
ls-filesalone) andupdate-indexnever completes — >207s CPU observed, still running after 5+ minutes,index.lockheld in the snapshot repo — at which pointopencode rundoes not return at all.Other facts confirmed on 2.1.2 / Windows 11, for the record:
git update-index --add --remove --replace -z --stdin(same form as @cyanyux's 2.0.32 comment above), so the fix surface is the same in 2.1.2;index.lockin the snapshot repo;.gitignorecoveringAppData/, tool state and caches, orsnapshot: false.Filed as #57841 (the bot triaged it as a duplicate of this issue — agreed, and it also relates to #50734 and #32981); cross-posting here so the measurements live with the fix.