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
@@ -122,25 +122,31 @@ export async function bootstrapDirectory(input: {
|
||||
sdk: OpencodeClient
|
||||
getState: () => State
|
||||
set: (patch: Partial<State>) => void
|
||||
isStale?: () => boolean
|
||||
global: {
|
||||
config: Record<string, unknown>
|
||||
projects: Project[]
|
||||
}
|
||||
loadSessions: (directory: string) => Promise<void> | void
|
||||
}) {
|
||||
}): Promise<"complete" | "failed" | "stale"> {
|
||||
const { directory, sdk, getState, set, global: g } = input
|
||||
const commit = (patch: Partial<State>): boolean => {
|
||||
if (input.isStale?.()) return false
|
||||
set(patch)
|
||||
return true
|
||||
}
|
||||
const state = getState()
|
||||
const loading = state.status !== "complete"
|
||||
|
||||
// Seed from global state while we fetch directory-specific data
|
||||
const seededProject = projectID(directory, g.projects)
|
||||
if (seededProject) set({ project: seededProject })
|
||||
if (seededProject) commit({ project: seededProject })
|
||||
if (Object.keys(state.config ?? {}).length === 0 && Object.keys(g.config ?? {}).length > 0) {
|
||||
const seededConfig = g.config as State["config"]
|
||||
set({ config: seededConfig })
|
||||
emitSyncConfigChanged(directory, seededConfig)
|
||||
if (commit({ config: seededConfig })) emitSyncConfigChanged(directory, seededConfig)
|
||||
}
|
||||
if (loading) set({ status: "partial" })
|
||||
if (loading) commit({ status: "partial" })
|
||||
if (input.isStale?.()) return "stale"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 1: Critical path — block until these resolve so the UI can render.
|
||||
@@ -149,23 +155,24 @@ export async function bootstrapDirectory(input: {
|
||||
const phase1Results = await Promise.allSettled([
|
||||
seededProject
|
||||
? Promise.resolve()
|
||||
: retry(() => sdk.project.current().then((x) => set({ project: unwrap(x, "project.current").id }))),
|
||||
: retry(() => sdk.project.current().then((x) => commit({ project: unwrap(x, "project.current").id }))),
|
||||
retry(() => sdk.config.get().then((x) => {
|
||||
const config = unwrap(x, "config.get")
|
||||
set({ config })
|
||||
emitSyncConfigChanged(directory, config)
|
||||
if (commit({ config })) emitSyncConfigChanged(directory, config)
|
||||
})),
|
||||
retry(() =>
|
||||
sdk.path.get().then((x) => {
|
||||
const data = unwrap(x, "path.get")
|
||||
set({ path: data })
|
||||
commit({ path: data })
|
||||
const next = projectID(data?.directory ?? directory, g.projects)
|
||||
if (next) set({ project: next })
|
||||
if (next) commit({ project: next })
|
||||
}),
|
||||
),
|
||||
retry(() => sdk.session.status().then((x) => set({ session_status: unwrap(x, "session.status") }))),
|
||||
retry(() => sdk.session.status().then((x) => commit({ session_status: unwrap(x, "session.status") }))),
|
||||
])
|
||||
|
||||
if (input.isStale?.()) return "stale"
|
||||
|
||||
const phase1Errors = phase1Results
|
||||
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
|
||||
.map((r) => r.reason)
|
||||
@@ -184,27 +191,27 @@ export async function bootstrapDirectory(input: {
|
||||
|
||||
if (phase1Errors.length === phase1Results.length || pathFailedWithoutProject) {
|
||||
console.error(`[bootstrap] directory bootstrap failed for ${directory}`, phase1Errors[0])
|
||||
return
|
||||
return "failed"
|
||||
}
|
||||
|
||||
// Mark ready after critical data arrives so the UI can paint.
|
||||
if (loading) set({ status: "complete" })
|
||||
if (loading) commit({ status: "complete" })
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 2: Deferrable — fetch after first paint without blocking.
|
||||
// These enrich the UI but aren't required for basic functionality.
|
||||
// ---------------------------------------------------------------------------
|
||||
void Promise.allSettled([
|
||||
retry(() => sdk.command.list().then((x) => set({ command: unwrap(x, "command.list") }))),
|
||||
retry(() => sdk.mcp.status().then((x) => set({ mcp: unwrap(x, "mcp.status") }))),
|
||||
retry(() => sdk.lsp.status().then((x) => set({ lsp: unwrap(x, "lsp.status") }))),
|
||||
const runDeferredPhase = () => Promise.allSettled([
|
||||
retry(() => sdk.command.list().then((x) => commit({ command: unwrap(x, "command.list") }))),
|
||||
retry(() => sdk.mcp.status().then((x) => commit({ mcp: unwrap(x, "mcp.status") }))),
|
||||
retry(() => sdk.lsp.status().then((x) => commit({ lsp: unwrap(x, "lsp.status") }))),
|
||||
retry(() =>
|
||||
sdk.vcs.get().then((x) => {
|
||||
const current = getState()
|
||||
if (x.error) {
|
||||
throw new Error(`vcs.get failed: ${String(x.error)}`)
|
||||
}
|
||||
set({ vcs: x.data ?? current.vcs })
|
||||
commit({ vcs: x.data ?? current.vcs })
|
||||
}),
|
||||
),
|
||||
retry(async () => {
|
||||
@@ -236,7 +243,7 @@ export async function bootstrapDirectory(input: {
|
||||
if (currentSignature !== beforeSignature) continue
|
||||
delete merged[sessionID]
|
||||
}
|
||||
set({ question: merged })
|
||||
commit({ question: merged })
|
||||
}),
|
||||
retry(async () => {
|
||||
const before = getState()
|
||||
@@ -267,7 +274,7 @@ export async function bootstrapDirectory(input: {
|
||||
if (currentSignature !== beforeSignature) continue
|
||||
delete merged[sessionID]
|
||||
}
|
||||
set({ permission: merged })
|
||||
commit({ permission: merged })
|
||||
}),
|
||||
]).then((results) => {
|
||||
const errors = results
|
||||
@@ -279,9 +286,19 @@ export async function bootstrapDirectory(input: {
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 3: Lazy — session list can be large; don't block on it.
|
||||
// Phase 3: Authoritative session list. Keep this scheduler-owned so bounded
|
||||
// bootstrap concurrency also bounds list pagination, but do not hold the slot
|
||||
// for the deferrable enrichment phase above.
|
||||
// ---------------------------------------------------------------------------
|
||||
void Promise.resolve(input.loadSessions(directory)).catch((err) => {
|
||||
console.error(`[bootstrap] session load failed for ${directory}`, err)
|
||||
})
|
||||
const sessionsResult = await Promise.allSettled([Promise.resolve(input.loadSessions(directory))])
|
||||
if (input.isStale?.()) return "stale"
|
||||
const sessionLoad = sessionsResult[0]
|
||||
setTimeout(() => {
|
||||
if (!input.isStale?.()) void runDeferredPhase()
|
||||
}, 0)
|
||||
if (sessionLoad?.status === "rejected") {
|
||||
console.error(`[bootstrap] session load failed for ${directory}`, sessionLoad.reason)
|
||||
return "failed"
|
||||
}
|
||||
return "complete"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user