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,6 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
|
||||
import { getSessionMaterializationStatus, materializeSessionSnapshots } from "../materialization"
|
||||
import {
|
||||
getSessionMaterializationRequestKey,
|
||||
getSessionMaterializationStatus,
|
||||
isSessionMaterializationStillNeeded,
|
||||
materializeSessionSnapshots,
|
||||
} from "../materialization"
|
||||
|
||||
function message(id: string, sessionID = "ses_1"): Message {
|
||||
return { id, sessionID, role: "assistant", time: { created: 1 } } as Message
|
||||
@@ -14,6 +19,13 @@ function part(id: string, messageID: string, type = "text", text = id): Part {
|
||||
return { id, messageID, sessionID: "ses_1", type, text } as Part
|
||||
}
|
||||
|
||||
describe("getSessionMaterializationRequestKey", () => {
|
||||
test("isolates the same directory and session identity across runtimes", () => {
|
||||
expect(getSessionMaterializationRequestKey("runtime-a", "/repo", "ses_1"))
|
||||
.not.toBe(getSessionMaterializationRequestKey("runtime-b", "/repo", "ses_1"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("materializeSessionSnapshots", () => {
|
||||
test("marks an empty successful page as materialized", () => {
|
||||
const result = materializeSessionSnapshots(
|
||||
@@ -182,3 +194,47 @@ describe("getSessionMaterializationStatus", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("isSessionMaterializationStillNeeded", () => {
|
||||
test("skips empty-assistant recovery after a part bucket arrives", () => {
|
||||
const state = { message: { ses_1: [message("msg_1")] }, part: { msg_1: [part("prt_1", "msg_1")] } }
|
||||
|
||||
expect(isSessionMaterializationStillNeeded(state, "ses_1", {
|
||||
reason: "empty-assistant-message",
|
||||
messageID: "msg_1",
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test("treats an explicit empty part bucket as authoritative", () => {
|
||||
const state = { message: { ses_1: [message("msg_1")] }, part: { msg_1: [] } }
|
||||
|
||||
expect(isSessionMaterializationStillNeeded(state, "ses_1", {
|
||||
reason: "empty-assistant-message",
|
||||
messageID: "msg_1",
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test("skips missing-message and missing-part recovery after ordered events repair state", () => {
|
||||
const state = { message: { ses_1: [message("msg_1")] }, part: { msg_1: [part("prt_1", "msg_1")] } }
|
||||
|
||||
expect(isSessionMaterializationStillNeeded(state, "ses_1", {
|
||||
reason: "missing-owning-message",
|
||||
messageID: "msg_1",
|
||||
})).toBe(false)
|
||||
expect(isSessionMaterializationStillNeeded(state, "ses_1", {
|
||||
reason: "missing-delta-part",
|
||||
messageID: "msg_1",
|
||||
partID: "prt_1",
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test("keeps recovery active while the requested entity is still missing", () => {
|
||||
const state = { message: { ses_1: [message("msg_1")] }, part: {} }
|
||||
|
||||
expect(isSessionMaterializationStillNeeded(state, "ses_1", {
|
||||
reason: "orphan-delta",
|
||||
messageID: "msg_1",
|
||||
partID: "prt_1",
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
import { shouldSkipSessionPrefetch } from "../session-prefetch-cache"
|
||||
|
||||
describe("shouldSkipSessionPrefetch", () => {
|
||||
test("does not skip when only metadata exists without cached messages", () => {
|
||||
expect(shouldSkipSessionPrefetch({
|
||||
hasMessages: false,
|
||||
info: { limit: 200, complete: true, at: 1_000 },
|
||||
pageSize: 200,
|
||||
now: 1_001,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test("does not skip a larger fetch when only a smaller partial prefetch is cached", () => {
|
||||
expect(shouldSkipSessionPrefetch({
|
||||
hasMessages: true,
|
||||
info: { limit: 50, complete: false, at: 1_000 },
|
||||
pageSize: 200,
|
||||
now: 1_001,
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
test("still skips a recent partial prefetch when cached coverage matches the request", () => {
|
||||
expect(shouldSkipSessionPrefetch({
|
||||
hasMessages: true,
|
||||
info: { limit: 200, complete: false, at: 1_000 },
|
||||
pageSize: 200,
|
||||
now: 1_001,
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -3,8 +3,14 @@ import type { Event, Session } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
let currentSessions: Session[] = []
|
||||
const upsertedSessions: Session[] = []
|
||||
const removedSessionIds: string[] = []
|
||||
let runtimeKey = "runtime-a"
|
||||
let runtimeWillChange: (() => void) | null = null
|
||||
|
||||
mock.module("@/stores/useGlobalSessionsStore", () => ({
|
||||
isGlobalSessionRecencyOnlyUpdate: (existing: Session, incoming: Session) => (
|
||||
existing.title === incoming.title && existing.time?.updated !== incoming.time?.updated
|
||||
),
|
||||
useGlobalSessionsStore: {
|
||||
getState: () => ({
|
||||
activeSessions: currentSessions,
|
||||
@@ -12,9 +18,22 @@ mock.module("@/stores/useGlobalSessionsStore", () => ({
|
||||
upsertSession: (session: Session) => {
|
||||
upsertedSessions.push(session)
|
||||
},
|
||||
upsertSessions: (sessions: Session[]) => {
|
||||
upsertedSessions.push(...sessions)
|
||||
},
|
||||
removeSessions: (ids: string[]) => {
|
||||
removedSessionIds.push(...ids)
|
||||
},
|
||||
}),
|
||||
},
|
||||
}))
|
||||
mock.module("@/lib/runtime-switch", () => ({
|
||||
getRuntimeKey: () => runtimeKey,
|
||||
subscribeRuntimeEndpointWillChange: (callback: () => void) => {
|
||||
runtimeWillChange = callback
|
||||
return () => undefined
|
||||
},
|
||||
}))
|
||||
import { applySessionEventToGlobalSessions } from "../session-event-router"
|
||||
|
||||
const buildSession = (title: string, time: Session["time"]): Session => ({
|
||||
@@ -30,10 +49,23 @@ const buildEvent = (session: Session): Event => ({
|
||||
},
|
||||
} as Event)
|
||||
|
||||
const buildDeleteEvent = (sessionId: string): Event => ({
|
||||
type: "session.deleted",
|
||||
properties: { sessionID: sessionId },
|
||||
} as Event)
|
||||
|
||||
const buildLifecycleEvent = (type: "session.idle" | "session.error", sessionId: string): Event => ({
|
||||
type,
|
||||
properties: { sessionID: sessionId },
|
||||
} as Event)
|
||||
|
||||
describe("applySessionEventToGlobalSessions", () => {
|
||||
beforeEach(() => {
|
||||
runtimeWillChange?.()
|
||||
runtimeKey = "runtime-a"
|
||||
currentSessions = []
|
||||
upsertedSessions.length = 0
|
||||
removedSessionIds.length = 0
|
||||
})
|
||||
|
||||
test("skips stale global session.updated echoes after a newer rename", () => {
|
||||
@@ -43,4 +75,45 @@ describe("applySessionEventToGlobalSessions", () => {
|
||||
|
||||
expect(upsertedSessions).toEqual([])
|
||||
})
|
||||
|
||||
test("commits only the latest recency update when a session becomes idle", () => {
|
||||
currentSessions = [buildSession("Initial", { created: 1, updated: 10 })]
|
||||
|
||||
applySessionEventToGlobalSessions(buildEvent(buildSession("Initial", { created: 1, updated: 20 })))
|
||||
applySessionEventToGlobalSessions(buildEvent(buildSession("Initial", { created: 1, updated: 30 })))
|
||||
|
||||
expect(upsertedSessions).toEqual([])
|
||||
applySessionEventToGlobalSessions(buildLifecycleEvent("session.idle", "ses_1"))
|
||||
expect(upsertedSessions.map((session) => session.time.updated)).toEqual([30])
|
||||
})
|
||||
|
||||
test("applies substantive session updates immediately", () => {
|
||||
currentSessions = [buildSession("Initial", { created: 1, updated: 10 })]
|
||||
|
||||
applySessionEventToGlobalSessions(buildEvent(buildSession("Renamed", { created: 1, updated: 20 })))
|
||||
|
||||
expect(upsertedSessions.map((session) => session.title)).toEqual(["Renamed"])
|
||||
})
|
||||
|
||||
test("cancels a pending global update when the session is deleted", () => {
|
||||
currentSessions = [buildSession("Initial", { created: 1, updated: 10 })]
|
||||
|
||||
applySessionEventToGlobalSessions(buildEvent(buildSession("Initial", { created: 1, updated: 20 })))
|
||||
applySessionEventToGlobalSessions(buildDeleteEvent("ses_1"))
|
||||
applySessionEventToGlobalSessions(buildLifecycleEvent("session.idle", "ses_1"))
|
||||
|
||||
expect(upsertedSessions).toEqual([])
|
||||
expect(removedSessionIds).toEqual(["ses_1"])
|
||||
})
|
||||
|
||||
test("discards pending global updates when the runtime changes", () => {
|
||||
currentSessions = [buildSession("Initial", { created: 1, updated: 10 })]
|
||||
applySessionEventToGlobalSessions(buildEvent(buildSession("Initial", { created: 1, updated: 20 })))
|
||||
|
||||
runtimeKey = "runtime-b"
|
||||
runtimeWillChange?.()
|
||||
applySessionEventToGlobalSessions(buildLifecycleEvent("session.idle", "ses_1"))
|
||||
|
||||
expect(upsertedSessions).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user