fix: preserve sessions when bootstrap races with event stream
Two fixes for intermittent "sessions missing at app launch": - loadSessions: if session.list comes back empty but the store already has sessions populated via WS events, don't clobber. OpenCode can answer HTTP with [] while the WS snapshot is still arriving. - loadSessions: wrap SDK errors preserving HTTP status so retry()'s transient detection (5xx) actually fires. String(errorObject) was erasing the status and retry gave up after one attempt. - retry: broaden transient 5xx window from just 502/503 to any 5xx. OpenCode can return 500/504 while warming up (session store reading from disk), and those deserve the same retry treatment. - Log bootstrap empty-session retries for future diagnosis.
This commit is contained in:
@@ -24,9 +24,10 @@ function isTransientError(error: unknown): boolean {
|
||||
if (!error) return false
|
||||
const message = String(error instanceof Error ? error.message : error).toLowerCase()
|
||||
if (TRANSIENT_MESSAGES.some((m) => message.includes(m))) return true
|
||||
// SDK errors from HTTP 502/503 responses (VS Code bridge returns these before OpenCode is ready)
|
||||
// Any HTTP 5xx is considered transient — server-side issues during warmup
|
||||
// (OpenCode reading sessions from disk, bridge not ready, etc.) are retryable.
|
||||
const status = (error as { status?: number })?.status
|
||||
if (status === 502 || status === 503) return true
|
||||
if (typeof status === "number" && status >= 500 && status < 600) return true
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1255,13 +1255,34 @@ export function SyncProvider(props: {
|
||||
limit: 50,
|
||||
})
|
||||
// SDK returns { error } instead of { data } on non-ok responses (503).
|
||||
// Throw so retry() retries and allSettled marks it as rejected.
|
||||
if ((result as { error?: unknown }).error) {
|
||||
throw new Error("session.list failed: " + String((result as { error?: unknown }).error))
|
||||
// Preserve HTTP status so retry()'s transient detection works.
|
||||
const rawError = (result as { error?: unknown }).error
|
||||
if (rawError) {
|
||||
const response = (result as { response?: { status?: number } }).response
|
||||
const status = response?.status
|
||||
const message = typeof rawError === "object" && rawError !== null && "message" in rawError
|
||||
? String((rawError as { message?: unknown }).message)
|
||||
: String(rawError)
|
||||
const wrapped = new Error(`session.list failed${status ? ` (${status})` : ""}: ${message}`)
|
||||
if (status !== undefined) {
|
||||
;(wrapped as Error & { status?: number }).status = status
|
||||
}
|
||||
throw wrapped
|
||||
}
|
||||
const sessions = (result.data ?? [])
|
||||
.filter((s) => !!s?.id)
|
||||
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
||||
// Race guard: if the list came back empty but event pipeline
|
||||
// already populated the store, don't clobber. OpenCode can
|
||||
// answer HTTP with empty sessions while WS delivers session
|
||||
// events for the same data (disk warmup race on app launch).
|
||||
const currentSessions = store.getState().session
|
||||
if (sessions.length === 0 && currentSessions.length > 0) {
|
||||
console.warn(
|
||||
`[bootstrap] session.list returned empty for ${dir}; preserving ${currentSessions.length} existing sessions`,
|
||||
)
|
||||
return
|
||||
}
|
||||
store.setState({ session: sessions, sessionTotal: sessions.length, limit: Math.max(sessions.length, 50) })
|
||||
ingestDirectoryStateIntoRoutingIndex(routingIndex, directory, store.getState())
|
||||
}),
|
||||
@@ -1271,9 +1292,12 @@ export function SyncProvider(props: {
|
||||
// wasn't ready yet (bridge returned 503). Retry a few times.
|
||||
const state = store.getState()
|
||||
if (state.session.length === 0 && attempt < 5) {
|
||||
console.warn(`[bootstrap] sessions empty for ${directory} after attempt ${attempt + 1}; retrying in 2s`)
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
store.setState({ status: "loading" as const })
|
||||
await runBootstrap(attempt + 1)
|
||||
} else if (state.session.length === 0) {
|
||||
console.warn(`[bootstrap] sessions empty for ${directory} after ${attempt + 1} attempts; giving up`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user