{{ message }}
feat(server): read a location's catalog in one request - #47578
Open
Hona wants to merge 1 commit into
Open
Conversation
Hona
force-pushed
the
location-catalog
branch
3 times, most recently
from
September 6, 2026 07:17
d8c910e to
13049ef
Compare
Hona
force-pushed
the
location-catalog
branch
from
September 6, 2026 08:07
13049ef to
684ca85
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

data.location.sync— the shared read both the TUI and Desktop use to open a Location — fans out into 13 requests:location.getplus twelve list endpoints. Every session tab mount pays it, every/cdpays it, and reconnect pays it once per open directory. In a Desktop netlog those twelve endpoints were 646 of 759 real requests.This PR adds one server read model for a Location and teaches the shared client to seed from it, so both clients go from 13 requests to 2 with no client-specific code.
flowchart LR subgraph before [Before: 13 requests] L[location.get] --> F[agent · command · integration · mcp · mcp/resource · model · provider · reference · skill · shell · form · vcs] end subgraph after [After: 2 requests] C["GET /api/location/catalog"] --- V["GET /api/vcs (git; kept separate)"] endMeasured
One
location.syncper round, 30 rounds per strategy, p50 / p90, real servers with populated project directories. "Desktop today" is twelve requests through the app's 4-slot request queue; "TUI today" is twelve requests at once (Bun has no per-origin limit).* the LAN server runs a build without the endpoint; that row is one request to the slowest member (
/api/model), which bounds the catalog's cost. The WAN row is the real endpoint.location.sync, and ~140 ms on a first mount once the CORS preflights Chromium still pays against remote servers are counted (see below).GET /api/healthround trip in every environment — the server's concurrent read of nine services, which is the irreducible part.Local sidecar
packages/server/test/location-catalog.bench.tsruns onelocation.syncagainst a real server 30 times per strategy, on this repository's project directory (populated agents, commands, skills, MCP config), idle server, loopback.That is the floor. Bun's
fetchdoes not pay Chromium's per-request setup (2–7 ms each in the netlog), and an idle server does not queue; under a busy sidecar the twelve requests wait behind each other and behind other clients, while the catalog is one handler invocation. Reconnect with 20 open directories goes from 240 requests to 20.LAN
Remote server over the LAN (Windows → macOS over Tailscale, real project directory, keep-alive, 30 rounds). The remote runs a build without the endpoint, so the catalog row is one request to the slowest member (
/api/model, 21 ms p50) — what the catalog's cost is bounded by:WAN
Remote server over the WAN (Windows → a Debian VPS, 25 ms ping, SSH tunnel, this branch's server, real project directory, keep-alive, 30 rounds). Here the catalog row is the real endpoint:
GET /api/healthDesktop's three queue waves collapse to one round trip plus ~15 ms of concurrent server work (the catalog sits 15 ms above the bare round trip). The TUI already sent all twelve at once, so over a WAN its gain is server load and connections rather than latency; its latency gain is the local and LAN cases above.
Desktop against a remote server still sends
Authorizationfrom the renderer, so each of those twelve URLs also pays a CORS preflight on first use: a first mount of a new directory is 24 round trips today versus 2 — about six queue waves, or ~180 ms at this RTT.Where Desktop and the TUI converge
Neither client talks to the server directly for this; both call the same
createData().location.syncfrom@opencode-ai/client/solid, which is the single place the change lives.flowchart TB subgraph desktop [Desktop · packages/app] D1["workspaces/location.tsx · LocationProvider<br/>(session tab mount)"] D2["runtime/server/sync.tsx · bootstrapInstance<br/>(project open, reconnect)"] end subgraph tui [TUI · packages/tui] T1["context/location.tsx · LocationProvider<br/>(/cd, session open)"] T2["context/data.tsx · DataProvider bootstrap"] T3["component/dialog-session-list.tsx"] end D1 & D2 & T1 & T2 & T3 --> S["@opencode-ai/client/solid<br/>createData().location.sync(ref) · data.ts"] S --> G["@opencode-ai/client/promise (generated)<br/>api.location.catalog({ location })"] G -->|"GET /api/location/catalog?location[directory]=…"| P["@opencode-ai/protocol<br/>LocationGroup · location.catalog"] P --> H["@opencode-ai/server<br/>LocationHandler → LocationCatalog.read"] H -->|"Effect.all, unbounded"| C["core services<br/>Agent · Command · Integration · Mcp · Catalog · Reference · Skill · Shell · Form"]Before, the same
location.syncframe fanned out into twelve generated-client calls, each becoming its own HTTP request,LocationMiddlewareresolution, handler fiber, and JSON encode — on both clients, because it is the same code.One contract, enforced in both directions.
LocationCatalogis a struct whose fields are the list endpoints' owndataschemas (Schema.Array(Agent.Info),Mcp.ResourceCatalog, …), so:catalog.data[field]deep-equals each list endpoint'sdatafor the same location;catalog: (response) => response.data.agentreaders are typed against the generatedLocationCatalogOutput, so a field that changes shape failstypecheckin@opencode-ai/clientrather than at runtime in either app;agent.updatedevent still refetches/api/agentonly, on both clients, because that logic also lives indata.ts.The TUI and Desktop need no code changes and gain the same behaviour: the request count drops from 13 to 2 per Location, and the response is one consistent snapshot rather than twelve reads racing service settlement.
Server —
packages/protocol,packages/serverGET /api/location/catalog(location.catalog) returnsLocation.response(LocationCatalog): exactly what each list endpoint returns, keyed by field.packages/server/src/location-catalog.tsowns the read: the nine services, oneEffect.allwith unbounded concurrency.mcpServers(the public MCP summary) lives there too andmcp.listreuses it, so the two endpoints cannot drift.git statuson a large repository takes seconds (p50 1.8 s in the netlog) and would hold back everything else.location-catalog.test.tsasserts the catalog equals the individual endpoints for the same location.Client —
packages/client/src/solid/data.tsEach
locationResourcenow declares how to read itself from the catalog response, and the catalog resources are one list that drives seeding, syncing, and invalidation. The three hand-maintained 13-line blocks are gone.seedpublishes the field and marks its sync key current, so the per-resourcesync()calls views make (useProviders, MCP dialogs, …) are no-ops until an event invalidates them. Afteragent.updated, the nextlocation.syncfetches only/api/agent.seednever overwrites a key that already has a current or in-flight read: a read that lands while the catalog is in flight is newer and wins.bun run generate).Tests
/api/location/catalogfrom their per-endpoint mocks, so specs keep stubbing/api/agent,/api/skill, … individually; app specs that hand-roll the whole API return the catalog directly.