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:
Bohdan Triapitsyn
2026-07-21 20:52:20 +03:00
committed by GitHub
parent 485efc7117
commit 85400459e9
197 changed files with 10835 additions and 3400 deletions
@@ -18,6 +18,7 @@ import { canUseElectronDesktopIPC, invokeDesktop, isVSCodeRuntime } from '@/lib/
import { toast } from '@/components/ui';
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { isSessionPinned, type SessionPinnedTarget } from '@/stores/useSessionPinnedStore';
import { Icon } from "@/components/icon/Icon";
import { buildExportFilename, downloadAsMarkdown, formatSessionAsMarkdown, getExportRevealLabelKey, revealExportedMarkdown, saveAsMarkdownDesktop } from '@/lib/exportSession';
import type { ChildSessionExport } from '@/lib/exportSession';
@@ -25,7 +26,7 @@ import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSession
import { useSync } from '@/sync/use-sync';
import { useViewportStore, viewportSessionKey } from '@/sync/viewport-store';
import { DraggableSessionRow } from './sessionFolderDnd';
import { nodeContainsSessionId } from './sessionNodeItemUtils';
import { nodeContainsSessionId, nodeHasPinnedMembershipChange } from './sessionNodeItemUtils';
import type { SessionNodeChildRenderExtras, SessionNodeRenderExtras } from './sessionNodeItemUtils';
import type { SessionNode } from './types';
import { formatSessionCompactDateLabel, formatSessionDateLabel, normalizePath, renderHighlightedText } from './utils';
@@ -43,6 +44,8 @@ import { MultiRunFusionDialog } from '@/components/multirun/MultiRunFusionDialog
import { FusionIcon } from '@/components/icons/FusionIcon';
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
import { startSessionTreeWorktreeMove, useIsSessionWorktreeMovePending } from '@/lib/worktrees/sessionWorktreeMove';
import { streamPerfCount } from '@/stores/utils/streamDebug';
import { useSessionUIStore } from '@/sync/session-ui-store';
type Folder = { id: string; name: string; sessionIds: string[] };
@@ -57,7 +60,6 @@ type Props = {
groupDirectory?: string | null;
projectId?: string | null;
archivedBucket?: boolean;
currentSessionId: string | null;
pinnedSessionIds: Set<string>;
expandedParents: Set<string>;
hasSessionSearchQuery: boolean;
@@ -70,9 +72,9 @@ type Props = {
handleSaveEdit: (titleOverride?: string) => void;
handleCancelEdit: () => void;
toggleParent: (expansionKey: string) => void;
handleSessionSelect: (sessionId: string, sessionDirectory: string | null, projectId?: string | null) => void;
handleSessionSelect: (sessionId: string, sessionDirectory: string | null) => void;
handleSessionDoubleClick: (sessionId: string, sessionTitle: string) => void;
togglePinnedSession: (sessionId: string) => void;
togglePinnedSession: (target: SessionPinnedTarget) => void;
handleShareSession: (session: Session) => void;
copiedSessionId: string | null;
handleCopyShareUrl: (url: string, sessionId: string) => void;
@@ -101,16 +103,9 @@ type Props = {
) => React.ReactNode;
secondaryMeta?: SecondaryMeta | null;
renderContext?: 'project' | 'recent';
/**
* Precomputed set of session IDs whose subtree contains the current
* active session. Computed once per SessionGroupSection render (when
* currentSessionId changes) instead of being recomputed in every row's
* React.memo comparator.
*/
subtreeContainsActive: Set<string>;
/**
* Precomputed set of session IDs whose subtree contains the session
* currently being edited. Same rationale as subtreeContainsActive.
* currently being edited. Precomputed once per group render.
*/
subtreeContainsEditing: Set<string>;
/**
@@ -132,15 +127,52 @@ type Props = {
* to fetch the right key for each child it produces.
*/
childRenderExtrasFor?: (child: SessionNode) => SessionNodeChildRenderExtras;
/**
* Batched index of live session objects keyed by id. The previous
* implementation called `useSession(session.id)` per row, which used
* `findLiveSession` to iterate every child-store on every SSE event.
* With M visible rows that's M×child-stores per event; the batched
* map turns it into a single Map.get per row. The parent falls back
* to `useSession` only when this map returns undefined.
*/
liveSessionById: Map<string, Session>;
};
const cancelScrollAnchorByContainer = new WeakMap<HTMLElement, () => void>();
const holdSessionRowPosition = (target: HTMLElement): void => {
if (typeof window === 'undefined') return;
const row = target.closest<HTMLElement>('[data-session-row]');
const container = row?.closest<HTMLElement>('.overlay-scrollbar-container');
if (!row || !container) return;
cancelScrollAnchorByContainer.get(container)?.();
const initialTop = row.getBoundingClientRect().top;
let remainingFrames = 3;
let cancelled = false;
let frameId: number | null = null;
const cancel = () => {
cancelled = true;
if (frameId !== null) window.cancelAnimationFrame(frameId);
frameId = null;
cancelScrollAnchorByContainer.delete(container);
container.removeEventListener('wheel', cancel);
container.removeEventListener('touchstart', cancel);
};
const restore = () => {
if (cancelled || !row.isConnected || !container.isConnected) {
cancel();
return;
}
const delta = row.getBoundingClientRect().top - initialTop;
if (Math.abs(delta) > 0.5) {
container.scrollTop += delta;
streamPerfCount('ui.sidebar.selection_scroll_anchor_adjustment');
}
remainingFrames -= 1;
if (remainingFrames <= 0) {
cancel();
return;
}
frameId = window.requestAnimationFrame(restore);
};
container.addEventListener('wheel', cancel, { passive: true });
container.addEventListener('touchstart', cancel, { passive: true });
cancelScrollAnchorByContainer.set(container, cancel);
frameId = window.requestAnimationFrame(restore);
};
type QuickSessionActionProps = {
@@ -206,6 +238,7 @@ const QuickSessionAction = React.memo(function QuickSessionAction({
});
function SessionNodeItemComponent(props: Props): React.ReactNode {
streamPerfCount('ui.sidebar_session_node.render');
const { t } = useI18n();
const {
node,
@@ -213,7 +246,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
groupDirectory,
projectId,
archivedBucket = false,
currentSessionId,
pinnedSessionIds,
expandedParents,
hasSessionSearchQuery,
@@ -248,11 +280,9 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
renderSessionNode,
secondaryMeta,
renderContext = 'project',
subtreeContainsActive,
subtreeContainsEditing,
menuOpenSessionId,
childRenderExtrasFor,
liveSessionById,
} = props;
const hasSecondaryProjectLabel = Boolean(secondaryMeta?.projectLabel);
const hasSecondaryBranchLabel = Boolean(secondaryMeta?.branchLabel);
@@ -307,25 +337,15 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const formRef = React.useRef<HTMLFormElement>(null);
const session = node.session;
// Batched live-session lookup. `liveSessionById` is built once per
// Sidebar render from the same `useAllLiveSessions` selector that
// `useSession` would have iterated per child-store, so a Map.get
// here is equivalent in observed state but O(1) per row instead of
// O(child-stores). Falls back to the row session when the live map
// hasn't seen this id yet (sub-render latency between when a session
// is created and when the SSE-driven aggregate picks it up).
const resolvedSession = liveSessionById.get(session.id) ?? session;
const resolvedSession = session;
const isActive = useSessionUIStore((state) => state.currentSessionId === session.id);
const sessionDirectory =
normalizePath((session as Session & { directory?: string | null }).directory ?? null)
?? normalizePath(groupDirectory ?? null);
// Archived rows are historical and never need live state, yet they point at
// dozens of (often deleted) worktrees — bootstrapping each from the sidebar
// triggers a pointless session-list fetch + 6×2s empty-retry storm on startup.
// Skip bootstrap for archived rows; the store ref is only read on-demand via
// getState() in the export handlers (never subscribed). Active rows keep
// bootstrapping so live cross-directory session/status still aggregates.
const directoryStore = useDirectoryStore(sessionDirectory ?? undefined, { bootstrap: !archivedBucket });
// Directory bootstrap is scheduled once at sidebar level. A row only needs
// the lightweight store reference for scoped state and export actions.
const directoryStore = useDirectoryStore(sessionDirectory ?? undefined, { bootstrap: false });
const sync = useSync();
const selectionModeEnabled = useSessionMultiSelectStore((state) => state.enabled);
@@ -368,7 +388,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
);
const sessionStatus = useGlobalSessionStatus(session.id);
const isMovingToWorktree = useIsSessionWorktreeMovePending(session.id);
const sessionPermissions = useSessionPermissions(session.id, sessionDirectory ?? undefined);
const sessionPermissions = useSessionPermissions(session.id, sessionDirectory ?? undefined, { bootstrap: false });
const sessionGoal = getSessionGoal(resolvedSession);
const sessionGoalGlyph = sessionGoal ? (
<span
@@ -379,10 +399,9 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<Icon name="target" className="h-3 w-3" style={{ color: sessionGoalStatusColor[sessionGoal.status] }} />
</span>
) : null;
const isActive = currentSessionId === session.id;
const sessionTitle = resolvedSession.title || t('sessions.sidebar.session.untitled');
const hasChildren = node.children.length > 0;
const isPinnedSession = pinnedSessionIds.has(session.id);
const isPinnedSession = isSessionPinned(pinnedSessionIds, sessionDirectory, session.id);
// Per-render-context expansion key: the same session can appear in both
// the project's root and the "Recent" list, and expanding one should not
// expand the other. Matches the format of menuInstanceKey.
@@ -409,7 +428,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
let skipped = 0;
for (const child of children) {
try {
await sync.ensureSessionRenderable(child.session.id);
await sync.ensureSessionRenderable(child.session.id, false, sessionDirectory ?? undefined);
const childRecords = buildSessionMessageRecordsSnapshot(directoryStore.getState(), child.session.id).list;
const childTitle = child.session.title || t('sessions.sidebar.session.export.untitledSubagent');
const childAgent = (child.session as Session & { agent?: string }).agent;
@@ -426,7 +445,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
}
}
return { children: results, skipped };
}, [collectNodeDescendantIds, directoryStore, sync, t]);
}, [collectNodeDescendantIds, directoryStore, sessionDirectory, sync, t]);
const showSkippedSubtasksWarning = React.useCallback((count: number) => {
if (count <= 0) return;
@@ -441,7 +460,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
return;
}
await sync.ensureSessionRenderable(session.id);
await sync.ensureSessionRenderable(session.id, false, sessionDirectory);
const records = buildSessionMessageRecordsSnapshot(directoryStore.getState(), session.id).list;
if (records.length === 0) {
@@ -775,7 +794,8 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
toggleRowSelected(session.id, sessionDirectory ?? null, collectNodeDescendantIds(node));
return;
}
handleSessionSelect(session.id, sessionDirectory, projectId);
if (event?.currentTarget) holdSessionRowPosition(event.currentTarget);
handleSessionSelect(session.id, sessionDirectory);
};
// The selection/active highlight covers the WHOLE row box (gutter, edge
@@ -832,7 +852,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
<Icon name="pencil-ai" className="mr-1 h-4 w-4" />
{t('sessions.sidebar.session.menu.rename')}
</Item>
<Item onClick={() => togglePinnedSession(session.id)} className="[&>svg]:mr-1">
<Item onClick={() => sessionDirectory && togglePinnedSession({ directory: sessionDirectory, sessionId: session.id })} className="[&>svg]:mr-1">
{isPinnedSession ? <Icon name="unpin" className="mr-1 h-4 w-4" /> : <Icon name="pushpin" className="mr-1 h-4 w-4" />}
{isPinnedSession ? t('sessions.sidebar.session.menu.unpin') : t('sessions.sidebar.session.menu.pin')}
</Item>
@@ -1267,7 +1287,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const childRenderExtras: SessionNodeChildRenderExtras = childRenderExtrasFor
? childRenderExtrasFor(child)
: {
subtreeContainsActive,
subtreeContainsEditing,
menuOpenSessionId,
nodeStructureKey: '',
@@ -1387,26 +1406,6 @@ const hasSetMembershipChangeInNode = (
return false;
};
const hasResolvedSessionChangeInNode = (
prevNode: SessionNode,
nextNode: SessionNode,
prevLiveSessionById: Map<string, Session>,
nextLiveSessionById: Map<string, Session>,
): boolean => {
if (prevNode.session.id !== nextNode.session.id) return true;
const sessionId = prevNode.session.id;
if ((prevLiveSessionById.get(sessionId) ?? prevNode.session) !== (nextLiveSessionById.get(sessionId) ?? nextNode.session)) {
return true;
}
if (prevNode.children.length !== nextNode.children.length) return true;
for (let i = 0; i < prevNode.children.length; i += 1) {
if (hasResolvedSessionChangeInNode(prevNode.children[i], nextNode.children[i], prevLiveSessionById, nextLiveSessionById)) {
return true;
}
}
return false;
};
const hasExpansionMembershipChange = (prev: Props, next: Props): boolean => {
if (prev.hasSessionSearchQuery || next.hasSessionSearchQuery) return false;
const prevBucketTag = prev.archivedBucket ? 'archived' : 'active';
@@ -1428,6 +1427,7 @@ const hasExpansionMembershipChange = (prev: Props, next: Props): boolean => {
const areSessionNodeItemPropsEqual = (prev: Props, next: Props): boolean => {
if (prev.node.session.id !== next.node.session.id) return false;
if (prev.node.session !== next.node.session) return false;
if (prev.depth !== next.depth) return false;
if (prev.groupDirectory !== next.groupDirectory) return false;
if (prev.projectId !== next.projectId) return false;
@@ -1442,13 +1442,15 @@ const areSessionNodeItemPropsEqual = (prev: Props, next: Props): boolean => {
if (getNodeSessionDirectory(prev.node) !== getNodeSessionDirectory(next.node)) return false;
if (!isSecondaryMetaEqual(prev.secondaryMeta, next.secondaryMeta)) return false;
if (prev.liveSessionById !== next.liveSessionById
&& hasResolvedSessionChangeInNode(prev.node, next.node, prev.liveSessionById, next.liveSessionById)) {
return false;
}
if (prev.pinnedSessionIds !== next.pinnedSessionIds
&& hasSetMembershipChangeInNode(prev.node, next.node, prev.pinnedSessionIds, next.pinnedSessionIds, (node) => node.session.id)) {
&& nodeHasPinnedMembershipChange(
prev.node,
next.node,
prev.pinnedSessionIds,
next.pinnedSessionIds,
prev.groupDirectory,
next.groupDirectory,
)) {
return false;
}
@@ -1456,14 +1458,6 @@ const areSessionNodeItemPropsEqual = (prev: Props, next: Props): boolean => {
return false;
}
if (prev.currentSessionId !== next.currentSessionId
&& (
subtreeContainsSession(prev, prev.currentSessionId, prev.subtreeContainsActive)
|| subtreeContainsSession(next, next.currentSessionId, next.subtreeContainsActive)
)) {
return false;
}
if (prev.editingId !== next.editingId
&& (
subtreeContainsSession(prev, prev.editingId, prev.subtreeContainsEditing)