Repository navigation
core: project-copy refresh can report a 48-second no-op #37793
Description
Summary
Immediately after a 53.4-second cold Location acquisition, the forked ProjectCopy.refresh timer ran for another 48.1 seconds and completed with updated=[] and removed=[]. The log currently makes this look like 48 seconds of worktree discovery, but there is not enough stage-level evidence to distinguish actual refresh work from scheduler starvation or an unrelated synchronous post-boot task blocking the event loop.
This should be tracked separately from Location acquisition because the refresh starts only after the Location graph is available and runs in a scoped background fiber.
Environment
- opencode version:
0.0.0-next-15834 - OS: Darwin 25.5.0, arm64
- Terminal: WarpTerminal,
TERM=xterm-256color,COLORTERM=truecolor - Shell:
/bin/zsh - Install/channel: global Bun install,
next - Managed service: one existing version-matched service; same PID before and after the incident
- Repository shape: large VCS-backed repository with 81 worktree entries at follow-up
- Active plugins: three local user plugins; names and paths omitted
Reproduction
- Open a cold VCS-backed Location on the managed service.
- Wait for
location services booted. - Observe the automatically forked
project copy refresh startedandproject copy refresh donerecords. - Compare the refresh result, persisted project-directory count, direct Git command timings, service responsiveness, and event-loop delay.
The 48-second result was observed once in the same severe cold-start incident. Ordinary refreshes for this project usually complete in milliseconds to hundreds of milliseconds.
Expected Behavior
- A no-change automatic refresh should finish quickly and should not delay health checks, SSE, plugin readiness, or other clients.
- The timer should identify time spent loading persisted directories, checking paths, discovering each strategy, canonicalizing results, and committing changes.
- Time waiting to be scheduled should be distinguishable from time spent executing refresh work.
- Automatic discovery should be bounded for repositories with many linked worktrees.
- If the project has no persisted source directories, the refresh should take the empty fast path without invoking Git.
Actual Behavior
Sanitized UTC timeline:
18:01:52.738 location services booted, durationMs=53359
18:01:52.791 project copy refresh started
18:02:40.711 repository watcher fallback is logged
18:02:40.841 project copy refresh done, updated=[], removed=[]
The measured refresh interval is 48,050 ms. The server emitted almost no durable application traffic during this period, but normal service recovery and MCP/plugin startup were delayed until the same boundary.
At the pinned revision, refreshAfterBoot starts the timer, forks copies.refresh, and returns from layer acquisition.
ProjectCopy.refresh has these stages:
- Read persisted project directories.
- Check every stored path with unbounded concurrency.
- Run every registered strategy for every existing source directory with unbounded concurrency.
- Deduplicate discovered directories.
- Apply updates/removals in one database transaction.
- Publish an update only if rows changed.
The built-in strategy performs repository discovery, git worktree list --porcelain, and filesystem canonicalization for every returned worktree at copy-strategies.ts:23-32. The Git subprocess boundary is git.ts:853-875, and parsing is git.ts:910-921.
Additional Context
Follow-up measurements
- A direct repository-discovery command completed in 0.02 seconds.
git worktree list --porcelainreturned 81 entries in 0.12 seconds.- Read-only follow-up inspection of the same channel database found zero current
project_directoryrows globally.
The current row count does not prove the exact table state at the incident timestamp, and the fast Git commands do not reproduce the original load. They do show that a persistent 48-second Git cost is not currently present. Because the timer measures wall time inside a forked fiber, the strongest honest conclusion is that the refresh fiber remained incomplete for 48 seconds, not that git worktree list itself consumed 48 seconds.
Instrumentation needed
Record elapsed execution time and scheduler delay for each refresh stage. Strategy discovery should identify the strategy ID, source ordinal, returned entry count, and duration, but never log directory values. Subprocess diagnostics should include command category, exit status, and duration without stdout.
Also record event-loop delay over the interval. If a different synchronous post-boot operation starves an otherwise empty refresh, the ProjectCopy timer should make that explicit rather than attributing all wall time to refresh.
Acceptance criteria
- A no-directory refresh has a tested empty fast path and does not invoke any strategy.
- A no-change refresh over a repository with at least 100 synthetic worktree entries completes within a documented non-gating benchmark budget.
- Stage diagnostics distinguish database read, path checks, each strategy call, canonicalization, transaction, event publication, and scheduler delay.
- One slow or unavailable source has a bounded timeout or yields an isolated error instead of delaying every source indefinitely.
- Concurrent strategy/source work has a measured concurrency policy rather than unbounded fan-out by default.
- The automatic post-boot refresh never delays Location readiness and does not make the shared health or event endpoints unresponsive.
- A regression can reproduce either actual slow refresh work or scheduler starvation and names the correct owner.
Related
- 2.0: managed-service restart causes reconnect herd and resource spikes #36285 tracks broader restart and cold-Location resource spikes.
- The preceding 53.4-second Location acquisition is a separate timer and failure boundary.
- v2 tests: process and project-copy tests flake in CI #37321 tracks project-copy test flakiness, not this production latency.
All paths, project IDs, worktree names, and Session data have been removed from this report.
Activity
neriousy commentedon Oct 21, 2026
Outdated: #42149 replaced ProjectCopy with worktrees and removed the automatic post-boot
refreshAfterBoot.Worktree.refreshnow runs only when requested, so this 48s post-boot no-op no longer applies as written.