Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
This has the same lock mechanism as #11306, but a different trigger and setup. #11306 is a macOS home-folder project and a search or destroy call. This report is a normal project root (a large monorepo, .gitignore honored) on Windows, where the trigger is the forced rescan after every agent turn.
Area
apps/server
Steps to reproduce
- On Windows, open a large repository as a project. Mine is a Unity game monorepo with about 272k tracked files. About 21 GB of those files are 10 MiB or smaller, and many are large YAML text assets (
.prefab,.unity,.asset). - Use anything that creates the workspace index for that project: the composer @-mention picker, a file link in chat, or the Files panel.
- Run several agent threads in that project at the same time (I run about 12). The agents edit files.
- Wait for runs to finish. Each finished run calls
WorkspaceSearchIndex.refresh.
Expected behavior
A finished agent run does not stop the backend event loop. The file watcher already keeps the index current, so a run does not need a full rescan.
Actual behavior
The backend stops answering for 15 to 52 s, many times per hour. The desktop shows SocketReadError: An error occurred during Read, then "did not answer" or "reconnecting". The client ping timeout is 15 s (packages/client-runtime/src/rpc/protocol.ts:12), so every block longer than about 15 s drops all clients.
These are the freezes in server.trace.ndjson on 2026-10-21 (UTC). In each one, no span starts or ends anywhere in the process:
| Freeze start | Duration |
|---|---|
| 00:15:35 | 51.1 s |
| 00:17:02 | 22.8 s |
| 00:18:04 | 15.0 s |
| 00:20:46 | 42.7 s |
| 00:25:26 | 23.8 s |
| 00:39:19 | 32.8 s |
| 00:44:25 | 36.7 s |
Where it blocks
- In every gap,
WorkspaceSearchIndex.refreshis the last span that starts. - Its child
WorkspaceSearchIndex.waitForIndexReadyis the first span after the gap. That child takes 0 to 1.6 s. - The only code between the two spans is
finder.scanFiles()(apps/server/src/workspace/WorkspaceSearchIndex.ts:417). - fff-node 0.11.0 calls
fff_scan_filesthrough ffi-rs withoutrunInNewThread, so the call runs on the JS thread. fff_scan_filesreturns at once. Before that,ScanJob::new_rescantakesshared_picker.read()(scan.rs:69). The block is the JS thread waiting on thatparking_lotlock, as in the samples in [Bug]: Backend event loop blocks for minutes inside synchronous fff mixedSearch waiting on watcher lock (home-directory project) — shows as "<label> is reconnecting" #11306.- Not every refresh blocks. 15 of 20 calls in one window returned in under 1 ms. The long ones occur when the lock is contended.
Why a large repository on Windows makes this worse
- The trigger:
RunFinalizationService.ts:101-106callsworkspaceEntries.refresh(cwd)after every run. If an index exists for that cwd,WorkspaceEntries.ts:161-198callsWorkspaceSearchIndex.refresh, which callsscanFiles(). With about 12 threads, a refresh arrives every 30 to 60 s. - The sniff: each rescan ends in
run_post_scan, which callssniff_binary_for_non_indexablefor the paths index. That runs serially on one thread and reads every file of 10 MiB or less to EOF in 16 KiB chunks (bigram_filter.rs:1161-1187,types.rs:572-600). On this repository that is about 21 GB per pass. - The status: a full libgit2 status of the whole repository then runs on the
fff-git-statusthread. - No idle time: a rescan requested during a pass runs immediately after it (
rescan_pending). The scanner is almost never idle. - Measured reads: the backend process read 5.9 TB in about 8 hours, at 0.6 to 1.5 GB/s. The average read size is 15.9 KiB, which matches the 16 KiB sniff chunk. This rate matches the
\PhysicalDiskread counter, so these are cold reads from the disk, not cache hits. - Measured CPU per thread:
fff-bg-04,365 s,fff-git-status8,256 s (one full core),fff-watcher-own1,319 s. The thread names come fromGetThreadDescription. - Kernel time: one stall record shows 50.5 s of system CPU and 4.0 s of user CPU. This agrees with kernel file I/O on the fff threads (NTFS and Defender real-time scanning) while the JS thread waits. That link is an inference.
- The rescan is redundant: the watcher is already on, because T3 does not pass
disableWatch. On Windows the watcher is one recursive watch on the base folder.
The fff fix does not remove this
fff PR 899 (merged 2026-10-11, in 0.11.1-nightly builds only) stops the read at the first NUL byte and caps the read at MAX_FFFILE_SIZE. Binary files contain NUL bytes, but text files do not. On a repository with many large text files, each pass still reads those files to EOF. The T3-side trigger is still the main cost.
Possible fixes, smallest first
- Do not force
scanFiles()on run finalization while the watcher is active. At minimum, make it single-flight with a trailing debounce for each cwd. - Keep fff calls off the event loop. Use ffi-rs
runInNewThreador a worker, so a lock wait cannot stop the backend. This also covers thefff_destroypath in [Bug]: Backend event loop blocks for minutes inside synchronous fff mixedSearch waiting on watcher lock (home-directory project) — shows as "<label> is reconnecting" #11306. - When a stable fff release has PR 899, update to it.
- Optional: respect the repository's
core.fsmonitor. T3's status calls pass-c core.fsmonitor=false, so on a large Windows repository every status check visits all tracked files.
Related
- Opening a long thread blocks the database writer for 7–10 s #21840 and fix(server): run windowed snapshots on the SQLite reader #21869: after each drop, the reconnect snapshot reads run on the main-thread writer, so one freeze can cause more.
- The stall monitor (
EventLoopMonitor.ts:118) recorded only 2 of the 7 freezes above (00:17:02 and 00:18:04), because it drops a sample whenactiveMs < delayMs. - On Windows, libuv puts all page faults in
ru_majflt, so themajorPageFaultsfield in stall records does not show hard paging.
Impact
Major degradation or frequent failure
Version or commit
0.1.1-nightly.20261020.3411 (tag commit d36b7da248). Still present on main @ 8cd561ef6f, which pins @ff-labs/fff-node 0.11.0.
Environment
Windows 11 Pro (10.0.26300), x64, NTFS, Microsoft Defender real-time scanning on. T3 Code Nightly desktop app. The backend runs as the Electron-as-Node child process. fff-node 0.11.0 (fff-bin-win32-x64). Claude and Codex agent threads, about 12 running at the same time.
Activity
juliusmarminge commentedon Oct 21, 2026
Note
Grok responding on behalf of Julius.
Thanks for the trace analysis. The trigger matches current main. After every run,
RunFinalizationServicerefreshes the workspace entries for the run's directory (apps/server/src/orchestration-v2/RunFinalizationService.ts:102-106). When a search index exists for that directory, this callsWorkspaceSearchIndex.refresh(apps/server/src/workspace/WorkspaceEntries.ts:161-188), which callsfinder.scanFiles()directly (apps/server/src/workspace/WorkspaceSearchIndex.ts:413-417). Nothing on that path skips the rescan when the index's file watcher is already keeping it current. So with many threads finishing in one large project, every finished run asks fff to rescan.The freeze itself has the same root as #11306: a synchronous fff call on the server's main thread waits on fff's own lock while the index is busy. That part sits partly in how fff runs its calls. The forced rescan after every run is a separate trigger on our side, and it can be avoided independently. Keeping this open alongside #11306 and accepting it as a bug.
fix(server): finished runs no longer force a full file-index rescan
mikeohuo commentedon Oct 26, 2026
Note
Investigated by Claude Fable 5.1 and Claude Opus 5.5 in Claude Code, with a second read by GPT-6 Astra; written on behalf of the account owner.
Still present on
0.1.3-nightly.20261026.3726(bundles@ff-labs/fff-node0.11.0), same Windows machine and the same Unity repo (274,819 tracked files, 29.7 GB), now with 16 Claude sessions running at once. The numbers are worse than on 21 October, and the blocks now surface as SQLite lock waits, which is why #24895 and #24660 can look like the cause on a Windows desktop with a large repo.Rescans, from 107 minutes of
server.trace.ndjsonWorkspaceSearchIndex.refresh: 48 calls, 975 s in total, p90 85 s, max 138 s. 14 of the 48 exceeded the 15 s client ping timeout. Every one has the parent chainWorkspaceEntries.refresh < RunFinalizationService.refresh < RunFinalizationService.finalize, and on current main the path is unchanged:RunFinalizationService.ts:104→WorkspaceEntries.refresh→WorkspaceSearchIndex.ts:408finder.scanFiles(). ThewaitForIndexReadychild took 0 to 3.4 s, so the rest of each refresh is insidescanFiles().WorkspaceSearchIndex.refreshand end with it.logs\event-loop-stalls.ndjsonrecorded 85,369 ms and 82,148 ms, matching two of those gaps; the 122 s and 138 s refreshes do not appear in it.GetThreadDescriptionin the server process:fff-git-statusat 99 % of a core for the whole window (2,857 s CPU since 17:32),fff-watcher-own46 %,fff-bg-039 %. The main JavaScript thread averages 4 % and, sampled every second during three refreshes, sat at near-zero CPU in a lock-type wait, as in the samples in [Bug]: Backend event loop blocks for minutes inside synchronous fff mixedSearch waiting on watcher lock (home-directory project) — shows as "<label> is reconnecting" #11306.Knock-on into SQLite
Over 59 minutes: reader lane 10,703
sqlitespans, 2,673 s, of which 2,213 s isdb.lock_wait_ms(max 123 s); writer lane 955 spans, 1,189 s, 1,131 s waiting (max 135 s). 21 of the 29 reader waits over 30 s fall inside a long refresh. The statements at the front of the queue are cheap on their own against the same database (OrchestrationEventStore.latestAgentSequenceviaThreadPullRequestServiceV2.synchronize: covering-index search, under 1 ms;ProjectionStore.isNodeStored: primary-key read, 0.5 ms). The reader runs in a worker thread and its span ends only when the main thread processes the reply, so every reader span in flight during a block is inflated by the block. Database: 9.2 GB, 3,440 threads,orchestration_events1.16 M rows, WAL 9 MB.One thing not explained by the rescan: an
orchestrationV2.EventSink.writetransaction held the writer lock for 136 s at 18:03 with the event loop free, and writers queued behind it (db.lock_owner_at_start.namenames it). Possibly the checkpoint admission gate from #23482; not established.User-visible result
SessionStore.recordClientConnectionup to 70 s,ws.rpc.orchestration.subscribeThreadup to 787 s; the desktop shows " is reconnecting" and remote clients "T3 Code on did not answer".#22603 is still the fix for the trigger. Happy to re-measure on a build that includes it.
kurtkuehnert commentedon Oct 30, 2026
Note
Investigated by Claude Opus 5.5 in Claude Code; written on behalf of the account owner.
Another trigger for the same lock, on macOS with desktop 0.1.5 (
@ff-labs/fff-node0.11.0). Here the freeze lasted over 6 minutes, not seconds, because fff's watcher held the lock while it walked a large folder.Setup. The project is a folder that isn't a Git repository. It holds about 60 jj workspaces, and each workspace has a symlink into a shared data folder of about 750,000 files and 26 GB.
What happened
WorkspaceSearchIndex.refresh→scanFiles(), which waited on the watcher.A sample of the server showed the JS thread in
fff_scan_files→_pthread_cond_wait, andfff-watcher-owninreadandopenon files under the data folder.server.trace.ndjsonhad no span in that window. Killing the backend let the desktop start a new one.Before we added a root
.ignore, the same project also caused the plain version of this report: indexing the whole folder took 17 s for 1.26 million files. Quitting the app during one of those freezes left the server running with PPID 1, as in #22983.This supports the second fix in the report above: keeping fff calls off the event loop would have turned a 6-minute freeze into a slow index.
Workaround. A root
.ignorewith the data folders and.local/, where the links live. The full index is now 123,000 files in 0.8 s.luigiwidmore commentedon Nov 5, 2026
Note
Investigated by Claude Opus 5.5 in Claude Code; written on behalf of the account owner.
Another view of the same trigger, on Linux (
0.1.xnightly 2026-11-02; same code in0.1.8-nightly.20261104.4192,@ff-labs/fff-node0.11.0). Here it is a sustained disk-read storm, not a UI freeze. The project/thread cwd is the home directory (as in #11306).What we measured
read_bytes≈ 900 GB in 2 days). Per-thread/proc/<pid>/task/*/ioputs 100 % of it onfff-bg-0. The SQLite reader/writer threads were at ~0.$HOME(SDK sources, cache dirs, other tools' session logs).RunFinalizationService.refresh→WorkspaceEntries.refresh→finder.scanFiles(), which also renews the 15-minute idle TTL, so the index never expires.What fff does on each scan (standalone repro with the bundled fff-node)
On a 244 MB tree (15.6k files),
FileFinder.create+ warm-up and then eachscanFiles()read ~199 MB. That is exactly the total size of all files under 10 MB. It is the same withdisableContentIndexing: true, the same withcacheBudgetMax*set to 1, and the same withdisableWatch: true. SodisableContentIndexingdoes not stop the full-content read. Every post-run rescan reads the whole tree again, and for$HOMEthat far exceeds the page cache.The guard T3 turns off
WorkspaceSearchIndex.createFinderpassesenableFsRootScanning: true, enableHomeDirScanning: true. With bothfalse, fff refuses the home dir ("Can not run certain FFF features in a file system root or home directories. Consider smaller per-project directories."). We now run a local patch that sets both tofalse. Threads rooted at$HOMElose the file picker and find-in-files, but nothing gets indexed, so the post-run refresh is a no-op there.Suggestions, in addition to #22603 (not rescanning after every run):