perf: optimize session loading and desktop startup (#2545)

* perf: optimize session loading and startup

* fix(chat): stabilize history prepend virtualization

* perf: unblock first session open from startup network contention

Opening the first session after app start waited seconds for its message
fetch. Three independent contributors, each measured via CDP network
capture and Chromium net-log against the packaged desktop app:

- The active-session watchdog fired an uncapped per-directory status poll
  and child-session discovery burst at startup, and other subsystems
  (git checks, global session pages, command/skill discovery) fanned out
  alongside it, saturating the browser's ~6 HTTP/1.1 sockets per origin.
  Add a shared background-network gate (concurrency 3) and route the
  watchdog, poll-shaped git reads (also priority: low), global session
  pages, command/skill loads, and the background update check through it.

- The packaged renderer is cross-origin to the loopback backend, so every
  API call needs a CORS preflight; a few slow OpenCode-proxied requests
  held the whole pool while preflights and interactive traffic queued
  behind them. Lift Chromium's per-host connection cap for loopback via
  ignore-connections-limit in the Electron shell.

- OpenCode initializes each directory lazily on its first request, so the
  first click paid that cost interactively. Warm the last-used directory
  and the three most recently opened projects right after OpenCode
  readiness, sequentially and best-effort, overlapping UI startup.

Validation: new background-network tests, lifecycle warmup test, focused
store/sync tests, UI type-check and lint, dead-code report, node --check
plus electron type-check/lint, and CDP first-open measurements on the
packaged app (message fetch socket queue 5.4s -> 0.03s).

* fix(ui): keep interactive git reads out of background queue

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
𝖎𝖚𝖑𝖎𝖎𝖆
2026-07-31 12:51:15 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 09f0c64839
commit aae889b904
41 changed files with 1690 additions and 203 deletions
+6 -3
View File
@@ -128,6 +128,8 @@ Cross-directory selectors subscribe to the narrow child-store field they aggrega
Session display order is independent from streaming-frequency `time.updated` publications. `session-ordering.ts` promotes a session exactly when its authoritative activity phase crosses `settled` (`idle`/`error`) and `active` (`busy`/`retry`) in either direction. Repeated busy/retry or idle/error events are no-ops. The first authoritative status snapshot establishes a baseline without synthetic promotions; later snapshots reconcile missed transitions. Root sessions compare lifecycle rank only with other roots, while child sessions compare lifecycle rank only with siblings sharing the same `parentID`, so child activity never moves its root conversation. Pins remain the first ordering bucket. The timestamp/creation fallback is frozen when a session first participates in ordering, so later metadata-only updates cannot reorder it; creation time and ID provide deterministic ties. Runtime switches clear all phases, baselines, and ranks.
The active-session watchdog in `sync-context.tsx` (per-directory status polls and child-session discovery lists) runs its network calls through the shared background-network gate in `@/lib/background-network`, alongside poll-shaped git reads, global session pages, and command/skill discovery. Background fan-out must stay under that gate so the browser's per-origin connection pool keeps free sockets for interactive traffic — an uncapped startup burst previously queued the first session-open message fetch for seconds.
Imperative cross-directory session lookups use the cached ID index from `getAllSyncSessionMap()`. The index is rebuilt only when a child store's `state.session` reference changes; permission lineage checks must reuse it instead of rebuilding a full session map per call.
VS Code does not run the server permission-auto-accept runtime. The extension host persists and broadcasts authoritative policy, while its foreground UI runtime resolves missing child-session lineage through the OpenCode API before deciding whether to suppress and answer a `permission.asked` event. Enabling the policy and reconnect/bootstrap both reconcile pending requests in the session directory, including requests inherited by child sessions. Unknown lineage and exhausted reply retries fail closed and leave the request available for manual action. A later `permission.replied` event invalidates any older deferred ask so the async policy check cannot resurrect a resolved request. With every OpenChamber webview closed or suspended no responder runs; this is an intentional VS Code limitation. Other runtimes remain fully server-owned.
@@ -164,15 +166,16 @@ Rules:
4. Async commits are generation-checked. Runtime switches, forced refreshes, eviction, and disposal must reject stale completion.
5. Prefetch coverage and persisted directory data are runtime-scoped. Legacy persisted directory entries may seed startup continuity, but they are not live truth.
6. Message and part materialization preserves references for unchanged records and maintains direct message-to-parts lookup. Consumers subscribe to the selected session's records rather than broad message/part containers.
7. The ref-stable loader is disposed only after the current task when its provider unmounts. This lets React Strict Mode's development setup → cleanup → setup probe retain a usable loader for child effects, while real disposal still invalidates the preceding lifecycle's work.
7. Pagination demand must carry the selected session's effective directory. It must not fall back to the sync provider directory because the visible session may belong to another worktree.
8. The ref-stable loader is disposed only after the current task when its provider unmounts. This lets React Strict Mode's development setup → cleanup → setup probe retain a usable loader for child effects, while real disposal still invalidates the preceding lifecycle's work.
Initial loads use smaller pages on constrained VS Code/mobile surfaces. Older pages are fetched through the same loader and merged with optimistic records before publication.
Initial loads use smaller pages on constrained VS Code/mobile surfaces. Prefetch resolves only the initial renderable page; it does not eagerly download older history. The mounted chat timeline requests older pages when its viewport is underfilled or the user scrolls toward history, while mobile uses its explicit load-older action. Timeline caches, pending work, prepend snapshots, and stale checks use runtime + directory + session identity so equal session IDs in different worktrees cannot share lifecycle state. Older pages are fetched through the same loader and merged with optimistic records before publication.
## Loading diagnostics
Session loading instrumentation is disabled by default. Set `localStorage.openchamber_session_load_perf` to `"1"`, reproduce the interaction, then inspect `window.__openchamberSessionLoadPerformance.events`.
The bounded event buffer records bootstrap, message, and global-list operations with queue/duration, caller, outcome, retry count, and record count where applicable. Instrumentation is diagnostic only; unit/type/lint checks do not replace production runtime profiling at representative project/session scale.
The bounded event buffer records only controlled bootstrap, message, and global-list operation/caller labels with queue/duration, outcome, retry count, and downloaded record count where applicable. Message-page events also record the requested limit and whether a cursor was present. When diagnostics are enabled, the selected chat records its first painted renderable message snapshot once per recent session identity and immediately clears the corresponding browser performance entry after emitting the trace mark. Canceled frames retain no measured identity, so returning to that session can schedule a replacement measurement; completed identity tracking uses the same 1,000-entry ceiling as the event buffer. Exported events never retain runtime keys, directories, session IDs, credentials, or message content. Initial-message expansion counts every downloaded page, not only the accepted page. The browser profiler independently validates the known labels and finite numeric fields before export. Instrumentation is diagnostic only; unit/type/lint checks do not replace production runtime profiling at representative project/session scale.
High-frequency sync diagnostics are separately disabled by default. Set `localStorage.openchamber_sync_perf` to `"1"` before reload to enable fixed numeric counters for pipeline traffic, reducer publications, streaming reconciliations, entries/messages visited, targeted heartbeat work, and persistence serialization/write volume. The hot path performs only a null check while disabled; counters never retain IDs, payloads, or user content.
-1
View File
@@ -525,7 +525,6 @@ export class ChildStoreManager {
this.notifyBootstrapSubscribers()
const finishPerformanceEvent = startSessionLoadPerformanceEvent({
operation: "bootstrap.directory",
directory: next.directory,
caller: next.reason,
queuedMs: Math.max(0, Date.now() - next.enqueuedAt),
})
@@ -1,15 +1,51 @@
const STORAGE_KEY = "openchamber_session_load_perf"
const MAX_EVENTS = 1_000
const ALLOWED_OPERATIONS = new Set([
"bootstrap.directory",
"bootstrap.sessions.all",
"bootstrap.sessions.archived",
"bootstrap.sessions.roots",
"global-sessions.active",
"global-sessions.archived",
"session-messages.initial",
"session-messages.older",
"session-messages.page",
"session-messages.refresh",
"session-messages.visible",
"session-prefetch",
])
const ALLOWED_CALLERS = new Set([
"action-demand",
"current-directory",
"initial",
"initial-page",
"known-project",
"known-worktree",
"older",
"pagination",
"prefetch",
"project-expanded",
"refresh",
"selected-session",
"server-connected",
"worktree-expanded",
])
const ALLOWED_OUTCOMES = new Set<SessionLoadPerformanceOutcome>([
"complete",
"error",
"stale",
"deduplicated",
"canceled",
])
type SessionLoadPerformanceOutcome = "complete" | "error" | "stale" | "deduplicated" | "canceled"
type SessionLoadPerformanceEvent = {
operation: string
runtimeKey?: string
directory?: string
sessionID?: string
caller?: string
queuedMs?: number
requestLimit?: number
cursorPresent?: boolean
durationMs: number
outcome: SessionLoadPerformanceOutcome
retryCount?: number
@@ -27,7 +63,7 @@ declare global {
}
}
const enabled = (): boolean => {
const isSessionLoadPerformanceEnabled = (): boolean => {
if (typeof window === "undefined") return false
try {
return window.localStorage.getItem(STORAGE_KEY) === "1"
@@ -40,23 +76,100 @@ const now = (): number => typeof performance !== "undefined" && typeof performan
? performance.now()
: Date.now()
const nonNegativeNumber = (value: unknown): number | undefined => (
typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined
)
const nonNegativeInteger = (value: unknown): number | undefined => (
Number.isInteger(value) && Number(value) >= 0 ? Number(value) : undefined
)
export function startSessionLoadPerformanceEvent(input: Omit<SessionLoadPerformanceEvent, "at" | "durationMs" | "outcome">) {
if (!enabled()) return () => undefined
if (
!isSessionLoadPerformanceEnabled()
|| !ALLOWED_OPERATIONS.has(input.operation)
|| (input.caller !== undefined && !ALLOWED_CALLERS.has(input.caller))
) return () => undefined
const startedAt = now()
return (
outcome: SessionLoadPerformanceOutcome,
details?: Partial<Pick<SessionLoadPerformanceEvent, "retryCount" | "recordCount">>,
) => {
if (typeof window === "undefined") return
if (typeof window === "undefined" || !ALLOWED_OUTCOMES.has(outcome)) return
const state = window.__openchamberSessionLoadPerformance ?? { events: [] }
const queuedMs = nonNegativeNumber(input.queuedMs)
const requestLimit = nonNegativeInteger(input.requestLimit)
const retryCount = nonNegativeInteger(details?.retryCount ?? input.retryCount)
const recordCount = nonNegativeInteger(details?.recordCount ?? input.recordCount)
state.events.push({
...input,
...details,
operation: input.operation,
...(input.caller !== undefined ? { caller: input.caller } : {}),
...(queuedMs !== undefined ? { queuedMs } : {}),
...(requestLimit !== undefined ? { requestLimit } : {}),
...(typeof input.cursorPresent === "boolean" ? { cursorPresent: input.cursorPresent } : {}),
outcome,
durationMs: Math.max(0, now() - startedAt),
...(retryCount !== undefined ? { retryCount } : {}),
...(recordCount !== undefined ? { recordCount } : {}),
at: Date.now(),
})
if (state.events.length > MAX_EVENTS) state.events.splice(0, state.events.length - MAX_EVENTS)
window.__openchamberSessionLoadPerformance = state
}
}
type FirstVisibleSessionPerformanceDependencies = {
enabled: () => boolean
requestFrame: (callback: FrameRequestCallback) => number
cancelFrame: (frame: number) => void
markVisible: () => void
startEvent: typeof startSessionLoadPerformanceEvent
}
const FIRST_VISIBLE_MARK = "openchamber.chat.first_message_visible"
export function createFirstVisibleSessionPerformanceTracker(
dependencies?: Partial<FirstVisibleSessionPerformanceDependencies>,
) {
const enabled = dependencies?.enabled ?? isSessionLoadPerformanceEnabled
const requestFrame = dependencies?.requestFrame ?? ((callback) => window.requestAnimationFrame(callback))
const cancelFrame = dependencies?.cancelFrame ?? ((frame) => window.cancelAnimationFrame(frame))
const markVisible = dependencies?.markVisible ?? (() => {
performance.mark(FIRST_VISIBLE_MARK)
performance.clearMarks(FIRST_VISIBLE_MARK)
})
const startEvent = dependencies?.startEvent ?? startSessionLoadPerformanceEvent
const measuredKeys = new Set<string>()
let pending: { key: string; frame: number } | null = null
return {
schedule(key: string, recordCount: number): () => void {
if (!enabled() || measuredKeys.has(key)) return () => undefined
if (pending) {
cancelFrame(pending.frame)
pending = null
}
const finishPerformanceEvent = startEvent({
operation: "session-messages.visible",
caller: "selected-session",
recordCount,
})
const frame = requestFrame(() => {
if (pending?.key !== key || pending.frame !== frame) return
pending = null
measuredKeys.add(key)
if (measuredKeys.size > MAX_EVENTS) {
measuredKeys.delete(measuredKeys.values().next().value!)
}
markVisible()
finishPerformanceEvent("complete")
})
pending = { key, frame }
return () => {
if (pending?.key !== key || pending.frame !== frame) return
cancelFrame(frame)
pending = null
}
},
}
}
@@ -2,6 +2,10 @@ import { describe, expect, test } from "bun:test"
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
import { ChildStoreManager } from "./child-store"
import { SessionMessageLoader } from "./session-message-loader"
import {
createFirstVisibleSessionPerformanceTracker,
startSessionLoadPerformanceEvent,
} from "./session-load-performance"
const createRecord = (sessionID: string, id = "msg_1") => ({
info: { id, sessionID, role: "user", time: { created: 1 } } as Message,
@@ -56,6 +60,34 @@ describe("SessionMessageLoader", () => {
childStores.disposeAll()
})
test("leaves older history loading to explicit viewport demand", async () => {
const calls: Array<{ limit?: number; before?: string }> = []
const { childStores, loader } = createLoader(async ({ sessionID, limit, before }) => {
calls.push({ limit, before })
return before
? response([createRecord(sessionID, "msg_older")])
: response([createRecord(sessionID, "msg_latest")], "older-cursor")
})
const target = { directory: "/repo", sessionID: "session-a" }
await loader.ensure(target, { reason: "prefetch" })
await Promise.resolve()
expect(calls).toEqual([{ limit: 50, before: undefined }])
expect(loader.getSnapshot(target).cursor).toBe("older-cursor")
await loader.loadOlder(target)
expect(calls).toEqual([
{ limit: 50, before: undefined },
{ limit: 100, before: "older-cursor" },
])
expect(childStores.getChild(target.directory)?.getState().message[target.sessionID]?.map((message) => message.id))
.toEqual(["msg_latest", "msg_older"].sort())
loader.dispose()
childStores.disposeAll()
})
test("runs a requested tail refresh after an older in-flight load", async () => {
const initial = deferred<ReturnType<typeof response>>()
const refresh = deferred<ReturnType<typeof response>>()
@@ -128,6 +160,34 @@ describe("SessionMessageLoader", () => {
childStores.disposeAll()
})
test("loads older history with the selected directory's cursor for duplicate session IDs", async () => {
const providerDirectory = "/repo/provider"
const selectedDirectory = "/repo/selected-worktree"
const sessionID = "shared"
const calls: Array<{ directory?: string; before?: string }> = []
const { childStores, loader } = createLoader(async ({ directory, before }) => {
calls.push({ directory, before })
return before
? response([createRecord(sessionID, `older-${directory}`)])
: response([createRecord(sessionID, `latest-${directory}`)], `${directory}-cursor`)
})
await Promise.all([
loader.ensure({ directory: providerDirectory, sessionID }),
loader.ensure({ directory: selectedDirectory, sessionID }),
])
calls.length = 0
await loader.loadOlder({ directory: selectedDirectory, sessionID })
expect(calls).toEqual([{
directory: selectedDirectory,
before: `${selectedDirectory}-cursor`,
}])
loader.dispose()
childStores.disposeAll()
})
test("exposes a retryable error without clearing an existing snapshot", async () => {
let fail = true
const { childStores, loader } = createLoader(async ({ sessionID }) => {
@@ -209,4 +269,165 @@ describe("SessionMessageLoader", () => {
loader.dispose()
childStores.disposeAll()
})
test("reports retries and every downloaded initial expansion record", async () => {
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window")
const diagnosticWindow = {
location: { search: "" },
localStorage: {
getItem: (key: string) => key === "openchamber_session_load_perf" ? "1" : null,
},
} as unknown as Window
Object.defineProperty(globalThis, "window", { configurable: true, value: diagnosticWindow })
const target = { directory: "/repo", sessionID: "session-a" }
let calls = 0
const { childStores, loader } = createLoader(async () => {
calls += 1
if (calls === 1) return {}
if (calls === 2) {
const assistant = createRecord(target.sessionID, "msg_assistant")
assistant.info = { ...assistant.info, role: "assistant" } as Message
return response([assistant], "older")
}
return response([createRecord(target.sessionID, "msg_user")])
})
try {
await loader.ensure(target)
const events = diagnosticWindow.__openchamberSessionLoadPerformance?.events ?? []
const initialEvent = events.find((event) => event.operation === "session-messages.initial")
const pageEvents = events.filter((event) => event.operation === "session-messages.page")
expect(calls).toBe(3)
expect(pageEvents.map((event) => event.requestLimit)).toEqual([50, 100])
expect(pageEvents.map((event) => event.cursorPresent)).toEqual([false, false])
expect(pageEvents.map((event) => event.recordCount)).toEqual([1, 1])
expect(initialEvent?.outcome).toBe("complete")
expect(initialEvent?.retryCount).toBe(1)
expect(initialEvent?.recordCount).toBe(2)
expect("runtimeKey" in initialEvent!).toBe(false)
expect("directory" in initialEvent!).toBe(false)
expect("sessionID" in initialEvent!).toBe(false)
} finally {
loader.dispose()
childStores.disposeAll()
if (originalWindow) Object.defineProperty(globalThis, "window", originalWindow)
else Reflect.deleteProperty(globalThis, "window")
}
})
})
describe("session load performance diagnostics", () => {
test("rejects unknown raw labels and preserves approved input counts", () => {
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window")
const diagnosticWindow = {
localStorage: {
getItem: (key: string) => key === "openchamber_session_load_perf" ? "1" : null,
},
} as unknown as Window
Object.defineProperty(globalThis, "window", { configurable: true, value: diagnosticWindow })
try {
const finishUnknown = startSessionLoadPerformanceEvent({
operation: "secret-operation",
caller: "secret-caller",
recordCount: 999,
})
finishUnknown("complete")
const finishVisible = startSessionLoadPerformanceEvent({
operation: "session-messages.visible",
caller: "selected-session",
recordCount: 30,
})
finishVisible("complete")
expect(diagnosticWindow.__openchamberSessionLoadPerformance?.events).toHaveLength(1)
const event = diagnosticWindow.__openchamberSessionLoadPerformance?.events[0]
expect(event?.operation).toBe("session-messages.visible")
expect(event?.caller).toBe("selected-session")
expect(event?.recordCount).toBe(30)
expect(JSON.stringify(diagnosticWindow.__openchamberSessionLoadPerformance)).not.toContain("secret")
} finally {
if (originalWindow) Object.defineProperty(globalThis, "window", originalWindow)
else Reflect.deleteProperty(globalThis, "window")
}
})
test("does not schedule visibility work while diagnostics are disabled", () => {
let requestedFrames = 0
let visibleMarks = 0
const tracker = createFirstVisibleSessionPerformanceTracker({
enabled: () => false,
requestFrame: () => {
requestedFrames += 1
return 1
},
cancelFrame: () => undefined,
markVisible: () => {
visibleMarks += 1
},
})
tracker.schedule("session-a", 10)
expect(requestedFrames).toBe(0)
expect(visibleMarks).toBe(0)
})
test("reschedules an identity when its pending visibility frame was canceled", () => {
let nextFrame = 0
const frames = new Map<number, FrameRequestCallback>()
const marks: string[] = []
const tracker = createFirstVisibleSessionPerformanceTracker({
enabled: () => true,
requestFrame: (callback) => {
nextFrame += 1
frames.set(nextFrame, callback)
return nextFrame
},
cancelFrame: (frame) => {
frames.delete(frame)
},
markVisible: () => marks.push("visible"),
startEvent: () => () => undefined,
})
const cancelFirstA = tracker.schedule("session-a", 10)
cancelFirstA()
const cancelB = tracker.schedule("session-b", 10)
cancelB()
tracker.schedule("session-a", 10)
frames.get(3)?.(0)
expect(marks).toEqual(["visible"])
})
test("does not remeasure a completed identity after another session", () => {
let nextFrame = 0
const frames = new Map<number, FrameRequestCallback>()
const marks: string[] = []
const tracker = createFirstVisibleSessionPerformanceTracker({
enabled: () => true,
requestFrame: (callback) => {
nextFrame += 1
frames.set(nextFrame, callback)
return nextFrame
},
cancelFrame: (frame) => {
frames.delete(frame)
},
markVisible: () => marks.push("visible"),
startEvent: () => () => undefined,
})
tracker.schedule("session-a", 10)
frames.get(1)?.(0)
tracker.schedule("session-b", 10)
frames.get(2)?.(0)
tracker.schedule("session-a", 10)
expect(nextFrame).toBe(2)
expect(marks).toEqual(["visible", "visible"])
})
})
+70 -47
View File
@@ -61,6 +61,11 @@ type FetchedPage = {
complete: boolean
}
type LoadPerformanceDetails = {
retryCount: number
recordCount: number
}
type LoaderConfiguration = {
sdk: OpencodeClient
runtimeKey: string
@@ -201,15 +206,8 @@ export class SessionMessageLoader {
}
if (options?.force) this.bumpGeneration(entry)
const kind: SessionMessageLoadKind = options?.reason === "prefetch" ? "prefetch" : "initial"
return this.startLoad(normalized, entry, store, kind, async (isCurrent) => {
await this.loadInitial(normalized, entry, store, isCurrent)
if (!isMobileSurfaceRuntime() && isCurrent()) {
queueMicrotask(() => {
if (isCurrent() && entry.snapshot.cursor && !entry.snapshot.complete) {
void this.loadOlder(normalized)
}
})
}
return this.startLoad(normalized, entry, store, kind, async (isCurrent, performance) => {
await this.loadInitial(normalized, entry, store, isCurrent, performance)
})
}
@@ -225,8 +223,8 @@ export class SessionMessageLoader {
if (entry.snapshot.complete || !entry.snapshot.cursor) return Promise.resolve()
const store = this.childStores.ensureChild(normalized.directory, { bootstrap: false })
const cursor = entry.snapshot.cursor
return this.startLoad(normalized, entry, store, "older", async (isCurrent) => {
const page = await this.fetchPage(normalized, HISTORY_MESSAGE_PAGE_SIZE, cursor)
return this.startLoad(normalized, entry, store, "older", async (isCurrent, performance) => {
const page = await this.fetchPage(normalized, HISTORY_MESSAGE_PAGE_SIZE, cursor, "older", performance)
if (!isCurrent()) return
const committed = this.commitPage(normalized, entry, store, page, "prepend", isCurrent)
if (!committed || !isCurrent()) return
@@ -279,11 +277,11 @@ export class SessionMessageLoader {
}
const store = this.childStores.ensureChild(normalized.directory, { bootstrap: false })
this.bumpGeneration(entry)
return this.startLoad(normalized, entry, store, "refresh", async (isCurrent) => {
return this.startLoad(normalized, entry, store, "refresh", async (isCurrent, performance) => {
const previousCoverage = entry.snapshot.resolved
? { cursor: entry.snapshot.cursor, complete: entry.snapshot.complete }
: null
const page = await this.fetchPage(normalized, Math.max(1, limit))
const page = await this.fetchPage(normalized, Math.max(1, limit), undefined, "refresh", performance)
if (!isCurrent()) return
const committed = this.commitPage(normalized, entry, store, page, "merge", isCurrent)
if (!committed || !isCurrent()) return
@@ -455,15 +453,12 @@ export class SessionMessageLoader {
entry: LoaderEntry,
store: { getState: () => DirectoryStore; setState: DirectoryStoreSetter },
kind: SessionMessageLoadKind,
run: (isCurrent: () => boolean) => Promise<void>,
run: (isCurrent: () => boolean, performance: LoadPerformanceDetails) => Promise<void>,
): Promise<void> {
const generation = entry.snapshot.generation
const sdkEpoch = this.sdkEpoch
const finishPerformanceEvent = startSessionLoadPerformanceEvent({
operation: kind === "prefetch" ? "session-prefetch" : `session-messages.${kind}`,
runtimeKey: this.runtimeKey,
directory: target.directory,
sessionID: target.sessionID,
caller: kind,
})
const isCurrent = () => (
@@ -472,21 +467,22 @@ export class SessionMessageLoader {
&& entry.snapshot.generation === generation
&& this.childStores.getChild(target.directory) === store
)
const performance = { retryCount: 0, recordCount: 0 }
this.patchEntry(entry, { status: "loading", loadingKind: kind, error: null })
let loadPromise: Promise<void>
try {
loadPromise = run(isCurrent)
loadPromise = run(isCurrent, performance)
} catch (error) {
loadPromise = Promise.reject(error)
}
const promise = loadPromise
.then(() => finishPerformanceEvent(isCurrent() ? "complete" : "stale"))
.then(() => finishPerformanceEvent(isCurrent() ? "complete" : "stale", performance))
.catch((error: unknown) => {
if (!isCurrent()) {
finishPerformanceEvent("stale")
finishPerformanceEvent("stale", performance)
return
}
finishPerformanceEvent("error")
finishPerformanceEvent("error", performance)
this.patchEntry(entry, {
status: "error",
loadingKind: null,
@@ -505,10 +501,11 @@ export class SessionMessageLoader {
entry: LoaderEntry,
store: { getState: () => DirectoryStore; setState: DirectoryStoreSetter },
isCurrent: () => boolean,
performance?: LoadPerformanceDetails,
): Promise<void> {
const storeMessageCount = store.getState().message[target.sessionID]?.length ?? 0
const firstLimit = Math.max(entry.snapshot.limit, storeMessageCount, getInitialPageSize())
const firstPage = await this.fetchPage(target, firstLimit)
const firstPage = await this.fetchPage(target, firstLimit, undefined, "initial-page", performance)
if (!isCurrent()) return
const deferFirstCommit = !firstPage.complete && !hasUserMessage(firstPage.session)
let committed = deferFirstCommit
@@ -519,7 +516,7 @@ export class SessionMessageLoader {
if (deferFirstCommit) {
for (const limit of getInitialExpansionLimits()) {
if (limit <= firstLimit || !isCurrent()) continue
const expandedPage = await this.fetchPage(target, limit)
const expandedPage = await this.fetchPage(target, limit, undefined, "initial-page", performance)
if (!isCurrent()) return
acceptedPage = expandedPage
const boundaryFound = hasUserMessage(expandedPage.session)
@@ -547,32 +544,58 @@ export class SessionMessageLoader {
this.persistCoverage(target, entry.snapshot)
}
private async fetchPage(target: SessionMessageTarget, limit: number, before?: string): Promise<FetchedPage> {
const result = await retry(async () => {
const response = await this.sdk.session.messages({
sessionID: target.sessionID,
directory: target.directory,
limit,
before,
})
assertSdkSuccess(response, "session.messages")
if (!Array.isArray(response.data)) {
const error = new Error("session.messages returned no data") as Error & { status?: number }
error.status = 503
throw error
}
return { data: response.data, response: response.response }
private async fetchPage(
target: SessionMessageTarget,
limit: number,
before?: string,
caller: "initial-page" | "older" | "refresh" = "initial-page",
performance?: LoadPerformanceDetails,
): Promise<FetchedPage> {
const finishPagePerformance = startSessionLoadPerformanceEvent({
operation: "session-messages.page",
caller,
requestLimit: limit,
cursorPresent: before !== undefined,
})
const records = result.data.filter((record: { info?: { id?: string } }) => Boolean(record?.info?.id))
const session = records
.map((record: { info: Message }) => stripMessageDiffSnapshots(record.info))
.sort((left: Message, right: Message) => cmp(left.id, right.id))
const partsByMessageID = new Map<string, Part[]>()
for (const record of records as Array<{ info: { id: string }; parts?: Part[] }>) {
partsByMessageID.set(record.info.id, sortParts(record.parts ?? []))
let attempts = 0
let recordCount = 0
try {
const result = await retry(async () => {
attempts += 1
const response = await this.sdk.session.messages({
sessionID: target.sessionID,
directory: target.directory,
limit,
before,
})
assertSdkSuccess(response, "session.messages")
const data = response.data
if (!Array.isArray(data)) {
const error = new Error("session.messages returned no data") as Error & { status?: number }
error.status = 503
throw error
}
return { data, response: response.response }
})
const records = result.data.filter((record: { info?: { id?: string } }) => Boolean(record?.info?.id))
recordCount = records.length
if (performance) performance.recordCount += recordCount
const session = records
.map((record: { info: Message }) => stripMessageDiffSnapshots(record.info))
.sort((left: Message, right: Message) => cmp(left.id, right.id))
const partsByMessageID = new Map<string, Part[]>()
for (const record of records as Array<{ info: { id: string }; parts?: Part[] }>) {
partsByMessageID.set(record.info.id, sortParts(record.parts ?? []))
}
const cursor = result.response?.headers?.get?.("x-next-cursor") ?? undefined
finishPagePerformance("complete", { retryCount: Math.max(0, attempts - 1), recordCount })
return { session, partsByMessageID, cursor, complete: !cursor }
} catch (error) {
finishPagePerformance("error", { retryCount: Math.max(0, attempts - 1), recordCount })
throw error
} finally {
if (performance) performance.retryCount += Math.max(0, attempts - 1)
}
const cursor = result.response?.headers?.get?.("x-next-cursor") ?? undefined
return { session, partsByMessageID, cursor, complete: !cursor }
}
private commitPage(
+13 -2
View File
@@ -31,6 +31,7 @@ import { bootstrapGlobal, bootstrapDirectory } from "./bootstrap"
import { retry } from "./retry"
import { touchStreamingSession, updateChangedStreamingSessions, updateStreamingState } from "./streaming"
import { countSyncPerformance } from "./performance-diagnostics"
import { runBackgroundNetworkTask } from "@/lib/background-network"
import { setActionRefs } from "./session-actions"
import { setSyncRefs, getAllSyncSessions } from "./sync-refs"
import { stripSessionDiffSnapshots } from "./sanitize"
@@ -242,6 +243,16 @@ const ACTIVE_SESSION_STATUS_POLL_INTERVAL_MS = 5_000
const ACTIVE_SESSION_STALE_EVENT_MS = 20_000
const ACTIVE_SESSION_FULL_RESYNC_COOLDOWN_MS = 15_000
const CHILD_SESSION_DISCOVERY_INTERVAL_MS = 15_000
// Active-session watchdog network calls run under the shared
// background-network gate (see lib/background-network.ts). The watchdog walks
// every initialized child store each tick and fires a status poll plus a
// child-session discovery list per directory with active candidates — on
// startup with many cache-hydrated directories that is dozens of simultaneous
// requests, which would otherwise queue interactive traffic (opening a
// session) behind them on the browser's ~6 sockets per origin. Later ticks
// still cover every directory via the per-directory timestamps.
const requestSignature = (items: Array<{ id: string }> | undefined): string => {
if (!items || items.length === 0) return ""
return items
@@ -2072,7 +2083,7 @@ export function SyncProvider(props: {
if (parentSessionIds.length === 0) return
try {
const scopedClient = opencodeClient.getScopedSdkClient(directory)
const result = await scopedClient.session.list({ directory, limit: 200 })
const result: unknown = await runBackgroundNetworkTask(() => scopedClient.session.list({ directory, limit: 200 }))
const allSessions = ((result as { data?: unknown }).data ?? []) as Session[]
const state = store.getState()
const existingIds = new Set(state.session.map((s) => s.id))
@@ -2121,7 +2132,7 @@ export function SyncProvider(props: {
polling.add(directory)
try {
const before = store.getState()
const statuses = await resyncDirectorySessionStatuses(directory, store, candidateSessionIds, "monotonic")
const statuses = await runBackgroundNetworkTask(() => resyncDirectorySessionStatuses(directory, store, candidateSessionIds, "monotonic"))
if (!statuses) return
const needsSnapshot = candidateSessionIds.some((sessionId) => (
needsSnapshotAfterStatusPoll(before, sessionId, statuses[sessionId])
+2 -3
View File
@@ -313,12 +313,11 @@ export function useSync() {
// Load more (pagination)
const loadMore = useCallback(
async (sessionID: string, directoryOverride?: string) => {
const targetDirectory = directoryOverride || directory
async (sessionID: string, targetDirectory: string) => {
touch(sessionID, targetDirectory)
await messageLoader.loadOlder({ directory: targetDirectory, sessionID })
},
[directory, messageLoader, touch],
[messageLoader, touch],
)
const prefetchSession = useCallback(