Cross-directory selectors subscribe to the narrow child-store field they aggregate. Session aggregation listens to `state.session`; per-session status listens only to that session's `state.session_status` entry. Unrelated streaming events such as `message.part.delta` must not trigger global session/status scans.
Imperative cross-directory session lookups use the cached ID index from `getAllSyncSessionMap()`. The index is rebuilt only when a child store's `state.session` reference changes; permission lineage checks must reuse it instead of rebuilding a full session map per call.
VS Code does not run the server permission-auto-accept runtime. The extension host persists and broadcasts authoritative policy, while its foreground UI runtime resolves missing child-session lineage through the OpenCode API before deciding whether to suppress and answer a `permission.asked` event. Enabling the policy and reconnect/bootstrap both reconcile pending requests in the session directory, including requests inherited by child sessions. Unknown lineage and exhausted reply retries fail closed and leave the request available for manual action. With every OpenChamber webview closed or suspended no responder runs; this is an intentional VS Code limitation. Other runtimes remain fully server-owned.
When creating a draft in `handleDirectoryEvent`, **only clone the state fields the event will mutate**. Never spread all fields eagerly.
```typescript
// WRONG — clones everything, breaks referential equality for all subscribers
constdraft={
...current,
session:[...current.session],
message:{...current.message},
part:{...current.part},
permission:{...current.permission},
// ...
}
// RIGHT — only clone what this event type touches
constdraft={...current}
switch(event.type){
case"message.part.delta":
draft.part={...current.part}
break
}
```
## Why this matters
Zustand skips re-renders when a selector returns the same reference (`Object.is`). If you spread `session: [...current.session]` but the event only modifies `part`, the `session` array gets a new reference. Every component using `useSessions()` re-renders for nothing.
During streaming, `message.part.delta` fires ~60 times/sec. Eagerly cloning all fields caused every subscriber in the entire app to re-render 60/sec — a 10x overhead. Targeted cloning reduced MessageList renders from ~1972 to ~296 per session.
## Event → field mapping
Keep this in sync with `handleDirectoryEvent` in `sync-context.tsx`:
1. Add the case to the event reducer (`event-reducer.ts`)
2. Add a corresponding case to the switch in `handleDirectoryEvent` (`sync-context.tsx`) that clones **only** the fields your reducer writes to
3. If your event fires frequently (more than a few times per second), verify that unrelated components don't re-render — check with the stream perf counters
## Selector hygiene
Select leaf values, not containers:
```typescript
// WRONG — returns entire Map/object, new reference on any mutation
useDirectorySync((s)=>s.permission)
// RIGHT — returns the value for one key, stable unless that key changes
Same applies to `useStreamingStore` — select `.get(key)` not the Map itself.
## Store splitting pattern
### Why split
A single Zustand store with N properties means every subscriber's selector re-evaluates on every state change — even if the change is unrelated to what that subscriber reads. During streaming, `sessionMemoryState` updates ~60/sec. Before the split, all 68+ `useSessionUIStore` subscribers re-evaluated on each update. After splitting into focused stores, only `useViewportStore` subscribers (2-3 components) re-evaluate.
The optimization multiplies with targeted event cloning: fewer new references per event × fewer subscribers per store = dramatically less work per SSE frame.
1.**Never add to `session-ui-store`** unless it's session selection, draft lifecycle, or abort state
2.**Group by change frequency** — state that changes during streaming (viewport, memory) must not live with state that changes on user action (selections, input)
3.**Group by subscriber set** — if only 2 components read a value, it should be in a store that only those 2 components subscribe to
4.**Prefer a new store over growing an existing one** if the new state has different subscribers or change frequency
5.**Cross-store reads use `.getState()`** — actions in one store that need to read another store call `useOtherStore.getState()` (imperative, no subscription)
### Anti-patterns
```typescript
// WRONG — stuffing unrelated state into one store
constuseEverythingStore=create(()=>({
voiceMode:"idle",
scrollAnchor: 0,
selectedModel: null,
pendingInput:"",
// 20 more fields...
}))
// RIGHT — separate stores by concern + change frequency