perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing cache, synchronization, and persistence correctness across runtimes, projects, directories, and worktrees. - prioritize selected and visible sessions during bootstrap and defer non-critical enrichment work - reduce redundant message loading, event processing, store publication, and hidden sidebar work - prevent stale session and message requests from overwriting newer authoritative state - preserve existing data when authoritative fetches fail instead of treating failures as successful empty responses - scope session materialization, messages, drafts, queues, todos, pins, permissions, folders, tabs, Git state, and pull request data by runtime and directory identity - harden runtime switching, reconnect, cleanup, mutation reconciliation, and persisted-state ordering - preserve live subagent Task linkage when metadata arrives after an older message request or while streaming parts are suspended - coalesce overlapping tail refreshes without losing newer refresh demand - improve cold-session loading by moving deferrable work out of the critical bootstrap path - isolate URL authentication, mobile credentials, native secrets, and other runtime-owned state across endpoint changes - bound long-lived caches and remove avoidable allocations from event and rendering hot paths - limit virtualization to archive collections where it improves rendering without disrupting active sidebar layout - stabilize session folders, pin ordering, expanded state, and persisted sidebar behavior - open skill files through the same secure editor and outside-workspace grant flow used by file navigation, including worktree sessions - expand regression coverage for stale completions, runtime collisions, reconnect behavior, persistence races, authoritative empty results, and subagent refresh ordering - document the updated synchronization, cache ownership, performance, and runtime-isolation invariants
This commit is contained in:
committed by
GitHub
parent
485efc7117
commit
85400459e9
@@ -1,13 +1,10 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Event } from '@opencode-ai/sdk/v2/client';
|
||||
import type { Event, SessionStatus } from '@opencode-ai/sdk/v2/client';
|
||||
import { normalizeProjectPath } from '@/lib/projectResolution';
|
||||
|
||||
// Live busy/retry status for sessions in directories WITHOUT a synced child
|
||||
// store. The global event stream (`/api/global/event/ws`) carries status
|
||||
// events for every directory, but the sync dispatcher can only apply them to
|
||||
// an existing child store — events for unopened directories used to be
|
||||
// dropped. They land here instead, so cross-project consumers (the macOS tray)
|
||||
// see live status for all sessions, not just the synced ones.
|
||||
// Shared live busy/retry index for every directory. Global events update it
|
||||
// incrementally and authoritative directory snapshots reconcile it, so each
|
||||
// sidebar row can subscribe to one leaf instead of every child store.
|
||||
//
|
||||
// Only non-idle entries are kept; absence means idle. Entries carry their
|
||||
// directory so a polled per-directory snapshot can authoritatively replace
|
||||
@@ -15,7 +12,7 @@ import { normalizeProjectPath } from '@/lib/projectResolution';
|
||||
|
||||
type ActiveStatusType = 'busy' | 'retry';
|
||||
|
||||
type GlobalSessionStatusEntry = { status: ActiveStatusType; directory: string };
|
||||
type GlobalSessionStatusEntry = { status: SessionStatus; directory: string };
|
||||
|
||||
type GlobalSessionStatusState = {
|
||||
statusById: Map<string, GlobalSessionStatusEntry>;
|
||||
@@ -34,16 +31,17 @@ const normalizeStatusType = (type: unknown): ActiveStatusType | 'idle' =>
|
||||
const normalizeDirectory = (directory: string): string =>
|
||||
normalizeProjectPath(directory) ?? directory;
|
||||
|
||||
const setStatus = (sessionId: string, directory: string, status: ActiveStatusType | 'idle'): void => {
|
||||
const setStatus = (sessionId: string, directory: string, status: SessionStatus | { type: 'idle' }): void => {
|
||||
useGlobalSessionStatusStore.setState((state) => {
|
||||
const current = state.statusById.get(sessionId);
|
||||
if (status === 'idle') {
|
||||
if (status.type === 'idle') {
|
||||
if (!current) return state;
|
||||
const next = new Map(state.statusById);
|
||||
next.delete(sessionId);
|
||||
return { statusById: next };
|
||||
}
|
||||
if (current && current.status === status && current.directory === directory) return state;
|
||||
if (current && current.status.type === status.type && current.directory === directory
|
||||
&& JSON.stringify(current.status) === JSON.stringify(status)) return state;
|
||||
const next = new Map(state.statusById);
|
||||
next.set(sessionId, { status, directory });
|
||||
return { statusById: next };
|
||||
@@ -58,14 +56,19 @@ export const applyGlobalSessionStatusEvent = (directory: string, payload: Event)
|
||||
case 'session.status': {
|
||||
const props = payload.properties as { sessionID?: string; status?: { type?: string } } | undefined;
|
||||
if (typeof props?.sessionID !== 'string' || !props.sessionID) return;
|
||||
setStatus(props.sessionID, normalizeDirectory(directory), normalizeStatusType(props.status?.type));
|
||||
const type = normalizeStatusType(props.status?.type);
|
||||
setStatus(
|
||||
props.sessionID,
|
||||
normalizeDirectory(directory),
|
||||
type === 'idle' ? { type: 'idle' } : { ...(props.status ?? {}), type } as SessionStatus,
|
||||
);
|
||||
return;
|
||||
}
|
||||
case 'session.idle':
|
||||
case 'session.error': {
|
||||
const props = payload.properties as { sessionID?: string } | undefined;
|
||||
if (typeof props?.sessionID === 'string' && props.sessionID) {
|
||||
setStatus(props.sessionID, normalizeDirectory(directory), 'idle');
|
||||
setStatus(props.sessionID, normalizeDirectory(directory), { type: 'idle' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -101,14 +104,16 @@ export const applyGlobalSessionStatusSnapshot = (
|
||||
const type = normalizeStatusType(status?.type);
|
||||
const current = next.get(sessionId);
|
||||
if (type === 'idle') {
|
||||
if (current && current.directory === directory) {
|
||||
if (current && (current.directory === directory || known.has(sessionId))) {
|
||||
next.delete(sessionId);
|
||||
changed = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!current || current.status !== type || current.directory !== directory) {
|
||||
next.set(sessionId, { status: type, directory });
|
||||
const normalizedStatus = { ...status, type } as SessionStatus;
|
||||
if (!current || current.status.type !== type || current.directory !== directory
|
||||
|| JSON.stringify(current.status) !== JSON.stringify(normalizedStatus)) {
|
||||
next.set(sessionId, { status: normalizedStatus, directory });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user