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
+87 -125
View File
@@ -10,14 +10,11 @@ import { useInputStore } from "./input-store"
import type { ChildStoreManager } from "./child-store"
import { computeSubtreeIds } from "./scoped-blocking-requests"
import { opencodeClient } from "@/lib/opencode/client"
import { mergeSessionDirectoryMetadata, useGlobalSessionsStore } from "@/stores/useGlobalSessionsStore"
import { mergeSessionDirectoryMetadata, resolveGlobalSessionDirectory, useGlobalSessionsStore } from "@/stores/useGlobalSessionsStore"
import { useConfigStore } from "@/stores/useConfigStore"
import { registerSessionDirectory } from "./sync-refs"
import { isSyntheticPart } from "@/lib/messages/synthetic"
import { getSessionMaterializationStatus, materializeSessionSnapshots } from "./materialization"
import { retry } from "./retry"
import { isVSCodeRuntime } from "@/lib/desktop"
import { isMobileSurfaceRuntime } from "@/lib/runtimeSurface"
import { materializeSessionSnapshots } from "./materialization"
import { stripMessageDiffSnapshots, stripSessionDiffSnapshots } from "./sanitize"
import { sessionEvents } from "@/lib/sessionEvents"
import {
@@ -28,6 +25,9 @@ import {
type SessionMetadataRecord,
} from "@/lib/sessionReviewMetadata"
import { withContextObligatoryMessage, type ContextObligatoryMessage } from "@/lib/contextObligatoryMessages"
import { getImperativeSessionMessageLoader } from "./session-message-loader"
import { cleanupPersistedSessionState } from "./session-deletion-cleanup"
import { getRuntimeKey } from "@/lib/runtime-switch"
const MESSAGE_REFETCH_LIMIT = 100
const SEND_CONFIRMATION_REFETCH_LIMIT = 30
@@ -49,6 +49,37 @@ let _optimisticAdd: ((input: OptimisticAddInput) => void) | null = null
let _optimisticRemove: ((input: OptimisticRemoveInput) => void) | null = null
let _optimisticConfirm: ((input: OptimisticConfirmInput) => void) | null = null
function sessionMutationPatch(
state: ReturnType<DirectoryStoreApi["getState"]>,
sessionId: string,
deleted: boolean,
) {
const revision = (state.sessionRevision ?? 0) + 1
const sessionEventRevision = { ...(state.sessionEventRevision ?? {}) }
const sessionDeletedRevision = { ...(state.sessionDeletedRevision ?? {}) }
if (deleted) {
sessionDeletedRevision[sessionId] = revision
delete sessionEventRevision[sessionId]
} else {
sessionEventRevision[sessionId] = revision
delete sessionDeletedRevision[sessionId]
}
return {
sessionListSource: "live" as const,
sessionRevision: revision,
sessionEventRevision,
sessionDeletedRevision,
}
}
function invalidateSessionLoads(sessionId: string, directories: Iterable<string | null | undefined>): void {
const loader = getImperativeSessionMessageLoader()
if (!loader) return
for (const directory of new Set(directories)) {
if (directory) loader.invalidateSession({ directory, sessionID: sessionId })
}
}
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
type SdkResult<T> = {
@@ -263,6 +294,7 @@ function reconcileSessionMove(
question: questions.source,
message: messages.source,
part: parts.source,
...sessionMutationPatch(sourceState, session.id, true),
})
destinationStore.setState({
session: destinationSessions,
@@ -276,6 +308,7 @@ function reconcileSessionMove(
question: questions.destination,
message: messages.destination,
part: parts.destination,
...sessionMutationPatch(destinationState, session.id, false),
})
return movedSession
@@ -294,6 +327,8 @@ export async function moveSessionToDirectory(
})
assertSdkSuccess(result, "Move session")
invalidateSessionLoads(session.id, [sourceDirectory, destinationDirectory])
const moved = reconcileSessionMove(session, sourceDirectory, destinationDirectory)
registerSessionDirectory(session.id, destinationDirectory)
@@ -368,7 +403,6 @@ export async function waitForConnectionOrThrow(): Promise<void> {
type SessionListSnapshot = {
directory: string
sessions: Session[]
}
type DirectoryStoreApi = ReturnType<ChildStoreManager["ensureChild"]>
@@ -378,14 +412,11 @@ function getGlobalSessionSnapshot(sessionId: string): Session | null {
return [...global.activeSessions, ...global.archivedSessions].find((session) => session.id === sessionId) ?? null
}
function restoreGlobalSessionSnapshot(session: Session | null): void {
if (!session) return
useGlobalSessionsStore.getState().upsertSession(session)
}
function getSessionDirectory(sessionId: string): string | undefined {
const globalSession = getGlobalSessionSnapshot(sessionId)
return findSessionDirectoryInChildStores(sessionId)
|| useSessionUIStore.getState().getDirectoryForSession(sessionId)
|| (globalSession ? resolveGlobalSessionDirectory(globalSession) ?? undefined : undefined)
|| dir()
}
@@ -622,8 +653,8 @@ async function cleanupReviewMetadataBeforeDelete(sessionId: string, directory?:
}
}
/** Optimistically remove a session from every live child store that has it. */
function optimisticRemoveSession(sessionId: string, preferredDirectory?: string): SessionListSnapshot[] {
/** Remove a server-confirmed session from every live child store that has it. */
function removeSessionFromLiveStores(sessionId: string, preferredDirectory?: string): SessionListSnapshot[] {
if (!_childStores) return []
const snapshots: SessionListSnapshot[] = []
@@ -648,45 +679,46 @@ function optimisticRemoveSession(sessionId: string, preferredDirectory?: string)
if (!current.session.some((session) => session.id === sessionId)) {
continue
}
snapshots.push({ directory, sessions: current.session })
store.setState({ session: current.session.filter((session) => session.id !== sessionId) })
snapshots.push({ directory })
store.setState({
session: current.session.filter((session) => session.id !== sessionId),
...sessionMutationPatch(current, sessionId, true),
})
}
return snapshots
}
function restoreSessionListSnapshots(snapshots: SessionListSnapshot[]): void {
if (!_childStores) return
for (const snapshot of snapshots) {
const store = _childStores.children.get(snapshot.directory)
if (!store) continue
store.setState({ session: snapshot.sessions })
}
}
function cleanupSessionWorktreeMetadata(sessionId: string): void {
useSessionUIStore.getState().setWorktreeMetadata(sessionId, null)
}
function finalizeConfirmedSessionDeletion(sessionId: string, sessionDirectory?: string): void {
const snapshots = removeSessionFromLiveStores(sessionId, sessionDirectory)
invalidateSessionLoads(sessionId, [...snapshots.map((snapshot) => snapshot.directory), sessionDirectory])
useGlobalSessionsStore.getState().removeSessions([sessionId])
const ui = useSessionUIStore.getState()
if (ui.currentSessionId === sessionId) ui.setCurrentSession(null)
cleanupSessionWorktreeMetadata(sessionId)
if (sessionDirectory) {
cleanupPersistedSessionState({
runtimeKey: getRuntimeKey(),
directory: sessionDirectory,
sessionId,
})
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function deleteSession(sessionId: string, _options?: Record<string, unknown>): Promise<boolean> {
const sessionDirectory = getSessionDirectory(sessionId)
const snapshots = optimisticRemoveSession(sessionId, sessionDirectory)
const globalSnapshot = getGlobalSessionSnapshot(sessionId)
useGlobalSessionsStore.getState().removeSessions([sessionId])
const ui = useSessionUIStore.getState()
if (ui.currentSessionId === sessionId) {
ui.setCurrentSession(null)
}
try {
await cleanupReviewMetadataBeforeDelete(sessionId, sessionDirectory)
const deleted = await opencodeClient.deleteSession(sessionId, sessionDirectory)
if (deleted !== true) {
throw new Error("session.delete failed: server did not confirm deletion")
}
useGlobalSessionsStore.getState().removeSessions([sessionId])
cleanupSessionWorktreeMetadata(sessionId)
finalizeConfirmedSessionDeletion(sessionId, sessionDirectory)
return true
} catch (error) {
console.error("[session-actions] deleteSession failed", error)
@@ -694,66 +726,50 @@ export async function deleteSession(sessionId: string, _options?: Record<string,
// Subsequent delete attempts for those children return 404; treat as
// success since the session was already deleted by the cascade.
if ((error as { status?: number })?.status === 404) {
cleanupSessionWorktreeMetadata(sessionId)
finalizeConfirmedSessionDeletion(sessionId, sessionDirectory)
return true
}
restoreSessionListSnapshots(snapshots)
restoreGlobalSessionSnapshot(globalSnapshot)
return false
}
}
/** Delete a session specifying which directory it lives in. Used by agent groups for cross-directory deletes. */
export async function deleteSessionInDirectory(sessionId: string, directory: string): Promise<boolean> {
if (!_childStores) return false
const snapshots = optimisticRemoveSession(sessionId, directory)
const globalSnapshot = getGlobalSessionSnapshot(sessionId)
useGlobalSessionsStore.getState().removeSessions([sessionId])
const ui = useSessionUIStore.getState()
if (ui.currentSessionId === sessionId) ui.setCurrentSession(null)
try {
await cleanupReviewMetadataBeforeDelete(sessionId, directory)
const deleted = await opencodeClient.deleteSession(sessionId, directory)
if (deleted !== true) {
throw new Error("session.delete failed: server did not confirm deletion")
}
useGlobalSessionsStore.getState().removeSessions([sessionId])
cleanupSessionWorktreeMetadata(sessionId)
finalizeConfirmedSessionDeletion(sessionId, directory)
return true
} catch (error) {
console.error("[session-actions] deleteSessionInDirectory failed", error)
if ((error as { status?: number })?.status === 404) {
cleanupSessionWorktreeMetadata(sessionId)
finalizeConfirmedSessionDeletion(sessionId, directory)
return true
}
restoreSessionListSnapshots(snapshots)
restoreGlobalSessionSnapshot(globalSnapshot)
return false
}
}
export async function archiveSession(sessionId: string): Promise<boolean> {
const sessionDirectory = getSessionDirectory(sessionId)
const snapshots = optimisticRemoveSession(sessionId, sessionDirectory)
const globalSnapshot = getGlobalSessionSnapshot(sessionId)
const archivedAt = Date.now()
useGlobalSessionsStore.getState().archiveSessions([sessionId], archivedAt)
const ui = useSessionUIStore.getState()
if (ui.currentSessionId === sessionId) {
ui.setCurrentSession(null)
}
try {
await cleanupReviewMetadataBeforeDelete(sessionId, sessionDirectory)
const archived = await opencodeClient.updateSession(sessionId, { time: { archived: archivedAt } }, sessionDirectory)
if (!archived) {
throw new Error("session.update failed: server did not return the archived session")
}
const snapshots = removeSessionFromLiveStores(sessionId, sessionDirectory)
invalidateSessionLoads(sessionId, [...snapshots.map((snapshot) => snapshot.directory), sessionDirectory])
useGlobalSessionsStore.getState().upsertSession(archived)
const ui = useSessionUIStore.getState()
if (ui.currentSessionId === sessionId) ui.setCurrentSession(null)
return true
} catch (error) {
console.error("[session-actions] archiveSession failed", error)
restoreSessionListSnapshots(snapshots)
restoreGlobalSessionSnapshot(globalSnapshot)
return false
}
}
@@ -1288,6 +1304,16 @@ export async function revertToMessage(sessionId: string, messageId: string): Pro
export async function refetchSessionMessages(sessionId: string): Promise<void> {
const { store, directory } = dirStoreForSession(sessionId)
const loader = getImperativeSessionMessageLoader()
if (loader && directory) {
await loader.refreshTail({ directory, sessionID: sessionId }, MESSAGE_REFETCH_LIMIT)
const snapshot = loader.getSnapshot({ directory, sessionID: sessionId })
if (snapshot.status === "error") throw snapshot.error ?? new Error("Session message refresh failed")
return
}
// Actions can run in isolated tests before SyncProvider binds the shared
// loader. The application runtime always takes the shared path above.
const result = await sdk().session.messages({ sessionID: sessionId, directory, limit: MESSAGE_REFETCH_LIMIT })
const records = (assertSdkSuccess(result, "session.messages") ?? [])
.filter((record: { info?: { id?: string } }) => !!record?.info?.id)
@@ -1393,75 +1419,11 @@ export async function forkFromMessage(sessionId: string, messageId: string): Pro
restoreFilePartsToInput(fileParts)
}
// ---------------------------------------------------------------------------
// Imperative fetch path — starts message loading on the same tick as
// setCurrentSession, before the React commit cycle fires useEffect.
// ---------------------------------------------------------------------------
const FETCH_MESSAGES_LOADING = new Set<string>()
const DESKTOP_INITIAL_PAGE_SIZE = 50
const CONSTRAINED_INITIAL_PAGE_SIZE = 30
const getFetchPageSize = () => {
if (isVSCodeRuntime() || isMobileSurfaceRuntime()) return CONSTRAINED_INITIAL_PAGE_SIZE
return DESKTOP_INITIAL_PAGE_SIZE
}
export async function fetchMessagesForSession(sessionID: string, directory?: string | null): Promise<void> {
const resolvedDir = directory ?? dir()
if (!resolvedDir) return
const loadingKey = `${resolvedDir}:${sessionID}`
if (FETCH_MESSAGES_LOADING.has(loadingKey)) return
FETCH_MESSAGES_LOADING.add(loadingKey)
try {
const s = sdk()
const store = directory
? dirStoreForDirectory(directory)
: dirStore()
if (getSessionMaterializationStatus(store.getState(), sessionID).renderable) return
const result = await retry(async () => {
const response = await s.session.messages({
sessionID,
directory: resolvedDir,
limit: getFetchPageSize(),
})
return response
})
const records = (assertSdkSuccess(result, "session.messages") ?? [])
.filter((record: { info?: { id?: string } }) => !!record?.info?.id)
if (records.length === 0) return
// Staleness guard: a rapid session switch may have moved the user off this
// session while the fetch was in flight. Skip the write so a slow fetch
// can't repopulate (and un-evict) a session already navigated away from.
if (useSessionUIStore.getState().currentSessionId !== sessionID) return
const latestState = store.getState()
const latestStatus = getSessionMaterializationStatus(latestState, sessionID)
if (latestStatus.renderable && (latestState.message[sessionID]?.length ?? 0) >= records.length) return
store.setState((state) => {
const materialized = materializeSessionSnapshots(
state,
sessionID,
records.map((record: { info: Message; parts?: Part[] }) => ({
info: stripMessageDiffSnapshots(record.info),
parts: record.parts ?? [],
})),
{ skipPartTypes: MESSAGE_REFETCH_SKIP_PARTS },
)
if (!materialized.messagesChanged && !materialized.partsChanged) return state
return { message: materialized.message, part: materialized.part }
})
} catch {
// Transient failure — the reactive path in ChatContainer will retry
} finally {
FETCH_MESSAGES_LOADING.delete(loadingKey)
}
await getImperativeSessionMessageLoader()?.ensure(
{ directory: resolvedDir, sessionID },
{ reason: "navigation" },
)
}