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
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import type { SessionStatus, Message, Part } from "@opencode-ai/sdk/v2/client"
|
|
import type { Session } from "@opencode-ai/sdk/v2"
|
|
import { getSessionMaterializationStatus } from "./materialization"
|
|
|
|
type ReconnectMaterializationState = {
|
|
session: Session[]
|
|
session_status?: Record<string, SessionStatus>
|
|
message?: Record<string, Message[]>
|
|
part?: Record<string, Part[]>
|
|
}
|
|
|
|
type ViewedSessionMaterializationTarget = {
|
|
directory: string
|
|
sessionId: string
|
|
}
|
|
|
|
type ReconnectCandidateOptions = {
|
|
directory?: string
|
|
viewedSession?: ViewedSessionMaterializationTarget | null
|
|
}
|
|
|
|
type BootstrapSessionRevisionOptions = {
|
|
baselineRevision: number
|
|
eventRevision?: Record<string, number>
|
|
deletedRevision?: Record<string, number>
|
|
}
|
|
|
|
const getParentId = (session: Session): string | null | undefined => (
|
|
(session as Session & { parentID?: string | null }).parentID
|
|
)
|
|
|
|
const includeAncestorSessions = (
|
|
parentIds: string[],
|
|
includedIds: Set<string>,
|
|
sessionsById: Map<string, Session>,
|
|
excludedIds?: ReadonlySet<string>,
|
|
): void => {
|
|
while (parentIds.length > 0) {
|
|
const parentId = parentIds.pop()
|
|
if (!parentId || includedIds.has(parentId) || excludedIds?.has(parentId)) continue
|
|
const parent = sessionsById.get(parentId)
|
|
if (!parent) continue
|
|
includedIds.add(parentId)
|
|
const ancestorId = getParentId(parent)
|
|
if (ancestorId) parentIds.push(ancestorId)
|
|
}
|
|
}
|
|
|
|
export function mergeBootstrapSessions(
|
|
rootSessions: Session[],
|
|
allSessions: Session[] | null,
|
|
existingSessions: Session[],
|
|
revisions?: BootstrapSessionRevisionOptions,
|
|
): { sessions: Session[]; rootCount: number } {
|
|
const completeSessions = allSessions ?? existingSessions.filter(
|
|
(session) => Boolean(getParentId(session)),
|
|
)
|
|
const rootIds = new Set(rootSessions.map((session) => session.id))
|
|
const sessionsById = new Map(existingSessions.map((session) => [session.id, session]))
|
|
for (const session of completeSessions) sessionsById.set(session.id, session)
|
|
for (const session of rootSessions) sessionsById.set(session.id, session)
|
|
|
|
const includedIds = new Set(rootIds)
|
|
const pendingParentIds: string[] = []
|
|
for (const session of completeSessions) {
|
|
const parentId = getParentId(session)
|
|
if (!parentId) continue
|
|
includedIds.add(session.id)
|
|
pendingParentIds.push(parentId)
|
|
}
|
|
includeAncestorSessions(pendingParentIds, includedIds, sessionsById)
|
|
|
|
if (revisions) {
|
|
const deletedIds = new Set(
|
|
Object.entries(revisions.deletedRevision ?? {})
|
|
.filter(([, revision]) => revision > revisions.baselineRevision)
|
|
.map(([sessionId]) => sessionId),
|
|
)
|
|
for (const session of existingSessions) {
|
|
if ((revisions.eventRevision?.[session.id] ?? 0) <= revisions.baselineRevision) continue
|
|
sessionsById.set(session.id, session)
|
|
includedIds.add(session.id)
|
|
const parentId = getParentId(session)
|
|
if (parentId) pendingParentIds.push(parentId)
|
|
}
|
|
for (const sessionId of deletedIds) includedIds.delete(sessionId)
|
|
includeAncestorSessions(pendingParentIds, includedIds, sessionsById, deletedIds)
|
|
}
|
|
|
|
const sessions = [...includedIds]
|
|
.map((id) => sessionsById.get(id))
|
|
.filter((session): session is Session => Boolean(session))
|
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
const rootCount = sessions.reduce((count, session) => (
|
|
getParentId(session) ? count : count + 1
|
|
), 0)
|
|
|
|
return { sessions, rootCount }
|
|
}
|
|
|
|
export function getReconnectCandidateSessionIds(state: ReconnectMaterializationState, options?: ReconnectCandidateOptions) {
|
|
const ids = new Set<string>()
|
|
|
|
for (const [sessionId, status] of Object.entries(state.session_status ?? {})) {
|
|
if (status && status.type !== "idle") ids.add(sessionId)
|
|
}
|
|
|
|
for (const [sessionId, messages] of Object.entries(state.message ?? {})) {
|
|
const lastMessage = messages[messages.length - 1]
|
|
if (
|
|
lastMessage
|
|
&& lastMessage.role === "assistant"
|
|
&& typeof (lastMessage as { time?: { completed?: number } }).time?.completed !== "number"
|
|
) {
|
|
ids.add(sessionId)
|
|
} else if (!getSessionMaterializationStatus({ message: state.message ?? {}, part: state.part ?? {} }, sessionId).renderable) {
|
|
ids.add(sessionId)
|
|
}
|
|
}
|
|
|
|
const parentIds = new Set<string>()
|
|
for (const session of state.session) {
|
|
const parentId = getParentId(session)
|
|
if (parentId) {
|
|
parentIds.add(parentId)
|
|
}
|
|
}
|
|
for (const pid of parentIds) {
|
|
ids.add(pid)
|
|
}
|
|
|
|
const viewedSession = options?.viewedSession
|
|
if (viewedSession?.sessionId && viewedSession.directory === options?.directory) {
|
|
const sessionId = viewedSession.sessionId
|
|
const sessionExists = state.session.some((session) => session.id === sessionId)
|
|
|| Object.hasOwn(state.session_status ?? {}, sessionId)
|
|
|| Object.hasOwn(state.message ?? {}, sessionId)
|
|
|
|
if (sessionExists) {
|
|
ids.add(sessionId)
|
|
}
|
|
}
|
|
|
|
return Array.from(ids)
|
|
}
|