|
| 1 | +import type { ClawdbotConfig } from "../../config/config.js"; |
| 2 | +import { updateSessionStore, type SessionEntry } from "../../config/sessions.js"; |
| 3 | +import { normalizeProviderId } from "../model-selection.js"; |
| 4 | +import { |
| 5 | + ensureAuthProfileStore, |
| 6 | + isProfileInCooldown, |
| 7 | + resolveAuthProfileOrder, |
| 8 | +} from "../auth-profiles.js"; |
| 9 | + |
| 10 | +function isProfileForProvider(params: { |
| 11 | + provider: string; |
| 12 | + profileId: string; |
| 13 | + store: ReturnType<typeof ensureAuthProfileStore>; |
| 14 | +}): boolean { |
| 15 | + const entry = params.store.profiles[params.profileId]; |
| 16 | + if (!entry?.provider) return false; |
| 17 | + return normalizeProviderId(entry.provider) === normalizeProviderId(params.provider); |
| 18 | +} |
| 19 | + |
| 20 | +export async function clearSessionAuthProfileOverride(params: { |
| 21 | + sessionEntry: SessionEntry; |
| 22 | + sessionStore: Record<string, SessionEntry>; |
| 23 | + sessionKey: string; |
| 24 | + storePath?: string; |
| 25 | +}) { |
| 26 | + const { sessionEntry, sessionStore, sessionKey, storePath } = params; |
| 27 | + delete sessionEntry.authProfileOverride; |
| 28 | + delete sessionEntry.authProfileOverrideSource; |
| 29 | + delete sessionEntry.authProfileOverrideCompactionCount; |
| 30 | + sessionEntry.updatedAt = Date.now(); |
| 31 | + sessionStore[sessionKey] = sessionEntry; |
| 32 | + if (storePath) { |
| 33 | + await updateSessionStore(storePath, (store) => { |
| 34 | + store[sessionKey] = sessionEntry; |
| 35 | + }); |
| 36 | + } |
| 37 | +} |
| 38 | + |
| 39 | +export async function resolveSessionAuthProfileOverride(params: { |
| 40 | + cfg: ClawdbotConfig; |
| 41 | + provider: string; |
| 42 | + agentDir: string; |
| 43 | + sessionEntry?: SessionEntry; |
| 44 | + sessionStore?: Record<string, SessionEntry>; |
| 45 | + sessionKey?: string; |
| 46 | + storePath?: string; |
| 47 | + isNewSession: boolean; |
| 48 | +}): Promise<string | undefined> { |
| 49 | + const { |
| 50 | + cfg, |
| 51 | + provider, |
| 52 | + agentDir, |
| 53 | + sessionEntry, |
| 54 | + sessionStore, |
| 55 | + sessionKey, |
| 56 | + storePath, |
| 57 | + isNewSession, |
| 58 | + } = params; |
| 59 | + if (!sessionEntry || !sessionStore || !sessionKey) return sessionEntry?.authProfileOverride; |
| 60 | + |
| 61 | + const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); |
| 62 | + const order = resolveAuthProfileOrder({ cfg, store, provider }); |
| 63 | + let current = sessionEntry.authProfileOverride?.trim(); |
| 64 | + |
| 65 | + if (current && !store.profiles[current]) { |
| 66 | + await clearSessionAuthProfileOverride({ sessionEntry, sessionStore, sessionKey, storePath }); |
| 67 | + current = undefined; |
| 68 | + } |
| 69 | + |
| 70 | + if (current && !isProfileForProvider({ provider, profileId: current, store })) { |
| 71 | + await clearSessionAuthProfileOverride({ sessionEntry, sessionStore, sessionKey, storePath }); |
| 72 | + current = undefined; |
| 73 | + } |
| 74 | + |
| 75 | + if (current && order.length > 0 && !order.includes(current)) { |
| 76 | + await clearSessionAuthProfileOverride({ sessionEntry, sessionStore, sessionKey, storePath }); |
| 77 | + current = undefined; |
| 78 | + } |
| 79 | + |
| 80 | + if (order.length === 0) return undefined; |
| 81 | + |
| 82 | + const pickFirstAvailable = () => |
| 83 | + order.find((profileId) => !isProfileInCooldown(store, profileId)) ?? order[0]; |
| 84 | + const pickNextAvailable = (active: string) => { |
| 85 | + const startIndex = order.indexOf(active); |
| 86 | + if (startIndex < 0) return pickFirstAvailable(); |
| 87 | + for (let offset = 1; offset <= order.length; offset += 1) { |
| 88 | + const candidate = order[(startIndex + offset) % order.length]; |
| 89 | + if (!isProfileInCooldown(store, candidate)) return candidate; |
| 90 | + } |
| 91 | + return order[startIndex] ?? order[0]; |
| 92 | + }; |
| 93 | + |
| 94 | + const compactionCount = sessionEntry.compactionCount ?? 0; |
| 95 | + const storedCompaction = |
| 96 | + typeof sessionEntry.authProfileOverrideCompactionCount === "number" |
| 97 | + ? sessionEntry.authProfileOverrideCompactionCount |
| 98 | + : compactionCount; |
| 99 | + |
| 100 | + const source = |
| 101 | + sessionEntry.authProfileOverrideSource ?? |
| 102 | + (typeof sessionEntry.authProfileOverrideCompactionCount === "number" |
| 103 | + ? "auto" |
| 104 | + : current |
| 105 | + ? "user" |
| 106 | + : undefined); |
| 107 | + if (source === "user" && current && !isNewSession) { |
| 108 | + return current; |
| 109 | + } |
| 110 | + |
| 111 | + let next = current; |
| 112 | + if (isNewSession) { |
| 113 | + next = current ? pickNextAvailable(current) : pickFirstAvailable(); |
| 114 | + } else if (current && compactionCount > storedCompaction) { |
| 115 | + next = pickNextAvailable(current); |
| 116 | + } else if (!current || isProfileInCooldown(store, current)) { |
| 117 | + next = pickFirstAvailable(); |
| 118 | + } |
| 119 | + |
| 120 | + if (!next) return current; |
| 121 | + const shouldPersist = |
| 122 | + next !== sessionEntry.authProfileOverride || |
| 123 | + sessionEntry.authProfileOverrideSource !== "auto" || |
| 124 | + sessionEntry.authProfileOverrideCompactionCount !== compactionCount; |
| 125 | + if (shouldPersist) { |
| 126 | + sessionEntry.authProfileOverride = next; |
| 127 | + sessionEntry.authProfileOverrideSource = "auto"; |
| 128 | + sessionEntry.authProfileOverrideCompactionCount = compactionCount; |
| 129 | + sessionEntry.updatedAt = Date.now(); |
| 130 | + sessionStore[sessionKey] = sessionEntry; |
| 131 | + if (storePath) { |
| 132 | + await updateSessionStore(storePath, (store) => { |
| 133 | + store[sessionKey] = sessionEntry; |
| 134 | + }); |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + return next; |
| 139 | +} |
0 commit comments