Repository navigation
Description
Problem
The V2 TUI has two different location concepts that are not connected:
defaultLocationis resolved from the TUI startup directory and drives bootstrap plus every location-optional data getter.LocationProviderfollows the active Home or Session route, but it is only an accessor. It does not hydrate, invalidate, or reconcile that location's cached data.
As a result, some session UI explicitly reads the route location while other code silently reads the startup location. Navigating to a session at another location does not bootstrap that location's agents, commands, integrations, MCP servers, models, providers, references, skills, or shells.
This is broader than the stale shell count tracked in #35816.
Observed Failure
On 0.0.0-next-15234, session ses_0b8ba14adffeC6p87v57JF1FSf showed 2 shells in the footer while the Shell tab showed no commands.
The timeline exposed two independent problems:
- The footer read the session's
life-hubshell cache, while the Shell tab read the startup/default cache. - After the service upgrade/restart, the new server reported no shells, but reconnect refreshed shells only for
defaultLocation, leaving the non-defaultlife-hubcache stale.
The restart also exposed the inverse performance problem. Bootstrap discovered historical session locations and eagerly refreshed global forms across at least eight location graphs. Location boot took 7-21 seconds for several projects, watchers were still starting around 25 seconds, and the service reached roughly 53% CPU and 3.1 GB RSS during the reconnect herd.
Current Lifecycle
DataProvider initializes defaultLocation from process.cwd() and changes it only during no-argument bootstrap location resolution.
Every initial SSE connection and reconnect emits server.connected. DataProvider responds by refreshing the default location's:
- sessions
- permissions and forms
- agents and commands
- integrations and MCP
- models and providers
- references and skills
- shells
It then refreshes only global forms for every distinct non-default location found in the loaded session list.
Session route activation separately refreshes session info, messages, permissions, forms, and global forms. It does not hydrate the location-scoped resources above. session.moved changes the reactive LocationProvider, but the route-ID activation effect does not rerun, so project/editor side effects and cached resources remain attached to the old location.
Confirmed Bugs
- Non-default session routes read uninitialized location caches.
- Agent/model state can come from the startup location and be submitted to a session at another location.
- The Shell tab lists the default location even though shell termination uses the active route location.
- Prompt autocomplete reads default-location references and agents.
- Model, integration, MCP, status, and provider-connectivity views frequently read the default location.
reference.updatedignoresevent.locationand refreshes default references.- Reconnect leaves non-default agents, models, commands, references, skills, integrations, MCP, and shells stale.
- Default-location whole-map reconciliation can remove a non-default session's pending permissions/forms without restoring them.
- A reconnect arriving during an existing bootstrap can be coalesced into the old bootstrap without scheduling a fresh pass.
- Snapshot responses can overwrite newer event projections. For shells, an older
/api/shellresponse can resurrect a process already removed byshell.exited. - Remote/server-attached startup can split the implicit default between the local TUI cwd and the server/client default.
Desired Model
Make location activation a real cache lifecycle seam.
A location scope should:
- be keyed by canonical
{ directory, workspaceID } - retain its canonical location rather than relying only on a serialized record key
- lazily bootstrap when Home or a Session route acquires it
- expose bound resource selectors so descendants cannot silently fall back to another location
- receive event updates by
event.location - become stale when the event stream disconnects
- refresh only active or explicitly pinned scopes after reconnect
- refresh before reuse when returning to an inactive cached scope
- be evictable after it is inactive
Conceptually:
const location = data.location.scope(session.location)
location.agent.list()
location.model.list()
location.shell.list()defaultLocation should mean the Home/new-session location only. It should not be the implicit location for code rendered under an active Session location.
The existing App global-sync implementation is a useful precedent: lazy directory child stores, bootstrap-on-access, reconnect queues, pinning, and eviction. The TUI likely needs a smaller version of that module rather than shell-specific provenance fields.
Reconnect Semantics
On disconnect:
- Mark event-backed inventories stale.
- Never continue presenting an old shell inventory as authoritative.
- Preserve cached durable/config data only with an explicit stale state.
On server.connected:
- Refresh the root/default scope only if Home needs it.
- Refresh currently active/pinned location scopes.
- Do not bootstrap every location represented by recent session history.
For resources populated by both snapshots and live events, define ordering rather than relying on last-write-wins. A refresh should capture a connection epoch or mutation revision and discard/retry if a newer lifecycle event arrives before the snapshot is applied. If generation machinery remains intentionally excluded from DataProvider, the server inventory protocol needs an equivalent snapshot watermark.
Route Semantics
Entering location B should:
- Acquire B's location scope.
- Bootstrap B if absent or stale.
- Render all descendants against B's bound data.
- Release the previous scope after navigation.
Moving the current session from A to B should trigger the same lifecycle, including project workspace and editor reconnection.
Returning to A may use cached data while revalidating for durable/config resources, but ephemeral process inventory must not be shown as current until reconciled.
Suggested Sequence
- Introduce a generic active-location scope/lease in the TUI data layer.
- Move location bootstrap from global default bootstrap into scope activation.
- Bind Session descendants to scoped selectors and remove implicit default fallbacks.
- Make session movement rerun location activation side effects.
- Reconcile active scopes after reconnect and stop eagerly booting historical locations.
- Add snapshot/event ordering for ephemeral inventories.
- Add inactive-scope eviction if cache growth warrants it.
Acceptance Criteria
- Opening an existing session at a non-default location hydrates all location-scoped resources before they are treated as ready.
- Switching locations cannot display or submit agents/models from the previous/default location.
- Session movement updates data scope, project workspace, and editor directory.
- Reconnect cannot leave stale shell counts or erase non-default pending interactions.
- The Shell tab and footer read the same active location.
- Location-scoped update events refresh the event's location.
- Restart cost scales with active/pinned locations, not all locations in recent session history.
- Tests cover first navigation, route switching, session movement, transient reconnect, service replacement, and a snapshot/event race.
Open Decisions
- Should location bootstrap block route readiness, or expose partial/stale states while resources load?
- Which resources may use stale-while-revalidate, and which must be hidden until authoritative?
- Should the cache support multiple simultaneously pinned locations now, or begin with one active route location?
- Should snapshot/event ordering be solved client-side with epochs/revisions or server-side with inventory watermarks?
- Shell footer semantics remain a separate decision in bug(tui): shell footer count can become stale #35816: all shell-tool processes versus explicitly backgrounded jobs.
Activity
Dante-dan commentedon Oct 2, 2026
The current V2 TUI still has the seam described here:
LocationProvideris only an accessor, autocomplete readsreference.list()without its route location, andreference.updatedrefreshes the default inventory. I have not reproduced the original shell/restart trace.I propose a first slice for steps 1–3: a scope retaining canonical
{ directory, workspaceID }, acquired/released by the reactive Home/Session location, with lazy bootstrap and bound selectors. Session movement reacquires the scope even when the session ID is unchanged. The route can render while loading, but agent/model selection and submission wait for the selected scope to become ready; they never fall back to another location.Reconnect would invalidate scopes and refresh only acquired/pinned locations. A generation change during an in-flight bootstrap schedules another pass. Tests would cover first navigation, switching/backtracking, session movement, and reconnect during bootstrap. Snapshot/event ordering for process inventories and shell-footer semantics would remain separate follow-ups.
Could the core team approve this first-slice approach, including nonblocking route rendering with readiness-gated selections?
opencodejr commentedon Oct 28, 2026
Thanks for the detailed write-up. I reproduced one of the listed bugs: prompt autocomplete reads default-location agents on a session at another location.
0.0.0-next-15234isn't on npm, so I tested 2.2.0:/tmp/opencode/location-b) has a config-only subagentzebra, and the server's agent list for B includes it./tmp/opencode/location-b.@lists B's files, but the only agents are@generaland@explorefrom the startup location. There is no@zebra.It still happens on the latest
dev(77a0fd6f). The likely cause is thatautocomplete.tsxcallsdata.location.agent.list()/reference.list()without a location, so they fall back todefaultLocation(). The script and recording below show it.I didn't cover the reconnect, shell-count, permission or snapshot-race scenarios here.
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.