# OpenCode Provider Snapshot Startup Timeout Diagnosis - 2026-05-27 ## Answer The startup timeout is real OpenCode provider snapshot work, not an agent resume path. In the dev-style copied-home reproduction, the OpenCode snapshot misses the 30s budget because several expensive things stack: 1. Paseo starts from a copied `PASEO_HOME` containing 4,851 agent records. 2. Clients ask for provider snapshots for three cwd scopes at almost the same time: - `/Users/moboudra` - `/Users/moboudra/dev/paseo` - `/Users/moboudra/dev/blankpage/editor` 3. Each OpenCode snapshot runs two OpenCode SDK calls: - `GET /provider?directory=...` through `client.provider.list()` - `GET /agent?directory=...` through `client.app.agents()` 4. One cold `opencode serve` process is shared by the three cwd scopes. It took 8.562s to become ready. 5. After OpenCode was listening, Paseo issued six OpenCode HTTP calls concurrently. 6. The OpenCode `/provider` responses are large: about 3,549,620 decompressed bytes per cwd. 7. During the same window, the daemon was still doing heavy startup workspace git work. In the exact 18:14:19-18:14:43 window, the daemon log has 292 git spawn/close events. 8. The `/provider` calls eventually succeeded, but too late: they completed about 32.2s-32.5s after the snapshot fetch started, while the snapshot timeout is 30s. So the root cause is: ```text Cold OpenCode server startup + three concurrent cwd snapshots + large OpenCode /provider responses + daemon startup git contention causes client.provider.list() to complete after Paseo's 30s snapshot budget. ``` More precise wording: the contention is machine-level process/CPU/filesystem contention created by daemon startup work, especially git work. It is not proven to be an OpenCode internal lock or a Paseo-only event-loop issue. A daemon-free repro with only OpenCode plus an external git storm slowed the same six OpenCode calls from about 1s to about 30s total. Manual settings refresh works because it runs after startup contention is gone and uses `force: true`, which creates fresh OpenCode runtime/server state. The same OpenCode provider refreshes then complete in about 1.7s-2.2s. The daemon does not auto-retry error snapshots. A failed provider snapshot is cached as `status: "error"` until an explicit refresh resets it to loading. ## Follow-up: Normal Copied-Home Startup Check I later reran a normal dev-daemon startup against a fresh copy of the same Paseo home metadata and drove the app startup request path: ```text fetchWorkspaces fetchAgents getProvidersSnapshot(home scope) getProvidersSnapshot(first workspace scope) ``` That run did not reproduce the 30s OpenCode timeout. ```text home scope: OpenCode ready at ~8s availability: 1.6s fetch total: 5.2s first workspace scope: OpenCode ready at ~26s availability: 2.0s fetch total: 15.4s ``` The slowest OpenCode operation in that successful run was the workspace-scoped `/provider` response body read: `13.6s`. The daemon log had no `Timed out refreshing OpenCode` entry and no OpenCode provider snapshot failure. This means the timeout is reproducible under the heavier multi-scope startup contention captured below, but it is not guaranteed on every copied-home dev startup. ## Reproduction Used The user's correction was right: the useful reproduction is not a random isolated home. It must match `dev.sh` worktree behavior. Relevant scripts: - `scripts/dev.sh` - `scripts/dev-daemon.sh` - `scripts/dev-home.sh` `dev-home.sh` only seeds this metadata into the dev home: ```text agents/**/*.json projects/**/*.json config.json ``` It does not copy `chat`, `loops`, `schedules`, sockets, pid files, logs, or worktree contents. I ran a separate daemon, not the main daemon: ```text PASEO_HOME=/var/folders/xl/kkk9drfd3ms_t8x7rmy4z6900000gn/T/paseo-devseed.Wms6pi PASEO_LISTEN=127.0.0.1:51116 PASEO_LOG_LEVEL=trace ``` Startup facts: ```text 18:13:39.552 Agent storage initialized: 712ms 18:13:39.559 Workspace registries bootstrapped: 719ms 18:13:39.961 Agent registry loaded: 4851 records 18:13:39.972 Server listening: http://127.0.0.1:51116 ``` The probe then connected four client sessions and requested: - workspaces - active agents - provider snapshots for home, paseo, and blankpage/editor Client-visible result: ```text 18:14:30.263 /Users/moboudra/dev/blankpage/editor opencode error: OpenCode app.agents timed out after 10s 18:14:41.687 /Users/moboudra/dev/paseo opencode error: Timed out refreshing OpenCode after 30000ms 18:14:41.688 /Users/moboudra opencode error: Timed out refreshing OpenCode after 30000ms ``` ## Exact OpenCode Timeline OpenCode snapshot requests began at `18:14:10`. Availability checks: ```text 18:14:10.780 opencode availability start for /Users/moboudra 18:14:10.787 opencode availability start for /Users/moboudra/dev/paseo 18:14:10.800 opencode availability start for /Users/moboudra/dev/blankpage/editor 18:14:11.363 paseo availability complete: 576ms 18:14:11.376 home availability complete: 597ms 18:14:11.391 blankpage availability complete: 591ms ``` OpenCode server acquisition: ```text 18:14:11.364 OpenCode server spawn start: opencode serve --port 56376 18:14:19.926 OpenCode server listening after 8562ms ``` Six SDK calls were then issued: ```text 18:14:19.931 GET /provider directory=/Users/moboudra/dev/paseo 18:14:19.931 GET /agent directory=/Users/moboudra/dev/paseo 18:14:19.931 GET /provider directory=/Users/moboudra 18:14:19.931 GET /agent directory=/Users/moboudra 18:14:19.931 GET /provider directory=/Users/moboudra/dev/blankpage/editor 18:14:19.936 GET /agent directory=/Users/moboudra/dev/blankpage/editor ``` Why six: | Cwd | Why that scope exists | Model call | Mode call | | -------------------------------------- | ---------------------------------------------------------- | --------------------------------------- | --------------------------------- | | `/Users/moboudra` | home/settings provider snapshot | `client.provider.list()` -> `/provider` | `client.app.agents()` -> `/agent` | | `/Users/moboudra/dev/paseo` | workspace-scoped provider snapshot for the Paseo workspace | `client.provider.list()` -> `/provider` | `client.app.agents()` -> `/agent` | | `/Users/moboudra/dev/blankpage/editor` | workspace/agent cwd snapshot for blankpage/editor | `client.provider.list()` -> `/provider` | `client.app.agents()` -> `/agent` | Multiple clients can request the same snapshot scope during startup, but non-forced provider loads are deduped by `(cwd, provider)`. Different cwd scopes are separate loads. Three cwd scopes times two OpenCode SDK calls each is the six OpenCode calls in this repro. Headers arrived before the 30s timeout: | Call | Cwd | Headers after request | | ----------- | ------------------------------- | --------------------- | | `/provider` | `/Users/moboudra` | 6.462s | | `/agent` | `/Users/moboudra` | 6.681s | | `/agent` | `/Users/moboudra/dev/paseo` | 6.681s | | `/provider` | `/Users/moboudra/dev/paseo` | 8.192s | | `/provider` | `/Users/moboudra/dev/blankpage` | 8.654s | | `/agent` | `/Users/moboudra/dev/blankpage` | 8.649s | But body consumption and completion lagged: ```text 18:14:29.380 /agent home complete, total app.agents duration 9450ms 18:14:29.813 /agent paseo complete, total app.agents duration 9883ms 18:14:30.263 /agent blankpage timed out at 10s 18:14:31.332 /agent blankpage body finally finished, after the 10s app.agents timeout 18:14:41.687 paseo snapshot outer 30s timeout fires 18:14:41.688 home snapshot outer 30s timeout fires 18:14:43.593 /provider home completes, provider.list duration 23664ms, total listModels 32218ms 18:14:43.798 /provider blankpage completes, provider.list duration 23868ms, total listModels 32411ms 18:14:43.839 /provider paseo completes, provider.list duration 23911ms, total listModels 32476ms ``` The useful `/provider` results arrived about 1.9s-2.2s after the snapshot manager had already marked home and paseo as failed. ## Why Settings Refresh Works After the daemon settled, I ran the same refresh path through the daemon on port `51116`, using `refreshProvidersSnapshot({ providers: ["opencode"] })`. Results: ```text home refresh: total: 2165ms status: ready models: 409 modes: 5 /Users/moboudra/dev/paseo refresh: total: 1675ms status: ready models: 409 modes: 5 /Users/moboudra/dev/blankpage/editor refresh: total: 1794ms status: ready models: 409 modes: 5 ``` Trace details for the manual-style refresh: ```text OpenCode server acquisition: 708ms-1291ms /agent completion: 433ms-592ms after request start /provider completion: 524ms-618ms after request start ``` That proves the startup failure is not bad credentials, not a permanently wedged OpenCode install, and not OpenCode generally taking more than 30s. It is startup timing and contention. ## Minimal OpenCode-Only Repros ### OpenCode Only, No Daemon, No Artificial Load I started a fresh `opencode serve`, waited for stdout `listening on`, then issued the same six HTTP calls concurrently: ```text GET /provider?directory=/Users/moboudra GET /agent?directory=/Users/moboudra GET /provider?directory=/Users/moboudra/dev/paseo GET /agent?directory=/Users/moboudra/dev/paseo GET /provider?directory=/Users/moboudra/dev/blankpage/editor GET /agent?directory=/Users/moboudra/dev/blankpage/editor ``` Three runs: | Run | `opencode serve` ready | All six calls complete | | --- | ---------------------- | ---------------------- | | 1 | 1376ms | 1295ms | | 2 | 906ms | 1050ms | | 3 | 939ms | 898ms | Slowest individual call in those runs: ```text /provider /Users/moboudra/dev/paseo: 1270ms total /agent /Users/moboudra/dev/blankpage/editor: 1251ms total ``` So six concurrent OpenCode calls alone are not the bug. ### OpenCode Only Plus External Git Storm, No Daemon I then ran the same OpenCode-only six-call test while an external shell spawned repeated git commands across the same real workspaces/worktrees. This did not use the Paseo daemon. Result: ```text opencode serve ready: 15479ms all six OpenCode calls complete: 15176ms after server ready combined cold-start + calls: about 30655ms ``` Individual calls under the external git storm: | Call | Cwd | Total | | ----------- | -------------------------------------- | ------: | | `/provider` | `/Users/moboudra` | 10684ms | | `/agent` | `/Users/moboudra` | 10767ms | | `/provider` | `/Users/moboudra/dev/paseo` | 13220ms | | `/agent` | `/Users/moboudra/dev/paseo` | 13147ms | | `/provider` | `/Users/moboudra/dev/blankpage/editor` | 14675ms | | `/agent` | `/Users/moboudra/dev/blankpage/editor` | 15038ms | This is the daemon-free minimal evidence that process/filesystem contention can push the same OpenCode cold-start + six-call workload to the same 30s boundary. ## Why It Does Not Retry `ProviderSnapshotManager.getSnapshot()` only starts background warmup for: - no existing snapshot - missing providers - entries still in `loading` with no active load When refresh fails, `refreshProvider()` stores: ```text status: "error" error: "Timed out refreshing OpenCode after 30000ms" ``` An `error` entry is not treated as stale/loading by `getSnapshot()`, so normal reads keep returning the cached error. Settings refresh calls `refresh_providers_snapshot_request`, which routes to: ```text refreshSettingsSnapshot() clearCachedProviders() resetSnapshotToLoading() refreshProviders(... force: true) ``` That is why you have to force a manual refresh. ## Git Work During The Repro This is not the final optimization report, but it matters for the timeout because it overlaps exactly with OpenCode response handling. Total git commands in the dev-style copied-home daemon log: ```text 632 spawned 632 closed ``` Top cwd counts: | Count | Cwd | | ----: | ------------------------------------------------------------------------------------- | | 44 | `/Users/moboudra/.paseo/worktrees/1luy0po7/merry-ladybug` | | 44 | `/Users/moboudra/.paseo/worktrees/1luy0po7/hopeful-eel` | | 44 | `/Users/moboudra/.paseo/worktrees/1luy0po7/fix-compaction-cancel-loading` | | 44 | `/Users/moboudra/.paseo/worktrees/1luy0po7/fix-archive-worktree-session-history` | | 44 | `/Users/moboudra/.paseo/worktrees/0vpo9h4b/breezy-toad` | | 36 | `/Users/moboudra/.paseo/worktrees/steering-policy-refactor-detached` | | 36 | `/Users/moboudra/.paseo/worktrees/1luy0po7/integration-session-mcp-command-stack` | | 36 | `/Users/moboudra/.paseo/worktrees/1luy0po7/fix-provider-diagnostic-binary-resolution` | | 36 | `/Users/moboudra/.paseo/worktrees/1luy0po7/feat-voice-runtime-on-demand` | | 36 | `/Users/moboudra/.paseo/worktrees/1luy0po7/feat-find-in-pane` | | 36 | `/Users/moboudra/.paseo/worktrees/1luy0po7/epic-paseo-client-sdk` | | 24 | `/Users/moboudra/dev/paseo` | | 24 | `/Users/moboudra/dev/blankpage/editor` | | 24 | `/Users/moboudra/dev/faro/main` | | 24 | `/Users/moboudra/dev/konbert/web` | | 24 | `/Users/moboudra/dev/paseo-cloud` | In the exact OpenCode pressure window, `18:14:19` through `18:14:43`, there were: ```text 142 git command spawns 150 git command closes ``` The main repeated command shapes were: ```text 76 git rev-parse --show-toplevel 72 git status --porcelain 72 git show-ref --verify --quiet refs/remotes/origin/main 72 git show-ref --verify --quiet refs/heads/main 16 git config --get branch.main.remote 16 git config --get branch.main.merge 16 git rev-list --count main..origin/main 16 git rev-list --count origin/main..main ``` ## Original `log.txt` Alignment The original startup showed the same home and paseo outer timeout shape: ```text 16:04:22.466 /Users/moboudra/dev/paseo: Timed out refreshing OpenCode after 30000ms 16:04:22.482 /Users/moboudra: Timed out refreshing OpenCode after 30000ms ``` The original logs did not include SDK fetch/header/body timing, so they could only show the wrapper-level timeout. The dev-style copied-home reproduction with instrumentation now shows the missing link: the `/provider` calls completed just after the 30s snapshot budget. ## Files Instrumented For Diagnosis Temporary trace instrumentation was added to: - `packages/server/src/server/agent/provider-snapshot-manager.ts` - `packages/server/src/server/agent/providers/opencode-agent.ts` - `packages/server/src/server/agent/providers/opencode/runtime.ts` - `packages/server/src/server/agent/providers/opencode/server-manager.ts` The instrumentation is behavior-neutral and only emits trace logs.