Skip to content

fix(app): load worktree inventory on demand and cap concurrent server requests - #47441

Merged
Hona merged 3 commits into
v2from
worktree-fanout
Sep 5, 2026
Merged

fix(app): load worktree inventory on demand and cap concurrent server requests#47441
Hona merged 3 commits into
v2from
worktree-fanout

Conversation

@Hona

@Hona Hona commented Sep 5, 2026

Copy link
Copy Markdown
Member

Desktop periodically goes unresponsive with a red server dot while assistant output is still streaming. NetLogs from three captures show the same shape every time: hundreds of local API jobs created within seconds, then health preflights and the question-reply POST waiting on SOCKET_POOL_STALLED_MAX_SOCKETS_PER_GROUP for 30–60 s. The server keeps answering 200 OK; the browser simply has no socket to send on.

Why the pool fills

flowchart LR
  subgraph before [Before]
    B1[bootstrap] -->|"GET /api/worktree × every project row (367 jobs / 263 dirs)"| S1[(server)]
    S1 -->|boots a Location per dir| E1[catalog / mcp / agent / ... events × 118 dirs]
    E1 -->|"eager re-fetch per event, per dir"| B1
  end
  subgraph after [After]
    A1[bootstrap] -->|"GET /api/project (1)"| S2[(server)]
    A2[mounted Location or selected project] -->|"GET /api/worktree (1 per shown project)"| S2
  end
Loading

Chromium allows 6 HTTP/1.1 connections per origin. /api/event holds one permanently and health probes need one, so the app has 4 to work with. loadProjectsQuery was spending far more than that: it called worktree.list for every project in the database to fill Project.sandboxes (the server always returns []). Each call boots a Location and runs git worktree list, and it re-ran on every config/agent/worktree.updated and every reconnect. Those booted Locations then emit catalog events that the client echoes back as more reads — bounded now by what the user actually opens.

This aligns Desktop with the TUI's usage pattern: sync only the Location you are showing.

Changes

Inventory loads when a project is shownpackages/app/src/workspaces/inventory.ts

  • Bootstrap is metadata-only. A project's worktrees load from useWorkspaceLocation (session / new-session view) and from Home project selection, keyed by the metadata root and merged into the same global record.
  • worktree.updated refreshes only a project that was loaded; project.updated no longer wipes a loaded inventory (pre-existing bug: the payload carries sandboxes: []).
  • Home resolves a session's project by directory, then by projectID, so worktree sessions of not-yet-loaded projects keep their label and open at the project root.
// sync.tsx
if (event.type === "worktree.updated") {
  const root = globalStore.project.find((project) => project.id === event.data.projectID)?.worktree
  if (root) void worktrees.refresh(root)   // no-op unless a view loaded it
  void bootstrap.refetch()                  // metadata only now
  return
}

Request queuepackages/app/src/runtime/server/request-queue.ts

  • FIFO semaphore in front of every SDK request to a server, capped at 4 in flight; /api/event is exempt. Stays saturated while there is demand, so a normal mount burst serialises instead of stalling inside Chromium where nothing can observe it.
  • When the oldest queued request has waited ≥ 2 s, one console.warn per 10 s lists every in-flight and queued request with its age. No toast; it lands in the debug export.
[server-request-queue] server thrashing detected {
  limit: 4,
  inflight: [{ method: "GET", url: ".../api/worktree?location[directory]=...", ms: 2200 }, ...],
  queued:   [{ method: "GET", url: ".../api/health", ms: 2000 }, ...]
}

Trade-offs

  • A mount fires ~19 requests; Chromium ran 6 at once, the queue runs 4. On a healthy local server that is on the order of ~100 ms worst case. Session-entry benchmark to follow in a comment.
  • The per-event catalog echo in @opencode-ai/client (createData re-fetching catalogs for any Location that emits, whether or not anything read them) is left as-is here so this PR stays app-only. With the fan-out gone it is bounded by booted Locations and throttled by the queue. The client-side fix is fix(client): refresh only loaded catalogs on location events #47443.
  • Settings → Worktrees still lists every project when opened (user-initiated). It needs strategy from the server to go away; that is the core follow-up, together with having /api/project return the worktree table so the app never has to enumerate at all.
  • Locations the old fan-out already booted are evicted by LocationActivity 60 min after their last session event; nothing re-boots them now.

Related: #47428

Hona added 2 commits September 5, 2026 14:39
Global bootstrap issued GET /api/worktree for every project row on the server,
each booting a Location and running Git discovery. With a few hundred
historical projects this saturated the browser's per-origin connection pool,
stalling health probes and user actions.

Project metadata is now loaded alone. A project's worktree inventory loads
when a view shows it: a mounted Location (session or new-session view) or a
project selected on Home. Loaded inventories survive metadata refetches and
project.updated payloads, and worktree.updated refreshes only the affected
project when it was loaded.

Home resolves a session's project by directory first and by projectID second,
so worktree sessions of not-yet-loaded projects keep their label and open at
the project root.
Route every SDK request to a server through a FIFO queue capped at four in
flight. Chromium allows six connections per origin; the event stream holds one
and health probes use their own fetch, so the app's own bursts can no longer
starve them inside the browser where nothing can observe it. /api/event is
exempt.

When the oldest queued request has waited two seconds, log the in-flight and
queued requests once per ten seconds so debug exports show what the server
was busy with.
@Hona
Hona requested a review from Brendonovich as a code owner September 5, 2026 04:43
Copilot AI lite review requested due to automatic review settings September 5, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Hona Hona changed the title fix(app): stop worktree fan-out and catalog echo from starving the connection pool fix(app): stop worktree fan-out from starving the connection pool Sep 5, 2026
@Hona Hona changed the title fix(app): stop worktree fan-out from starving the connection pool fix(app): load worktree inventory on demand and cap concurrent server requests Sep 5, 2026
@Hona
Hona enabled auto-merge (squash) September 5, 2026 05:05
LocationGetOutput.project is required. Three spec-local mocks omitted it, so
the Location provider's inventory lookup threw under those fixtures. Also
answer GET /api/worktree with the root instead of an empty object.
@Hona
Hona merged commit 30d1049 into v2 Sep 5, 2026
8 checks passed
@Hona
Hona deleted the worktree-fanout branch September 5, 2026 05:25

@dapperdan17 dapperdan17 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants