Skip to content

Commit 23e4e7b

Browse files
authored
fix(opencode): derive boot sidebar active account from routing mode and preserve live session state (#128)
Boot seeding, quota-only refreshes, and command paths no longer clobber the sidebar activeId written by a live routing session. Routing-authoritative writes are distinguished from display-only writes; the serialized write chain re-reads the file before merging, and cross-process writes are guarded by an mkdir directory lock (atomic rename-claim eviction, jittered retry, file-based ownerId release). On lock-budget exhaustion against an active contender the write is skipped — never performed unlocked. The commit path is fenced: ownership is re-verified adjacent to the rename and again after it; on detected post-rename loss, one bounded locked repair republishes only the frame's routing-authoritative fields into the successor's fresh state. Boot resolves preserved routing only after the asynchronous account-storage load completes, so a peer's authoritative publish during the load cannot be overwritten by a pre-load snapshot. The residual window is a process freeze exactly between the final ownership stat and the rename — accepted for a display-only file whose frames self-heal.
1 parent cb09a4d commit 23e4e7b

4 files changed

Lines changed: 1778 additions & 24 deletions

File tree

packages/opencode/src/index.ts

Lines changed: 174 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ import type {
145145
import { getRpcDir } from './rpc/rpc-dir.ts'
146146
import { type RpcServerHandle, startRpcServer } from './rpc/rpc-server.ts'
147147
import {
148+
getInitialSidebarRoutingTestHooks,
149+
getSidebarState,
148150
getSidebarStateFile,
149151
type SidebarState,
150152
setSidebarState,
@@ -173,6 +175,115 @@ const CONCURRENT_MAIN_REFRESH_POLL_BASE_MS = 200
173175
const MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES = 240
174176
const DEFAULT_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES =
175177
MIN_MAIN_REFRESH_BEFORE_EXPIRY_MINUTES
178+
const SIDEBAR_ROUTING_FRESH_MS = 10 * 60 * 1000
179+
180+
function hasEnabledOAuthAccount(
181+
storage: AccountStorage | null,
182+
activeId: string,
183+
) {
184+
return (storage?.accounts ?? []).some(
185+
(account) =>
186+
account.id === activeId &&
187+
account.enabled !== false &&
188+
isOAuthAccount(account),
189+
)
190+
}
191+
192+
function deriveSidebarRouting(
193+
storage: AccountStorage | null,
194+
): Pick<SidebarState, 'activeId' | 'route'> {
195+
const firstFallback = (storage?.accounts ?? []).find(
196+
(account): account is OAuthAccount =>
197+
account.enabled !== false && isOAuthAccount(account),
198+
)
199+
if (getRoutingMode(storage) === 'fallback-first' && firstFallback) {
200+
return { activeId: firstFallback.id, route: 'fallback-first' }
201+
}
202+
return { activeId: 'main', route: 'main' }
203+
}
204+
205+
function validateSidebarRouting(
206+
routing: Pick<SidebarState, 'activeId' | 'route'> | undefined,
207+
storage: AccountStorage | null,
208+
): Pick<SidebarState, 'activeId' | 'route'> {
209+
if (
210+
routing?.activeId === 'main' ||
211+
(routing?.activeId && hasEnabledOAuthAccount(storage, routing.activeId))
212+
) {
213+
return routing
214+
}
215+
return deriveSidebarRouting(storage)
216+
}
217+
218+
function resolveLoadedSidebarRouting(
219+
existing: SidebarState,
220+
freshStorage: AccountStorage | null,
221+
fallbackRouting?: Pick<SidebarState, 'activeId' | 'route'>,
222+
): Pick<SidebarState, 'activeId' | 'route'> {
223+
const existingAge = Date.now() - existing.lastUpdated
224+
if (
225+
!existing.activeId ||
226+
existingAge < 0 ||
227+
existingAge > SIDEBAR_ROUTING_FRESH_MS
228+
) {
229+
return validateSidebarRouting(fallbackRouting, freshStorage)
230+
}
231+
232+
return validateSidebarRouting(existing, freshStorage)
233+
}
234+
235+
async function resolveFreshSidebarRouting(
236+
existing: SidebarState,
237+
loadFreshStorage: () => Promise<AccountStorage | null>,
238+
fallbackRouting?: Pick<SidebarState, 'activeId' | 'route'>,
239+
): Promise<{
240+
activeId: string | undefined
241+
route: string
242+
freshStorage: AccountStorage | null
243+
}> {
244+
let freshStorage: AccountStorage | null
245+
try {
246+
freshStorage = await loadFreshStorage()
247+
} catch (error) {
248+
logger.warn('sidebar', 'account storage reload failed; routing preserved', {
249+
message: error instanceof Error ? error.message : String(error),
250+
})
251+
const preservedRouting = existing.activeId
252+
? { activeId: existing.activeId, route: existing.route }
253+
: (fallbackRouting ?? { activeId: 'main', route: 'main' })
254+
return { ...preservedRouting, freshStorage: null }
255+
}
256+
257+
return {
258+
...resolveLoadedSidebarRouting(existing, freshStorage, fallbackRouting),
259+
freshStorage,
260+
}
261+
}
262+
263+
async function resolveInitialSidebarRouting(
264+
accountStoragePath: string,
265+
): Promise<{
266+
activeId: string | undefined
267+
route: string
268+
freshStorage: AccountStorage | null
269+
}> {
270+
await getInitialSidebarRoutingTestHooks()?.beforeStorageLoad?.()
271+
let freshStorage: AccountStorage | null
272+
try {
273+
freshStorage = await loadAccounts(accountStoragePath)
274+
} catch {
275+
return { activeId: 'main', route: 'main', freshStorage: null }
276+
} finally {
277+
await getInitialSidebarRoutingTestHooks()?.afterStorageLoad?.()
278+
}
279+
280+
await getInitialSidebarRoutingTestHooks()?.beforeSidebarRead?.()
281+
const existing = await getSidebarState()
282+
return {
283+
...resolveLoadedSidebarRouting(existing, freshStorage),
284+
freshStorage,
285+
}
286+
}
176287

177288
/**
178289
* Format the user-facing 429 message for a killswitch block. When the block
@@ -906,16 +1017,18 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
9061017
NonNullable<SidebarState['fableRecoveries']>[number]
9071018
>()
9081019

909-
function writeSidebarState(
1020+
interface SidebarStateInput {
1021+
activeId?: string
1022+
route: string
1023+
mainAccessToken?: string
1024+
mainRefreshToken?: string
1025+
routingAuthoritative?: boolean
1026+
}
1027+
1028+
function buildSidebarState(
9101029
storage: Awaited<ReturnType<typeof loadAccounts>>,
911-
options: {
912-
activeId?: string
913-
route: string
914-
mainAccessToken?: string
915-
mainRefreshToken?: string
916-
},
917-
) {
918-
lastSidebarRouting = { activeId: options.activeId, route: options.route }
1030+
options: SidebarStateInput,
1031+
): SidebarState {
9191032
quotaManager.updateStorage(storage)
9201033
quotaManager.seedMainFromStorage(storage, options.mainAccessToken)
9211034
quotaManager.seedFallbacksFromAccounts(
@@ -924,7 +1037,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
9241037
const mainEntry = quotaManager.getMain(options.mainAccessToken)
9251038
const lastApiError = quotaManager.getLastApiError()
9261039
const mainRefreshError = storage?.refresh?.mainLastRefreshError
927-
const state: SidebarState = {
1040+
return {
9281041
main: {
9291042
quota: mainEntry?.quota ?? null,
9301043
quotaBackedOff: quotaManager.isBackedOff(),
@@ -995,7 +1108,43 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
9951108
: undefined,
9961109
lastUpdated: Date.now(),
9971110
}
998-
return setSidebarState(state, sidebarStateFile).catch((error) =>
1111+
}
1112+
1113+
function writeSidebarState(
1114+
storage: Awaited<ReturnType<typeof loadAccounts>>,
1115+
options: SidebarStateInput,
1116+
) {
1117+
const routingAuthoritative = options.routingAuthoritative !== false
1118+
if (routingAuthoritative) {
1119+
lastSidebarRouting = { activeId: options.activeId, route: options.route }
1120+
}
1121+
const state = buildSidebarState(storage, options)
1122+
return setSidebarState(state, sidebarStateFile, {
1123+
routingAuthoritative,
1124+
resolvePreservedRouting: routingAuthoritative
1125+
? undefined
1126+
: async (current) => {
1127+
const preservedRouting = await resolveFreshSidebarRouting(
1128+
current,
1129+
() => loadAccounts(accountStoragePath),
1130+
{ activeId: state.activeId, route: state.route },
1131+
)
1132+
return {
1133+
activeId: preservedRouting.activeId,
1134+
route: preservedRouting.route,
1135+
state: buildSidebarState(preservedRouting.freshStorage, {
1136+
...options,
1137+
activeId: preservedRouting.activeId,
1138+
route: preservedRouting.route,
1139+
}),
1140+
}
1141+
},
1142+
onRoutingResolved: routingAuthoritative
1143+
? undefined
1144+
: (routing) => {
1145+
lastSidebarRouting = routing
1146+
},
1147+
}).catch((error) =>
9991148
logger.warn('sidebar', 'state write failed', {
10001149
error: error instanceof Error ? error.message : String(error),
10011150
}),
@@ -1023,6 +1172,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
10231172
route: lastSidebarRouting.route,
10241173
mainAccessToken: access,
10251174
mainRefreshToken: refresh,
1175+
routingAuthoritative: false,
10261176
})
10271177
}
10281178

@@ -1172,6 +1322,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
11721322
route: lastSidebarRouting.route,
11731323
mainAccessToken: auth.access,
11741324
mainRefreshToken: auth.refresh,
1325+
routingAuthoritative: false,
11751326
})
11761327
} catch {
11771328
// auth not yet available — sidebar will refresh on next request
@@ -1202,6 +1353,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
12021353
route: lastSidebarRouting.route,
12031354
mainAccessToken: auth.access,
12041355
mainRefreshToken: auth.refresh,
1356+
routingAuthoritative: false,
12051357
})
12061358

12071359
if (!desktopText || isTuiConnected(notice.sessionId)) return
@@ -1231,6 +1383,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
12311383
route: lastSidebarRouting.route,
12321384
mainAccessToken: auth.access,
12331385
mainRefreshToken: auth.refresh,
1386+
routingAuthoritative: false,
12341387
})
12351388
}
12361389

@@ -1584,6 +1737,7 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
15841737
route: lastSidebarRouting.route,
15851738
mainAccessToken: auth.access,
15861739
mainRefreshToken: auth.refresh,
1740+
routingAuthoritative: false,
15871741
})
15881742
} catch {
15891743
// auth not yet available — sidebar will refresh on next request
@@ -1941,10 +2095,11 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
19412095
? await latestGetAuth().catch(() => undefined)
19422096
: undefined
19432097
writeSidebarState(cmdStorage, {
1944-
activeId: 'main',
1945-
route: 'main',
2098+
activeId: lastSidebarRouting.activeId,
2099+
route: lastSidebarRouting.route,
19462100
mainAccessToken: cmdAuth?.access,
19472101
mainRefreshToken: cmdAuth?.refresh,
2102+
routingAuthoritative: false,
19482103
})
19492104
}
19502105
if (isTuiConnected(input.sessionID)) {
@@ -2360,11 +2515,14 @@ export const AnthropicAuthPlugin: Plugin = async (ctx) => {
23602515
quotaManager.seedFallbacksFromAccounts(
23612516
(initialStorage?.accounts ?? []).filter(isOAuthAccount),
23622517
)
2363-
writeSidebarState(initialStorage, {
2364-
activeId: 'main',
2365-
route: 'main',
2518+
const initialSidebarRouting =
2519+
await resolveInitialSidebarRouting(accountStoragePath)
2520+
await writeSidebarState(initialSidebarRouting.freshStorage, {
2521+
activeId: initialSidebarRouting.activeId,
2522+
route: initialSidebarRouting.route,
23662523
mainAccessToken: auth.access,
23672524
mainRefreshToken: auth.refresh,
2525+
routingAuthoritative: false,
23682526
})
23692527

23702528
function isReplayableRequest(

0 commit comments

Comments
 (0)