dsh-agy 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accounts-BjdDqUq1.mjs","names":["ENC_PREFIX","parseFutureResetMs","fingerprintData","parseFutureResetMs"],"sources":["../src/runtime/classify.ts","../src/store/keyring.ts","../src/store/paths.ts","../src/runtime/rotation.ts","../src/runtime/version.ts","../src/oauth/refresh.ts","../src/runtime/quota.ts","../src/runtime/fingerprint-data.json","../src/runtime/fingerprint.ts","../src/runtime/risk.ts","../src/session.ts","../src/store/accounts.ts"],"sourcesContent":["/**\n * Upstream failure classification: HTTP status + headers + body → FailureKind.\n * Conservative by design: only auth-failure may revoke an account; everything\n * else cools, rotates, or retries.\n */\n\nimport { isProxyRouted } from '../types.ts'\nimport type { AccountRouting, FailureKind } from '../types.ts'\nimport { isProxyUnreachableError } from '../proxy.ts'\nimport { redactCredentials } from '../redact.ts'\n\nexport interface ClassifiedError {\n kind: FailureKind\n status?: number\n /** Server-provided retry delay (ms), when the response carried it. */\n retryAfterMs?: number\n /** Server-provided absolute reset time, when the response carried it. */\n resetTime?: string\n message?: string\n /** 429 sub-category, present when kind is rate-limit. */\n rateLimitCategory?: RateLimitCategory\n /**\n * Appeal/verification link from a `verification-required` body, when the\n * upstream supplied one. Surfaced to the user, never followed automatically.\n */\n verificationUrl?: string\n}\n\n/**\n * Phrases the upstream uses to ask for account verification rather than\n * rejecting a credential. Matched case-insensitively on the raw body.\n *\n * Kept as a phrase list (not a bare `includes('verify')`) so an ordinary\n * permission error is not mistaken for a recoverable challenge — the cost of a\n * false positive is a healthy account parked behind a timed block.\n */\nconst VERIFICATION_PHRASES = [\n 'validation_required',\n 'verify your account',\n 'verification required',\n 'verification_required',\n] as const\n\n/** Whether a 403 body asks the account owner to verify rather than meaning dead credentials. */\nexport function isVerificationRequired(bodyText: string | undefined): boolean {\n if (!bodyText) return false\n const text = bodyText.toLowerCase()\n return VERIFICATION_PHRASES.some((phrase) => text.includes(phrase))\n}\n\n/**\n * Pull the appeal/verification link out of a Google RPC error body.\n *\n * Prefers the structured location — `error.details[].metadata.validation_url`,\n * then `appeal_url` — and only falls back to the first bare `https://` match,\n * because a loose match can pick up an unrelated link. Google escapes `&` as\n * `\\u0026` inside the JSON string form, so the fallback unescapes it.\n */\nexport function extractVerificationUrl(bodyText: string | undefined): string | undefined {\n if (!bodyText) return undefined\n try {\n const data = JSON.parse(bodyText) as { error?: { details?: unknown } }\n const details = data.error?.details\n if (Array.isArray(details)) {\n for (const detail of details) {\n const meta = (detail as { metadata?: Record<string, unknown> } | null)?.metadata\n if (!meta) continue\n for (const field of ['validation_url', 'appeal_url'] as const) {\n const value = meta[field]\n if (typeof value === 'string' && value.length > 0) return value\n }\n }\n }\n } catch {\n // not JSON — fall through to the textual scan\n }\n // Unescape BEFORE matching: a URL inside a JSON string carries `&` as\n // `\\u0026`, and a pattern that rejects backslashes would truncate the link at\n // its first query separator — handing the user a broken appeal URL.\n const unescaped = bodyText.replace(/\\\\u0026/gi, '&')\n const match = /https:\\/\\/[^\\s\"'\\\\]+/.exec(unescaped)\n return match ? match[0] : undefined\n}\n\n/**\n * 429 sub-category (mirrors CLIProxyAPI/OmniRoute's classify429):\n * - soft_rate_limit: transient burst, retry immediately on the same account\n * - rate_limited: per-minute limit, short cooldown then retry (same account\n * when there is no other)\n * - quota_exhausted: daily/plan quota gone, long cooldown (24h)\n * - unknown: exponential backoff\n */\nexport type RateLimitCategory = 'soft_rate_limit' | 'rate_limited' | 'quota_exhausted' | 'unknown'\n\nconst QUOTA_EXHAUSTED_KEYWORDS = [\n 'quota_exhausted',\n 'quota exhausted',\n 'quota reached',\n 'enable overages',\n 'individual quota',\n]\n\n/** Classify a 429 body into the four upstream categories. */\nexport function classifyRateLimit(\n bodyText: string | undefined,\n retryAfterMs: number | undefined,\n): RateLimitCategory {\n const text = (bodyText ?? '').toLowerCase()\n if (QUOTA_EXHAUSTED_KEYWORDS.some((keyword) => text.includes(keyword))) {\n return 'quota_exhausted'\n }\n if (retryAfterMs !== undefined && retryAfterMs < 3000) {\n return 'soft_rate_limit'\n }\n if (retryAfterMs !== undefined) {\n return 'rate_limited'\n }\n // No explicit retry hint: \"quota\" wording → daily quota, otherwise unknown.\n return text.includes('quota') || text.includes('resource_exhausted') ? 'quota_exhausted' : 'unknown'\n}\n\nconst RATE_LIMIT_RESET_FIELDS = ['resetTime', 'reset_time', 'resetAt', 'quotaResetTime'] as const\n\nfunction extractResetTime(bodyText: string | undefined): string | undefined {\n if (!bodyText) return undefined\n try {\n const data = JSON.parse(bodyText) as Record<string, unknown>\n for (const field of RATE_LIMIT_RESET_FIELDS) {\n const value = data[field]\n if (typeof value === 'string' && value) return value\n if (typeof value === 'number' && Number.isFinite(value)) {\n // Unix ms when large, seconds when small (relative to now).\n return value > 1_000_000_000_000 ? new Date(value).toISOString() : new Date(Date.now() + value * 1000).toISOString()\n }\n }\n const quotaInfo = data.quotaInfo as Record<string, unknown> | undefined\n if (quotaInfo && typeof quotaInfo.resetTime === 'string') return quotaInfo.resetTime\n } catch {\n // not JSON — no reset info\n }\n return undefined\n}\n\nfunction parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined\n const seconds = Number(header)\n if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000\n const date = Date.parse(header)\n if (!Number.isNaN(date)) return Math.max(0, date - Date.now())\n return undefined\n}\n\n/** Classify a completed HTTP response (non-2xx). */\nexport function classifyHttpError(\n status: number,\n headers: Headers,\n bodyText?: string,\n): ClassifiedError {\n const retryAfterMs = parseRetryAfter(headers.get('retry-after'))\n const resetTime = extractResetTime(bodyText)\n\n if (status === 429) {\n const category = classifyRateLimit(bodyText, retryAfterMs)\n return {\n kind: 'rate-limit',\n rateLimitCategory: category,\n status,\n retryAfterMs,\n resetTime,\n message: bodyText ? bodyText.slice(0, 200) : undefined,\n }\n }\n if (status === 401) {\n return { kind: 'auth-failure', status, message: bodyText ? bodyText.slice(0, 200) : undefined }\n }\n if (status === 403) {\n // A verification wall is its own signal, checked BEFORE the auth-failure\n // fallback: the credential is fine, the account is temporarily gated, and the\n // body carries a URL the user can act on. Collapsing it into `auth-failure`\n // permanently disabled a healthy account on a signal that meant \"come back\n // after verifying\", with no automatic way back.\n if (isVerificationRequired(bodyText)) {\n return {\n kind: 'verification-required',\n status,\n verificationUrl: extractVerificationUrl(bodyText),\n message: bodyText ? bodyText.slice(0, 200) : undefined,\n }\n }\n // Google also reports quota walls as 403 RESOURCE_EXHAUSTED, and the\n // endpoint fallback chain ends on hosts answering 403 for \"no license\"\n // (not bad auth). Only treat a 403 as auth-failure when the body carries\n // no quota wording — a false revoke permanently disables the account.\n const category = classifyRateLimit(bodyText, undefined)\n if (category === 'quota_exhausted') {\n return {\n kind: 'rate-limit',\n rateLimitCategory: category,\n status,\n message: bodyText ? bodyText.slice(0, 200) : undefined,\n }\n }\n return { kind: 'auth-failure', status, message: bodyText ? bodyText.slice(0, 200) : undefined }\n }\n if (status === 404) {\n // Model/route missing — nothing to rotate for; surface as transient request error.\n return { kind: 'transient', status, message: bodyText ? bodyText.slice(0, 200) : undefined }\n }\n if (status >= 500) {\n return { kind: 'transient', status, retryAfterMs, message: bodyText ? bodyText.slice(0, 200) : undefined }\n }\n if (status === 400) {\n // A 400 is a request-construction error (permanent — retrying sends the\n // same broken payload). Only capacity-style phrases are recoverable\n // (context overflow / model unavailable), mirroring OmniRoute's\n // classifyProviderError: everything else surfaces as a terminal\n // request-error so rotation does not waste attempts.\n const text = (bodyText ?? '').toLowerCase()\n const recoverable = (\n (text.includes('context') && (text.includes('overflow') || text.includes('too long') || text.includes('exceeded')))\n || (text.includes('model') && (text.includes('not found') || text.includes('unavailable') || text.includes('not supported')))\n )\n if (recoverable) {\n return { kind: 'transient', status, message: bodyText ? bodyText.slice(0, 200) : undefined }\n }\n return { kind: 'request-error', status, message: bodyText ? bodyText.slice(0, 200) : undefined }\n }\n // 400-range others (401 handled above): request-level errors, no rotation semantics.\n return { kind: 'transient', status, message: bodyText ? bodyText.slice(0, 200) : undefined }\n}\n\n/**\n * Phrases the upstream returns when a conversation's SERVER-SIDE accumulated\n * input for one `sessionId` passes the 1M ceiling.\n *\n * This is not a request-construction error: the payload is fine, but the\n * upstream session that `sessionId` names has accumulated too much across turns.\n * The recovery is to derive a different `sessionId` (a \"generation\" bump) and\n * resend — hence this predicate is checked before {@link classifyHttpError} so\n * such a 400 is not collapsed into a terminal `request-error`.\n *\n * Matched on the measured upstream text; kept phrase-specific (rather than a\n * bare `includes('tokens')`) so a genuine model-capability 400 is not mistaken\n * for a bumpable session wall.\n */\nconst SESSION_ACCUMULATION_PHRASES = [\n 'exceeds the maximum number of tokens',\n 'input token count exceeds',\n 'token count exceeds the maximum',\n] as const\n\n/**\n * Whether a failed response is the recoverable per-`sessionId` accumulation\n * wall rather than a malformed request.\n *\n * @param status - HTTP status of the failed response.\n * @param bodyText - response body, when readable.\n * @returns true when a session-generation bump plus one resend can recover.\n */\nexport function isSessionAccumulationOverflow(status: number, bodyText: string | undefined): boolean {\n if (status !== 400 || !bodyText) return false\n const text = bodyText.toLowerCase()\n return SESSION_ACCUMULATION_PHRASES.some((phrase) => text.includes(phrase))\n}\n\n/** Codes that carry a socket/syscall `code` worth surfacing in a failure message. */\nconst TRANSPORT_CAUSE_CODES = new Set([\n 'ECONNREFUSED',\n 'ECONNRESET',\n 'ETIMEDOUT',\n 'ENETUNREACH',\n 'EHOSTUNREACH',\n 'EPIPE',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'CERT_HAS_EXPIRED',\n 'DEPTH_ZERO_SELF_SIGNED_CERT',\n 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',\n 'ERR_TLS_CERT_ALTNAME_INVALID',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_SOCKET',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n 'UND_ERR_ABORTED',\n])\n\n/**\n * Placeholder messages undici/Node emit while the real reason sits deeper in\n * the cause chain; they must not shadow a usable code further down.\n */\nconst GENERIC_CAUSE_MESSAGES = new Set(['fetch failed', 'terminated', 'other side closed', 'aborted'])\n\n/**\n * Walk the error cause chain collecting the actionable `code` and message.\n * Node/undici reports `TypeError: fetch failed` and stores the real reason in\n * `error.cause` (verified against live failures): without this, DNS, TLS,\n * timeout, reset, and proxy failures are indistinguishable in DSH session\n * events and the GUI.\n */\nfunction describeCause(error: unknown): { code?: string; message?: string } {\n const seen = new Set<unknown>()\n let current: unknown = (error as { cause?: unknown })?.cause ?? error\n let fallbackMessage: string | undefined\n // A recognized `code` anywhere wins over any message: the outer wrappers\n // (`TypeError: fetch failed`, or a refresh error repeating that text)\n // otherwise shadow the real reason.\n for (let depth = 0; current && depth < 6 && !seen.has(current); depth++) {\n seen.add(current)\n if (typeof current === 'object') {\n const code = (current as { code?: unknown }).code\n const message = (current as { message?: unknown }).message\n const usableMessage = typeof message === 'string' && message.length > 0 ? message : undefined\n if (typeof code === 'string' && TRANSPORT_CAUSE_CODES.has(code)) {\n return { code, message: usableMessage }\n }\n if (usableMessage && !GENERIC_CAUSE_MESSAGES.has(usableMessage) && !fallbackMessage) {\n fallbackMessage = usableMessage\n }\n // An AggregateError (Happy Eyeballs) carries its reasons in `errors`.\n const errors = (current as { errors?: unknown }).errors\n if (Array.isArray(errors)) {\n for (const nested of errors) {\n const described = describeCause({ cause: nested })\n if (described.code) return described\n if (described.message && !fallbackMessage) fallbackMessage = described.message\n }\n }\n }\n current = (current as { cause?: unknown })?.cause\n }\n return fallbackMessage ? { message: fallbackMessage } : {}\n}\n\n/**\n * Human-readable transport failure: `fetch failed (UND_ERR_SOCKET: other side\n * closed)`. The sanitized cause code/message is what makes a transient socket\n * error distinguishable from a DNS, TLS, or proxy failure in session records.\n */\nexport function describeFetchError(error: unknown): string {\n const base = error instanceof Error ? error.message : String(error)\n const { code, message } = describeCause(error)\n if (!code && !message) return redactCredentials(base)\n // Prefer the richer text: a syscall message already carrying the code\n // (\"getaddrinfo ENOTFOUND host\") keeps its host, while a bare reason\n // (\"other side closed\") is prefixed with the code.\n const detail = code && message\n ? (message.includes(code) ? message : `${code}: ${message}`)\n : (code ?? message)!\n // Skip a detail that merely repeats the base (\"fetch failed (fetch failed)\").\n if (detail === base) return redactCredentials(base)\n return redactCredentials(`${base} (${detail})`)\n}\n\n/**\n * Classify a fetch-level failure (DNS, refused, timeout, abort).\n * @param routing - how the failed request was routed; fail-closed applies only\n * when it carried an explicit per-account proxy (see {@link AccountRouting}).\n */\nexport function classifyFetchError(error: unknown, routing?: AccountRouting): ClassifiedError {\n const message = describeFetchError(error)\n if (error instanceof DOMException && error.name === 'AbortError') {\n return { kind: 'network-error', message }\n }\n // A tagged error is a definite proxy verdict (fast-fail pre-check, or a\n // dispatcher failure), so it stays authoritative in either context.\n const tagged = (error as { errorCode?: unknown })?.errorCode === 'proxy_unreachable'\n || (error as { code?: unknown })?.code === 'PROXY_UNREACHABLE'\n if (tagged) {\n return { kind: 'proxy-unreachable', message }\n }\n // Otherwise the bare socket codes in PROXY_UNREACHABLE_CODES are ambiguous:\n // they describe *the connection*, not *which* connection. Reading them as\n // \"proxy unreachable\" without a proxy in play skipped healthy accounts\n // un-cooldowned, so the context decides (issue #29).\n if (isProxyRouted(routing) && isProxyUnreachableError(error)) {\n return { kind: 'proxy-unreachable', message }\n }\n return { kind: 'network-error', message }\n}\n\n/** Classify the result of a refresh attempt (shared by adapter + verify paths). */\nexport function classifyRefreshFailure(status: number, code?: string): ClassifiedError {\n if (code === 'invalid_grant') return { kind: 'auth-failure', status, message: code }\n if (status === 429) return { kind: 'rate-limit', status, message: code }\n if (status >= 500) return { kind: 'transient', status, message: code }\n return { kind: 'transient', status, message: code }\n}\n\nexport type { FailureKind }\n","/**\n * Master-key management and AES-256-GCM secret codec for the account store.\n *\n * The master key lives in the DSH credentials document (`~/.dsh/.credentials.yaml`,\n * 0600) under `AGY_MASTER_KEY` so both the in-harness plugin (via\n * `ctx.credentials`) and the standalone `dsh-agy` CLI (direct file read) can\n * encrypt and decrypt the same account store.\n */\n\nimport { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto'\nimport { readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join, resolve } from 'node:path'\nimport { existsSync, mkdirSync, statSync } from 'node:fs'\nimport { Document, isMap, isScalar, parseDocument, type YAMLMap } from 'yaml'\n\nexport const MASTER_KEY_REF = 'AGY_MASTER_KEY'\n\n/** Encrypt/decrypt secrets at rest. */\nexport interface SecretCodec {\n encrypt(plaintext: string): string\n decrypt(payload: string): string\n}\n\nconst ENC_PREFIX = 'enc:v1:'\n\n/** AES-256-GCM codec; ciphertext format `enc:v1:<iv-b64>:<tag-b64>:<data-b64>`. */\nexport function createAesGcmCodec(key: Buffer): SecretCodec {\n if (key.length !== 32) {\n throw new Error(`createAesGcmCodec: master key must be 32 bytes, got ${key.length}`)\n }\n return {\n encrypt(plaintext: string): string {\n const iv = randomBytes(12)\n const cipher = createCipheriv('aes-256-gcm', key, iv)\n const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])\n const tag = cipher.getAuthTag()\n return `${ENC_PREFIX}${iv.toString('base64url')}:${tag.toString('base64url')}:${data.toString('base64url')}`\n },\n decrypt(payload: string): string {\n if (!payload.startsWith(ENC_PREFIX)) {\n throw new Error('decrypt: payload is not in encrypted format')\n }\n const [, , ivB64 = '', tagB64 = '', dataB64 = ''] = payload.split(':')\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivB64, 'base64url'))\n decipher.setAuthTag(Buffer.from(tagB64, 'base64url'))\n const plain = Buffer.concat([decipher.update(Buffer.from(dataB64, 'base64url')), decipher.final()])\n return plain.toString('utf8')\n },\n }\n}\n\n/** Derive a 32-byte key from an arbitrary master-key string (SHA-256). */\nexport function deriveKey(masterKey: string): Buffer {\n return createHash('sha256').update(masterKey, 'utf8').digest()\n}\n\n/** Default DSH home (`~/.dsh`), honoring `$DSH_HOME`. */\nexport function resolveDshHome(): string {\n return process.env.DSH_HOME ? resolve(process.env.DSH_HOME) : join(homedir(), '.dsh')\n}\n\nfunction homedir(): string {\n return process.env.HOME ?? process.env.USERPROFILE ?? '.'\n}\n\n/** A credentials reference name must be a POSIX-style identifier. */\nconst REF_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/\n\n/** The document's non-reference sections. */\nconst SECTION_KEYS = ['version', 'records'] as const\n\n/** Every top-level key DSH's credentials provider admits; anything else is rejected. */\nconst DOCUMENT_TOP_LEVEL = new Set<string>([...SECTION_KEYS, 'refs'])\n\n/**\n * Describe one YAML parse failure without quoting the source, which holds\n * secrets. The parser's own message embeds the offending line.\n */\nfunction describeYamlError(error: { code?: string; linePos?: { line: number; col: number }[] }): string {\n const at = error.linePos?.[0]\n return `${error.code ?? 'parse-error'}${at ? ` at line ${at.line}, column ${at.col}` : ''}`\n}\n\n/** One parsed credentials document, with the shape this build understands. */\ninterface CredentialsDocument {\n /** The mutable tree, for editing before re-serialization. */\n document: Document\n /** The root mapping. */\n root: YAMLMap\n /** The `refs` mapping, or undefined when absent, null, or not a mapping. */\n refs: YAMLMap | undefined\n /** True for a pre-release document: no `version` stamp, refs at top level. */\n flat: boolean\n}\n\n/**\n * Parse the credentials document and locate its `refs` section.\n *\n * The document is owned by DSH's credentials provider, which serializes it with\n * the `yaml` package. Values are folded across physical lines (a long\n * single-quoted scalar folds at whitespace) and `records` payloads nest\n * arbitrary keys, so this MUST be a real parse rather than a line scan: a\n * line-based reader cannot represent folding, and it lets a `records` payload\n * shadow a real top-level reference.\n *\n * Deliberately permissive: it validates only the shape it consumes, so the read\n * path keeps working on a document written by an older build. What may be\n * rewritten is enforced separately, in the writer.\n */\nfunction parseCredentialsDocument(text: string, file: string, context: string): CredentialsDocument {\n const document = parseDocument(text, { prettyErrors: true, uniqueKeys: true })\n if (document.errors.length > 0) {\n throw new Error(\n `${context}: cannot parse ${file}: ` + document.errors.map(describeYamlError).join('; '),\n )\n }\n const root = document.contents\n if (!isMap(root)) throw new Error(`${context}: ${file} must be a mapping`)\n\n const refs = root.get('refs')\n // The provider treats an absent or null section as empty.\n if (refs !== undefined && refs !== null && !isMap(refs)) {\n throw new Error(`${context}: \"refs\" in ${file} must be a mapping`)\n }\n // The pre-release layout is identified by the absence of the version stamp,\n // not by a missing `refs:` — a valid version-1 document may simply not have\n // written a `refs:` section yet.\n return { document, root, refs: isMap(refs) ? refs : undefined, flat: !root.has('version') }\n}\n\n/**\n * Read the references from the DSH credentials document.\n *\n * Only `refs` holds references, and a `records` payload must never be able to\n * shadow one. Two compatibility cases matter:\n *\n * - A pre-release \"flat\" document keeps its references at the top level; the\n * provider still reads that layout, so we do too.\n * - Version 0.2.7 of this plugin appended the master key as a *top-level* key.\n * The provider rejects such a document, but the key still decrypts an\n * existing account store, so it is read as a fallback. Dropping it would\n * silently mint a new master key and strand those accounts.\n *\n * Non-string and empty values are skipped, mirroring the provider.\n */\nexport function readCredentialsDocument(file: string): Map<string, string> {\n const entries = new Map<string, string>()\n if (!existsSync(file)) return entries\n const text = readFileSync(file, 'utf8')\n if (text.trim().length === 0) return entries\n const { root, refs, flat } = parseCredentialsDocument(text, file, 'readCredentialsDocument')\n\n const section = flat ? root : refs\n if (section) {\n for (const item of section.items) {\n if (!isScalar(item.key) || !isScalar(item.value)) continue\n const key = item.key.value\n const value = item.value.value\n if (typeof key !== 'string' || !REF_NAME.test(key)) continue\n // In the flat layout, `version`/`records` are sections, not references.\n if (flat && SECTION_KEYS.includes(key as (typeof SECTION_KEYS)[number])) continue\n if (typeof value !== 'string' || value.length === 0) continue\n entries.set(key, value)\n }\n }\n // The 0.2.7 top-level master key, only when `refs` does not already supply one.\n if (!flat && !entries.has(MASTER_KEY_REF)) {\n // Without `keepScalar`, a scalar node is unwrapped to its plain value.\n const legacy = root.get(MASTER_KEY_REF)\n if (typeof legacy === 'string' && legacy.length > 0) entries.set(MASTER_KEY_REF, legacy)\n }\n return entries\n}\n\n/**\n * POSIX owner-only enforcement. Windows mode bits never report 0600 (and\n * chmod is a no-op there), so the check is skipped on win32; the encrypted\n * account file and credentials document remain the only defense-in-depth\n * layer on that platform.\n */\nexport function assertOwnerOnly(file: string): void {\n if (process.platform === 'win32') return\n const mode = statSync(file).mode & 0o777\n if (mode !== 0o600) {\n throw new Error(\n `dsh-agy: ${file} is readable beyond its owner (mode ${mode.toString(8)}); ` +\n 'run \"chmod 600\" before starting again',\n )\n }\n}\n\n/**\n * Load the master key from the DSH credentials document. Returns undefined when\n * the document or the reference is absent.\n */\nexport function loadMasterKey(dshHome: string): string | undefined {\n const file = join(dshHome, '.credentials.yaml')\n if (!existsSync(file)) return undefined\n assertOwnerOnly(file)\n return readCredentialsDocument(file).get(MASTER_KEY_REF)\n}\n\n/** Top-level keys DSH's credentials provider admits; anything else is rejected. */\n/**\n * Render the version-1 layout for a pre-release \"flat\" document — a non-empty\n * top-level mapping of reference names to non-empty string scalars, with no\n * `version` key and no document directives. The original lines are nested\n * verbatim under `refs:`, so comments, blank lines, and each value's spelling\n * survive byte for byte. Returns undefined for anything the recognizer\n * declines, which the caller must then refuse to rewrite.\n *\n * Nesting whole lines is safe for block scalars: an explicit indent indicator\n * (`|2`) is absolute and the implied one is detected relative to the key, so\n * shifting every line by the same two spaces preserves the parsed value.\n */\nfunction renderFlatLayoutMigration(text: string): string | undefined {\n for (const line of text.split('\\n')) if (/^(%|---|\\.\\.\\.)/.test(line)) return undefined\n const document = parseDocument(text, { prettyErrors: true, uniqueKeys: true })\n if (document.errors.length > 0) return undefined\n const flat = document.contents\n if (!isMap(flat) || flat.items.length === 0) return undefined\n for (const pair of flat.items) {\n if (!isScalar(pair.key) || typeof pair.key.value !== 'string') return undefined\n if (DOCUMENT_TOP_LEVEL.has(pair.key.value) || !REF_NAME.test(pair.key.value)) return undefined\n if (!isScalar(pair.value) || typeof pair.value.value !== 'string') return undefined\n if (pair.value.value.length === 0) return undefined\n }\n const indented = text\n .split('\\n')\n .map((line) => (line.length === 0 ? line : ` ${line}`))\n .join('\\n')\n return `version: 1\\nrefs:\\n${indented}${text.endsWith('\\n') ? '' : '\\n'}`\n}\n\n/**\n * Render the credentials document with {@link MASTER_KEY_REF} added under `refs`.\n *\n * The key MUST be nested under `refs`: DSH's credentials provider rejects any\n * other top-level key, and a document it rejects makes *every* credential in\n * the file unreadable, not just ours. Editing the parsed document keeps the\n * comments and formatting of every untouched entry; a document this build\n * cannot prove it understands is never rewritten.\n */\nfunction renderWithMasterKey(existingText: string, masterKey: string, file: string): string {\n const fresh = (): string => new Document({ version: 1, refs: { [MASTER_KEY_REF]: masterKey } }).toString()\n if (existingText.trim().length === 0) return fresh()\n const { document, root, refs, flat } = parseCredentialsDocument(existingText, file, 'persistMasterKey')\n\n if (flat) {\n const migrated = renderFlatLayoutMigration(existingText)\n if (migrated === undefined) {\n throw new Error(\n `persistMasterKey: ${file} is not a credentials document this build can prove it ` +\n 'understands; refusing to rewrite it',\n )\n }\n const next = parseDocument(migrated)\n next.setIn(['refs', MASTER_KEY_REF], masterKey)\n return next.toString()\n }\n\n for (const key of root.items) {\n if (!isScalar(key.key) || typeof key.key.value !== 'string') continue\n if (!DOCUMENT_TOP_LEVEL.has(key.key.value)) {\n throw new Error(\n `persistMasterKey: ${file} has unknown top-level key \"${key.key.value}\"; refusing to rewrite it`,\n )\n }\n }\n // Never bump or downgrade the version stamp: the provider rejects any value\n // but 1 (\"this build reads version 1\"), so rewriting a future version to 1\n // would silently corrupt a document this build does not understand.\n const version = root.get('version')\n if (version !== 1) {\n throw new Error(\n `persistMasterKey: ${file} declares version ${JSON.stringify(version)}; refusing to rewrite it`,\n )\n }\n // An empty `refs:` parses to a null scalar, which `setIn` cannot descend\n // into; replacing the node materializes the map in place.\n if (refs) document.setIn(['refs', MASTER_KEY_REF], masterKey)\n else document.set('refs', { [MASTER_KEY_REF]: masterKey })\n return document.toString()\n}\n\n/**\n * Generate and persist a fresh master key (0600) in the DSH credentials document.\n *\n * The key is added under `refs` by editing the parsed document, then written\n * with an atomic tmp+rename, so every other credential — and the comments and\n * formatting DSH services rely on — survives. Never rewrite the whole file from\n * a rebuilt view: that would silently drop entries this build does not model.\n */\nexport function persistMasterKey(dshHome: string, masterKey: string): void {\n const file = join(dshHome, '.credentials.yaml')\n mkdirSync(dirname(file), { recursive: true })\n const existingText = existsSync(file) ? readFileSync(file, 'utf8') : ''\n if (existingText.length > 0 && readCredentialsDocument(file).has(MASTER_KEY_REF)) {\n throw new Error(`persistMasterKey: ${MASTER_KEY_REF} already exists in ${file}`)\n }\n const next = renderWithMasterKey(existingText, masterKey, file)\n const tmp = `${file}.tmp-masterkey`\n writeFileSync(tmp, next, { mode: 0o600 })\n renameSync(tmp, file)\n}\n\n/**\n * Resolve (load or create) the master key for a dsh home, then build the codec.\n * Creating a key writes the credentials document; read-only setups should call\n * {@link loadMasterKey} first and surface a friendly error instead.\n */\nexport function resolveMasterKeyCodec(dshHome: string): { codec: SecretCodec; created: boolean } {\n let masterKey = loadMasterKey(dshHome)\n let created = false\n if (!masterKey) {\n masterKey = randomBytes(32).toString('hex')\n persistMasterKey(dshHome, masterKey)\n created = true\n }\n return { codec: createAesGcmCodec(deriveKey(masterKey)), created }\n}\n","/**\n * The `agy` data folder: every data file this plugin creates lives under\n * `$DSH_HOME/agy/` instead of scattered across the DSH home.\n *\n * Why one folder: agy owned five JSON files sitting directly in `$DSH_HOME`\n * (accounts, stats, models, thinking, fingerprint override) next to the host's\n * own files. A folder is the boundary a user can see, back up, or delete whole.\n *\n * Migration is a ONE-SHOT ATOMIC RENAME, not a read-merge with lazy fallback:\n *\n * - `agy/` sits on the same filesystem as `$DSH_HOME`, so `rename` is atomic\n * and preserves the 0600 mode.\n * - A read-merge would need every store to carry two paths, two file locks,\n * and per-store merge logic — and the merge itself can still lose updates\n * (accounts mutate, stats counts).\n * - A lazy fallback reads the old path forever, which defeats the point: the\n * old file would never leave the home directory.\n *\n * Version-skew window contract: after a migration an OLD build recreates its\n * legacy file, and its writes are invisible to the new layout — the both-files\n * warning in `migrateToAgyDir` is the detector for exactly that (it fires when\n * a later process finds both files present). Local SOP re-syncs all profiles at\n * once, so the window is one app restart; npm users restart every dsh surface\n * per the release note. Accounts logged in by an old CLI during skew need one\n * re-login — enumerable, one-time, accepted in exchange for zero merge code.\n */\n\nimport { existsSync, mkdirSync, renameSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { resolveDshHome } from './keyring.ts'\n\n/** The directory under `$DSH_HOME` holding every agy data file. */\nexport function agyDataDir(dshHome: string = resolveDshHome()): string {\n return join(dshHome, 'agy')\n}\n\n/** A data file inside the agy directory. */\nexport function agyDataFile(name: string, dshHome: string = resolveDshHome()): string {\n return join(agyDataDir(dshHome), name)\n}\n\nexport interface AgyMigrationResult {\n /** The path callers must use from now on. */\n file: string\n /**\n * True when the legacy file was moved this call. The caller MAY log it; most\n * callers should stay silent — a migration is not news after it happened.\n */\n migrated: boolean\n /** Set when the old build's file still exists next to the new one (skew). */\n skew: boolean\n}\n\n/**\n * One-time legacy -> `agy/` migration for one data file.\n *\n * 1. New file already present: warn-once (via `skew`) when the legacy file is\n * ALSO present — an old-version process is still writing the old path.\n * Return the new path regardless: the new layout wins once it exists.\n * 2. No legacy file: fresh install (or already migrated), nothing to do.\n * 3. Otherwise create the directory and rename. On ANY failure (`agy/` cannot\n * be created, EXDEV, permissions, a locked target on Windows) fall back to\n * the LEGACY path for this process: for accounts, the old layout with the\n * real pool beats a new empty one, and `skew` surfaces the reason instead\n * of hiding it.\n *\n * The caller decides what `skew` becomes; `plugin-common.ts` and the CLI log\n * it once per process at most.\n */\nexport function migrateToAgyDir(name: string, dshHome: string = resolveDshHome()): AgyMigrationResult {\n const legacy = join(dshHome, name)\n const file = agyDataFile(name, dshHome)\n if (existsSync(file)) {\n return { file, migrated: false, skew: existsSync(legacy) }\n }\n if (!existsSync(legacy)) {\n return { file, migrated: false, skew: false }\n }\n try {\n mkdirSync(agyDataDir(dshHome), { recursive: true })\n renameSync(legacy, file)\n return { file, migrated: true, skew: false }\n } catch {\n return { file: legacy, migrated: false, skew: true }\n }\n}\n","/**\n * Rotation state machine: decides retry / cool / rotate / revoke for one failed\n * attempt, with tiered backoff and soft-quota pre-checks.\n */\n\nimport type { CachedQuota, FailureKind, ManagedAccount, RotationAction } from '../types.ts'\nimport type { RateLimitCategory } from './classify.ts'\n\nexport const BACKOFF_TIERS_MS = [5_000, 10_000, 20_000, 30_000, 60_000] as const\n\n/** Below this remaining fraction the account is treated as soft-quota-exhausted. */\nexport const SOFT_QUOTA_THRESHOLD = 0.15\n\n/**\n * The WEEKLY window's exhaustion threshold, and deliberately NOT\n * `SOFT_QUOTA_THRESHOLD`.\n *\n * The two numbers answer the same question — \"how much work is left in this\n * window?\" — about windows of very different lengths, so they only look\n * inconsistent when read side by side:\n *\n * 15% of a 5-hour window is ~45 minutes of runway.\n * 1% of a 7-day window is ~1.7 hours of runway.\n *\n * The weekly bar is the stricter one where it matters, because being wrong is\n * asymmetric: a drained 5-hour window refills within five hours, while a drained\n * weekly window parks the account for DAYS. Treating an account as drained a\n * little early costs one rotation; treating it as usable too late costs the user\n * a failed request with no healthy fallback.\n *\n * Do not \"unify\" these two constants without re-deriving both runways above.\n */\nexport const WEEKLY_QUOTA_THRESHOLD = 0.01\n\n/**\n * Concurrent upstream requests one account may carry before selection prefers\n * another. A real Antigravity window serves one conversation at a time, so\n * several simultaneous streams from one account is a shape the official client\n * does not produce. The cap is generous on purpose: it should only bite under\n * genuine fan-out (many parallel conversations), not on ordinary tool loops.\n */\nexport const MAX_IN_FLIGHT_PER_ACCOUNT = 3\n\n/**\n * In-flight accounting older than this is treated as leaked and discarded.\n *\n * The counter is released by the adapter's `finally`, which runs on completion,\n * error, and early consumer termination alike — but a process killed mid-request\n * cannot release anything, and a stuck counter would permanently deprioritize an\n * account. The sweep bounds that to one TTL.\n */\nexport const IN_FLIGHT_STALE_MS = 10 * 60 * 1000\n\nfunction backoffFor(consecutiveFailures: number, maxJitterMs = 1_000): number {\n const index = Math.min(Math.max(consecutiveFailures, 0), BACKOFF_TIERS_MS.length - 1)\n const base = BACKOFF_TIERS_MS[index] ?? BACKOFF_TIERS_MS[BACKOFF_TIERS_MS.length - 1]!\n return base + Math.floor(Math.random() * maxJitterMs)\n}\n\n/** Whether the account is currently in a cooldown window. */\nexport function isCoolingDown(account: ManagedAccount, now = Date.now()): boolean {\n return (account.coolingDownUntil ?? 0) > now\n}\n\n/** Whether the requested model family on this account is rate-limited. */\nexport function isFamilyRateLimited(account: ManagedAccount, family: string | undefined, now = Date.now()): boolean {\n if (!family) return false\n const resetAt = account.rateLimitResetTimes?.[family]\n return typeof resetAt === 'number' && resetAt > now\n}\n\n/**\n * Proxy for a pool-level probe that belongs to no single account (the version\n * feeds): the account that would serve the next request, else the first usable\n * one.\n *\n * These feeds are not account-scoped, but the host's IP is what a user who\n * configured per-account proxies asked to hide, and a probe on the env/direct\n * route leaks it at boot. `undefined` means \"no account route\" — no accounts, or\n * no usable one — and the caller then uses the env/direct route, which is also\n * where an unproxied account's traffic goes anyway.\n *\n * Deliberately not model-aware: the probe runs once at boot, before any model is\n * requested.\n */\nexport function pickProbeProxyUrl(\n accounts: ManagedAccount[],\n activeIndex: number,\n now = Date.now(),\n): string | undefined {\n const usable = (account: ManagedAccount | undefined): boolean =>\n account !== undefined && account.enabled !== false && !isCoolingDown(account, now)\n const active = accounts[activeIndex]\n if (usable(active)) return active!.proxy\n return accounts.find((account) => usable(account))?.proxy\n}\n\n/** Record a rate-limit reset for one model key, retaining the latest reset time. */\nexport function recordRateLimit(account: ManagedAccount, modelKey: string, resetAtMs: number): void {\n const current = account.rateLimitResetTimes?.[modelKey] ?? 0\n account.rateLimitResetTimes = {\n ...(account.rateLimitResetTimes ?? {}),\n [modelKey]: Math.max(current, resetAtMs),\n }\n}\n\n/** Clear expired rate limits and cooldowns in place. */\nexport function clearExpiredState(account: ManagedAccount, now = Date.now()): void {\n if (account.rateLimitResetTimes) {\n const fresh = Object.fromEntries(\n Object.entries(account.rateLimitResetTimes).filter(([, reset]) => reset > now),\n )\n account.rateLimitResetTimes = Object.keys(fresh).length > 0 ? fresh : undefined\n }\n if (account.coolingDownUntil && account.coolingDownUntil <= now) {\n account.coolingDownUntil = undefined\n account.cooldownReason = undefined\n // The age is only meaningful while the window it describes is live; leaving\n // it behind would let a stale timestamp pair with a future cooldown.\n account.cooldownSetAt = undefined\n }\n}\n\n/** 24h cooldown for a fully exhausted daily quota (single-account: stop hitting the wall). */\nexport const FULL_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000\n/** 5min cooldown for per-minute rate limits. */\nexport const RATE_LIMIT_COOLDOWN_MS = 5 * 60 * 1000\n/** Cap for a server-reported reset time on per-minute limits (guards against bogus far-future values). */\nexport const MAX_RATE_LIMIT_COOLDOWN_MS = 30 * 60 * 1000\n\n/**\n * How long a verification challenge parks an account before the pool tries it\n * again. Deliberately short and timed rather than permanent: the credential is\n * intact, the wall is upstream's, and it can clear on its own — so the account\n * returns to service without the user doing anything, while still not being\n * hammered in the meantime.\n */\nexport const VERIFICATION_COOLDOWN_MS = 15 * 60 * 1000\n\n/** Absolute server-reported reset in ms when it lies in the future, else undefined. */\nexport function parseFutureResetMs(resetTime: string | undefined, now = Date.now()): number | undefined {\n if (!resetTime) return undefined\n const reset = Date.parse(resetTime)\n if (Number.isNaN(reset) || reset <= now) return undefined\n return reset\n}\n\n/**\n * Decide what to do after one failed attempt.\n * @param kind - classified failure kind.\n * @param category - 429 sub-category when kind is rate-limit.\n * @param account - the account that failed (mutated with cooldown/rate-limit state).\n * @param consecutiveFailures - consecutive failures on this account.\n * @param retryAfterMs - server-provided retry delay when present.\n * @param resetTime - server-provided absolute reset time; cooldowns use it (capped) instead of fixed windows.\n */\nexport function decideRotation(\n kind: FailureKind,\n account: ManagedAccount,\n consecutiveFailures: number,\n retryAfterMs?: number,\n category: RateLimitCategory = 'unknown',\n resetTime?: string,\n): RotationAction {\n const now = Date.now()\n const backoffMs = backoffFor(consecutiveFailures)\n\n switch (kind) {\n case 'rate-limit': {\n if (category === 'soft_rate_limit') {\n // Transient burst: retry the same account almost immediately.\n return { action: 'retry', backoffMs: Math.min(retryAfterMs ?? backoffMs, 3000) }\n }\n if (category === 'quota_exhausted') {\n // Daily/plan quota gone: cool until the real reset when the backend\n // reported one (capped at 24h), else the fixed daily window.\n const resetMs = parseFutureResetMs(resetTime, now)\n const cooldownMs = resetMs !== undefined\n ? Math.min(resetMs - now, FULL_QUOTA_COOLDOWN_MS)\n : FULL_QUOTA_COOLDOWN_MS\n account.coolingDownUntil = now + Math.max(cooldownMs, 60_000)\n account.cooldownReason = 'quota-exhausted'\n account.cooldownSetAt = now\n return { action: 'cool', backoffMs: Math.max(cooldownMs, 60_000) }\n }\n // Per-minute rate limit: prefer the server's real reset (capped), then\n // Retry-After, then the fixed short window. The family-scoped reset is\n // recorded in account.rateLimitResetTimes, so other model families on\n // this account stay unblocked (AuthStorage-aligned).\n const resetMs = parseFutureResetMs(resetTime, now)\n const cooldownMs = resetMs !== undefined\n ? Math.min(resetMs - now, MAX_RATE_LIMIT_COOLDOWN_MS)\n : (retryAfterMs ?? RATE_LIMIT_COOLDOWN_MS)\n return { action: 'rotate', backoffMs: Math.max(cooldownMs, 1000) }\n }\n case 'auth-failure': {\n // Terminal: account credentials are dead; never auto-recover.\n //\n // One `invalid_grant` is enough, and that is a decision: the code means the\n // refresh token no longer works, and rotating/re-probing a dead credential\n // only burns requests. Another implementation demands repeated\n // confirmation, but it has more failure modes to tell apart (its own\n // decrypt path, a swappable OAuth app); here a false positive costs one\n // `dsh-agy verify`, which re-enables the account on success.\n account.verificationRequired = true\n account.verificationRequiredAt = now\n account.verificationRequiredReason = 'auth-failure'\n account.enabled = false\n return { action: 'revoke' }\n }\n case 'verification-required': {\n // Recoverable, and NOT a credential failure: the upstream is asking the\n // account owner to verify. Park the account for a timed window instead of\n // disabling it, so recovery needs no human action once the wall clears,\n // and keep the challenge state (plus the appeal URL, written by the caller)\n // so the UI can tell the user what happened.\n account.coolingDownUntil = now + VERIFICATION_COOLDOWN_MS\n account.cooldownReason = 'validation-required'\n account.cooldownSetAt = now\n account.verificationRequired = true\n account.verificationRequiredAt = now\n account.verificationRequiredReason = 'validation-required'\n return { action: 'cool', backoffMs: VERIFICATION_COOLDOWN_MS }\n }\n case 'network-error': {\n account.coolingDownUntil = now + backoffMs\n account.cooldownReason = 'network-error'\n account.cooldownSetAt = now\n return { action: 'rotate', backoffMs }\n }\n case 'project-error': {\n account.coolingDownUntil = now + backoffMs\n account.cooldownReason = 'project-error'\n account.cooldownSetAt = now\n return { action: 'cool', backoffMs }\n }\n case 'request-error': {\n // Request-construction error (e.g. generic 400): permanent, retrying\n // resends the same broken payload. No cooldown, no rotation, no revoke —\n // the adapter surfaces it as a terminal UPSTREAM error.\n return { action: 'noop' }\n }\n case 'transient': {\n return { action: 'retry', backoffMs }\n }\n case 'proxy-unreachable': {\n // Per-account proxy dead: fail-closed, skip this account for this request only.\n // Do NOT write coolingDownUntil / rateLimitResetTimes — next request may retry.\n return { action: 'rotate', backoffMs: Math.min(backoffMs, 1000) }\n }\n }\n}\n\n/**\n * Pick the next account index for rotation (round-robin across enabled,\n * non-cooling accounts; falls back to the active one when all are cooling).\n */\nexport function pickNextAccountIndex(\n accounts: ManagedAccount[],\n currentIndex: number,\n now = Date.now(),\n modelOrFamily?: string,\n): number {\n if (accounts.length <= 1) return currentIndex\n const enabled = accounts.map((a, i) => ({ account: a, index: i }))\n .filter(({ account, index }) => {\n if (index === currentIndex || account.enabled === false) return false\n if (isCoolingDown(account, now)) return false\n if (modelOrFamily && isFamilyRateLimited(account, modelOrFamily, now)) return false\n return true\n })\n if (enabled.length === 0) return currentIndex\n // Round-robin: first candidate after current index, else first eligible.\n const after = enabled.find((e) => e.index > currentIndex)\n return (after ?? enabled[0]!)!.index\n}\n\n/**\n * Build the soft-quota cache TTL: short when low, long when healthy.\n *\n * Driven by the MOST pressured of the two windows, because they drain on\n * different clocks: an account sitting at 90% of its 5-hour bucket but 2% of its\n * week still has to be re-measured on the short interval, or a weekly exhaustion\n * is discovered only when a request fails.\n *\n * `weeklyFraction` is the optional SECOND parameter, so the two fractions sit\n * adjacent.\n *\n * Takes NO timestamp on purpose: deciding which fractions are still meaningful is\n * `isQuotaStale`'s job, because a window whose reset has passed must not drive the\n * interval either. It applies that guard before calling in here, so both\n * arguments this function sees describe windows that still exist.\n */\nexport function computeSoftQuotaCacheTtlMs(\n remainingFraction: number | undefined,\n weeklyFraction?: number,\n): number {\n if (typeof remainingFraction === 'number' && remainingFraction < SOFT_QUOTA_THRESHOLD) return 60 * 1000\n if (typeof weeklyFraction === 'number' && weeklyFraction <= WEEKLY_QUOTA_THRESHOLD) return 60 * 1000\n if (typeof remainingFraction !== 'number' && typeof weeklyFraction !== 'number') return 10 * 60 * 1000\n if (typeof remainingFraction === 'number' && remainingFraction < 0.5) return 5 * 60 * 1000\n if (typeof weeklyFraction === 'number' && weeklyFraction < 0.2) return 5 * 60 * 1000\n return 15 * 60 * 1000\n}\n\nexport type { CachedQuota }\n","/**\n * Fresh Antigravity client version resolution: fetch from the product's own\n * release feeds (6h TTL, single in-flight dedupe), falling back to the pinned\n * pool. Keeps fingerprint User-Agent version strings current — a stale version\n * is the most detectable fingerprint anomaly.\n */\n\nimport { AGY_VERSION_FALLBACK, antigravityUserAgent, setResolvedAgyVersion } from '../oauth/constants.ts'\nimport { proxiedFetch } from '../proxy.ts'\n\nconst VERSION_CACHE_TTL_MS = 6 * 60 * 60 * 1000\nconst VERSION_FETCH_TIMEOUT_MS = 5_000\n\n/** Source 1: Antigravity IDE auto-update release feed (array of {version}). */\nconst IDE_RELEASE_FEED_URL = 'https://antigravity-auto-updater-974169037036.us-central1.run.app/releases'\n/** Source 2: agy CLI GitHub releases (object with tag_name). */\nconst CLI_RELEASE_URL = 'https://api.github.com/repos/google-antigravity/antigravity-cli/releases/latest'\n\nexport interface FetchLike {\n (input: string | URL, init?: RequestInit): Promise<Response>\n}\n\nexport interface VersionState {\n cache?: { version: string; fetchedAt: number }\n inFlight: Promise<string | undefined> | null\n}\n\nfunction compareSemver(a: string, b: string): number {\n const pa = a.split('.').map((n) => Number(n) || 0)\n const pb = b.split('.').map((n) => Number(n) || 0)\n for (let i = 0; i < 3; i++) {\n const diff = (pa[i] ?? 0) - (pb[i] ?? 0)\n if (diff !== 0) return diff\n }\n return 0\n}\n\nfunction pickNewestVersion(...versions: Array<unknown>): string | null {\n const valid = versions\n .filter((v): v is string => typeof v === 'string' && /^\\d+\\.\\d+\\.\\d+$/.test(v))\n if (valid.length === 0) return null\n return valid.reduce<string | null>((best, v) => (!best || compareSemver(v, best) > 0 ? v : best), null)\n}\n\n/**\n * Highest version published so far in this process.\n *\n * Kept locally so publication is monotonic: the two feeds resolve independently\n * and either may finish first, so without this the slower (possibly older) source\n * could lower what a User-Agent already advertises. A version never goes\n * backwards here — only the cold-start floor can be below a published value.\n */\nlet publishedVersion: string | undefined\n\n/**\n * Publish a newly observed version into the shared slot every User-Agent builder\n * reads ({@link setResolvedAgyVersion}), never lowering an already-published one.\n */\nfunction publishVersion(version: string | null | undefined): void {\n if (version === null || version === undefined) return\n const best = pickNewestVersion(version, publishedVersion)\n if (best === null || best === publishedVersion) return\n publishedVersion = best\n setResolvedAgyVersion(best)\n}\n\nfunction parseIdeReleaseFeed(payload: unknown): string | null {\n if (!Array.isArray(payload)) return null\n return pickNewestVersion(...payload.map((entry) => (entry as { version?: unknown })?.version))\n}\n\nfunction parseCliRelease(payload: unknown): string | null {\n if (!payload || typeof payload !== 'object') return null\n const release = payload as { name?: unknown; tag_name?: unknown }\n return pickNewestVersion(release.tag_name ?? release.name)\n}\n\nasync function fetchJsonWithTimeout(fetchImpl: FetchLike, url: string): Promise<unknown> {\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), VERSION_FETCH_TIMEOUT_MS)\n try {\n const response = await fetchImpl(url, {\n // Both feeds are services the official Antigravity client ecosystem\n // already talks to, so the client User-Agent is the consistent choice.\n // This previously sent a hardcoded `dsh-agy/0.1` — a self-identifying\n // string that also never tracked the package version.\n headers: { Accept: 'application/json', 'User-Agent': antigravityUserAgent() },\n signal: controller.signal,\n })\n if (!response.ok) throw new Error(`Version source ${url} returned ${response.status}`)\n return response.json()\n } finally {\n clearTimeout(timeoutId)\n }\n}\n\n/**\n * Resolve the newest OBSERVED version from one feed, or `undefined` when the\n * feed yielded nothing.\n *\n * Returning `undefined` rather than the pinned fallback is what keeps the\n * fallback out of {@link resolveAntigravityVersion}'s cross-source `max`. When\n * this returned the fallback, a source that simply failed to parse contributed\n * the fallback as though it were evidence, and once the fallback was raised to\n * track the live feed it started outranking genuine — but older — feed data.\n * The fallback is a last resort and is applied once, at the public boundary.\n */\nasync function resolveObservedVersion(\n state: VersionState,\n sourceUrl: string,\n parse: (payload: unknown) => string | null,\n fetchImpl: FetchLike,\n): Promise<string | undefined> {\n const now = Date.now()\n if (state.cache && now - state.cache.fetchedAt < VERSION_CACHE_TTL_MS) {\n publishVersion(state.cache.version)\n return state.cache.version\n }\n if (state.inFlight) return state.inFlight\n\n state.inFlight = (async () => {\n let resolved: string | null = null\n try {\n resolved = parse(await fetchJsonWithTimeout(fetchImpl, sourceUrl))\n } catch {\n resolved = null\n }\n const observed = pickNewestVersion(resolved, state.cache?.version)\n if (resolved) {\n state.cache = { version: observed ?? resolved, fetchedAt: Date.now() }\n // Publish only a genuinely observed version; the fallback is not evidence.\n publishVersion(resolved)\n }\n return observed ?? undefined\n })()\n\n try {\n return await state.inFlight\n } finally {\n state.inFlight = null\n }\n}\n\n/** Resolve the newest known Antigravity version from the IDE feed. */\nexport async function resolveAntigravityIdeVersion(fetchImpl: FetchLike = proxiedFetch): Promise<string> {\n return (await resolveObservedVersion(ideState, IDE_RELEASE_FEED_URL, parseIdeReleaseFeed, fetchImpl))\n ?? AGY_VERSION_FALLBACK\n}\n\n/** Resolve the newest known Antigravity version from the CLI releases. */\nexport async function resolveAntigravityCliVersion(fetchImpl: FetchLike = proxiedFetch): Promise<string> {\n return (await resolveObservedVersion(cliState, CLI_RELEASE_URL, parseCliRelease, fetchImpl))\n ?? AGY_VERSION_FALLBACK\n}\n\n/**\n * The newest version actually OBSERVED on the claimed product's feed, or\n * `undefined` when it yielded nothing.\n *\n * Reads the **CLI feed only**. This used to take the numeric max across the IDE\n * and CLI feeds, which is a category error: the three Antigravity lines are\n * separate version namespaces (IDE 2.x, hub 2.15.x, CLI 1.2.x), so \"newest\"\n * across them compares unrelated numbers and picks whichever line happens to\n * count higher. Since this client claims to be the CLI\n * (`docs/official-identity.json`), the CLI feed is the only one that can\n * describe the version we are entitled to advertise; the IDE resolver stays\n * exported for the freshness gate's cross-check, never for the wire.\n *\n * Distinct from {@link resolveAntigravityVersion}, which substitutes the pinned\n * fallback at the boundary. A caller that must tell \"observed\" from \"assumed\"\n * cannot use that one — and the freshness gate is exactly such a caller, since\n * the fallback is the value under test.\n */\nexport async function resolveObservedAgyVersion(fetchImpl: FetchLike = proxiedFetch): Promise<string | undefined> {\n return (await resolveObservedVersion(cliState, CLI_RELEASE_URL, parseCliRelease, fetchImpl)) ?? undefined\n}\n\n/** Best available version: the claimed product line, cached 6h. */\nexport async function resolveAntigravityVersion(fetchImpl: FetchLike = proxiedFetch): Promise<string> {\n return (await resolveObservedAgyVersion(fetchImpl)) ?? AGY_VERSION_FALLBACK\n}\n\n/**\n * Bounded resolve for fingerprint generation: never block the failure path on\n * cold version feeds. Resolves undefined on timeout/error so callers fall\n * back to the pinned version pool; the abandoned fetch keeps running and\n * populates the 6h cache for later calls.\n *\n * `fetchImpl` routes the probe: the feeds belong to no account, so the caller\n * decides which egress they use (the CLI passes the login proxy, the plugin\n * passes the pool's representative account).\n */\nexport async function resolveAntigravityVersionBounded(\n timeoutMs = 750,\n fetchImpl: FetchLike = proxiedFetch,\n): Promise<string | undefined> {\n let timer: NodeJS.Timeout | undefined\n try {\n return await Promise.race([\n resolveAntigravityVersion(fetchImpl).catch(() => undefined),\n new Promise<undefined>((resolve) => {\n timer = setTimeout(() => resolve(undefined), timeoutMs)\n }),\n ])\n } finally {\n clearTimeout(timer)\n }\n}\n\n/** Synchronously peek at a fresh cached version (no network). */\nexport function peekCachedAntigravityVersion(): string | undefined {\n const now = Date.now()\n for (const state of [cliState, ideState]) {\n if (state.cache && now - state.cache.fetchedAt < VERSION_CACHE_TTL_MS) {\n // A fresh cache is an observation too — publish it so a synchronous peek\n // (the fingerprint failure path) reaches User-Agent builders without I/O.\n publishVersion(state.cache.version)\n return state.cache.version\n }\n }\n return undefined\n}\n\nconst ideState: VersionState = { inFlight: null }\nconst cliState: VersionState = { inFlight: null }\n\n/**\n * Drop both feeds' cached state so a test can exercise the cold-start path.\n *\n * `inFlight` is cleared with the cache: a test that lets a probe hang leaves its\n * single-flight promise pending, and the next test's resolve would return THAT\n * promise instead of issuing its own request — silently testing nothing.\n *\n * Deliberately does NOT clear the published version slot: that one is monotonic\n * process state, and dropping it would let a test's User-Agent move backwards.\n */\nexport function _clearVersionCacheForTest(): void {\n ideState.cache = undefined\n cliState.cache = undefined\n ideState.inFlight = null\n cliState.inFlight = null\n}\n","/**\n * Refresh an agy access token, handling Google's varied error payload shapes and\n * marking revocation (`invalid_grant`) distinctly so callers can drop the account.\n */\n\nimport type { OAuthAuthDetails } from '../types.ts'\nimport { calculateTokenExpiry, formatRefreshParts, parseRefreshParts } from './auth.ts'\nimport { AGY_CLIENT_ID, OAUTH_TOKEN_URL, resolveAgyClientCredentials } from './constants.ts'\nimport { proxiedFetch } from '../proxy.ts'\n\ninterface OAuthErrorPayload {\n error?:\n | string\n | {\n code?: string\n status?: string\n message?: string\n }\n error_description?: string\n}\n\nexport class AgyTokenRefreshError extends Error {\n code?: string\n description?: string\n status: number\n statusText: string\n\n constructor(options: {\n message: string\n code?: string\n description?: string\n status: number\n statusText: string\n }) {\n super(options.message)\n this.name = 'AgyTokenRefreshError'\n this.code = options.code\n this.description = options.description\n this.status = options.status\n this.statusText = options.statusText\n }\n}\n\n/** Parse Google token-endpoint error payloads, tolerating varied shapes. */\nexport function parseOAuthErrorPayload(text: string | undefined): { code?: string; description?: string } {\n if (!text) return {}\n\n try {\n const payload = JSON.parse(text) as OAuthErrorPayload\n if (!payload || typeof payload !== 'object') {\n return { description: text }\n }\n\n let code: string | undefined\n if (typeof payload.error === 'string') {\n code = payload.error\n } else if (payload.error && typeof payload.error === 'object') {\n code = payload.error.status ?? payload.error.code\n if (!payload.error_description && payload.error.message) {\n return { code, description: payload.error.message }\n }\n }\n\n const description = payload.error_description\n if (description) return { code, description }\n\n if (payload.error && typeof payload.error === 'object' && payload.error.message) {\n return { code, description: payload.error.message }\n }\n\n return { code }\n } catch {\n return { description: text }\n }\n}\n\nexport type RefreshResult =\n | { type: 'success'; auth: OAuthAuthDetails; clientId: string }\n | { type: 'revoked' }\n | { type: 'failed'; error: AgyTokenRefreshError }\n\n/**\n * Refresh the access token for an account. `revoked` means Google rejected the\n * refresh token (`invalid_grant`) — the account must be re-authenticated.\n * For legacy accounts without a stored clientId, embedded credentials are tried\n * first with an env-override fallback before treating invalid_grant as fatal.\n */\nexport async function refreshAccessToken(auth: OAuthAuthDetails, options?: { clientId?: string; proxyUrl?: string }): Promise<RefreshResult> {\n const parts = parseRefreshParts(auth.refresh)\n if (!parts.refreshToken) {\n return { type: 'failed', error: new AgyTokenRefreshError({\n message: 'Missing refresh token',\n status: 400,\n statusText: 'Bad Request',\n }) }\n }\n\n // Candidate client IDs to try: explicit stored clientId first; for legacy\n // accounts, try embedded first, then env override (if distinct).\n const candidateIds = options?.clientId\n ? [options.clientId]\n : Array.from(new Set([AGY_CLIENT_ID, process.env.AGY_CLIENT_ID].filter((id): id is string => Boolean(id && id.length > 0))))\n\n let revokedCount = 0\n let transientFailure: RefreshResult | undefined\n\n for (const currentClientId of candidateIds) {\n try {\n const startTime = Date.now()\n const { clientId, clientSecret } = resolveAgyClientCredentials(currentClientId)\n const response = await proxiedFetch(OAUTH_TOKEN_URL, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: parts.refreshToken,\n client_id: clientId,\n client_secret: clientSecret,\n }),\n }, options?.proxyUrl ? { proxyUrl: options.proxyUrl } : undefined)\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => undefined)\n const { code, description } = parseOAuthErrorPayload(errorText)\n const details = [code, description ?? errorText].filter(Boolean).join(': ')\n const baseMessage = `Agy token refresh failed (${response.status} ${response.statusText})`\n const message = details ? `${baseMessage} - ${details}` : baseMessage\n\n if (code === 'invalid_grant') {\n revokedCount++\n continue // try next candidate if available before revoking\n }\n\n transientFailure = {\n type: 'failed',\n error: new AgyTokenRefreshError({\n message,\n code,\n description: description ?? errorText,\n status: response.status,\n statusText: response.statusText,\n }),\n }\n continue\n }\n const payload = (await response.json()) as {\n access_token: string\n expires_in?: number\n refresh_token?: string\n }\n\n const refreshedParts = {\n refreshToken: payload.refresh_token ?? parts.refreshToken,\n projectId: parts.projectId,\n managedProjectId: parts.managedProjectId,\n }\n\n return {\n type: 'success',\n auth: {\n access: payload.access_token,\n expires: calculateTokenExpiry(startTime, payload.expires_in),\n refresh: formatRefreshParts(refreshedParts),\n },\n clientId,\n }\n } catch (error) {\n const raw = error instanceof Error ? error : new Error(String(error))\n const code = (error as unknown as { code?: string })?.code\n const errorCode = (error as unknown as { errorCode?: string })?.errorCode\n const wrapped = new AgyTokenRefreshError({\n message: raw.message,\n status: 0,\n statusText: 'Network Error',\n code: code ?? errorCode,\n description: raw.message,\n })\n // Preserve original cause/code for isProxyUnreachableError chain\n ;(wrapped as unknown as { cause?: unknown }).cause = error\n if (code) (wrapped as unknown as { code?: string }).code = code\n if (errorCode) (wrapped as unknown as { errorCode?: string }).errorCode = errorCode\n transientFailure = { type: 'failed', error: wrapped }\n }\n }\n\n // If any candidate encountered a transient / inconclusive error, do NOT revoke.\n if (transientFailure) return transientFailure\n // Only if ALL candidates were definitively rejected with invalid_grant, revoke.\n if (revokedCount === candidateIds.length) return { type: 'revoked' }\n\n return {\n type: 'failed',\n error: new AgyTokenRefreshError({\n message: 'Token refresh failed on all candidate client IDs',\n status: 400,\n statusText: 'Bad Request',\n }),\n }\n}\n","/**\n * Usage-aware account pool scheduling, aligned with oh-my-pi's\n * google-antigravity usage provider + ranking strategy (AuthStorage):\n *\n * - Backend quotas are counters scoped by model family (google/anthropic/openai);\n * the requested model maps to one family via its id prefix.\n * - `fetchAvailableModels` quotaInfo entries are aggregated per family\n * (most-pressured model wins the family's remaining fraction). That endpoint\n * reports the rolling 5-hour counter and nothing else.\n * - `retrieveUserQuotaSummary` supplies the WINDOW dimension the other one lacks:\n * it is the only source of the weekly reading, and its groups are mapped onto\n * the same family keys by their bucket ids.\n * - BOTH windows gate selection. An exhausted weekly budget is invisible in the\n * 5-hour counter, so an account can look healthy on every probe and still be\n * unable to serve a single request.\n * - Candidates rank by: unblocked first, hot windows last, measured usage before\n * unmeasured, required-drain descending (headroom / hours-to-reset — \"use it\n * or lose it\"), then used-fraction ascending. Exhausted families block the\n * account until their real reset time.\n */\n\nimport type { DiscoveredModels } from '../adapter/models.ts'\nimport { QUOTA_WINDOWS } from '../types.ts'\nimport type { CachedQuota, ManagedAccount, QuotaGroup, QuotaWindow } from '../types.ts'\nimport {\n SOFT_QUOTA_THRESHOLD,\n WEEKLY_QUOTA_THRESHOLD,\n computeSoftQuotaCacheTtlMs,\n} from './rotation.ts'\n\nexport type ModelFamily = 'google' | 'anthropic' | 'openai'\n\n/** Family bucket for model ids with no recognizable prefix (mirrors OMP's counter:unknown). */\nexport const FAMILY_UNKNOWN = 'unknown'\n\n/** Mirrors AuthStorage.PRIMARY_WINDOW_HOT_FRACTION: near-exhausted windows rank last. */\nexport const PRIMARY_WINDOW_HOT_FRACTION = 0.85\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n/** Floor for remaining-time in drain-urgency scores (mirrors AuthStorage; a stale reset must not explode the score). */\nconst DRAIN_FLOOR_MS = 60_000\n\n/** Map a model id to its backend quota counter family (OMP getAntigravityCounterKeyForModel). */\nexport function modelFamilyOf(modelId?: string): ModelFamily | undefined {\n if (!modelId) return undefined\n const id = modelId.toLowerCase()\n if (id.startsWith('claude-')) return 'anthropic'\n if (id.startsWith('gemini-') || id.startsWith('gemma-')) return 'google'\n if (id.startsWith('gpt-') || id.startsWith('openai/')) return 'openai'\n return undefined\n}\n\n/** Quota-cache key for a request: the model's family, or the unknown bucket. */\nexport function familyKeyOf(modelId?: string): string {\n return modelFamilyOf(modelId) ?? FAMILY_UNKNOWN\n}\n\nfunction earliestResetTime(a?: string, b?: string): string | undefined {\n if (!a) return b\n if (!b) return a\n const ta = Date.parse(a)\n const tb = Date.parse(b)\n if (Number.isNaN(ta)) return b\n if (Number.isNaN(tb)) return a\n return ta <= tb ? a : b\n}\n\n/**\n * The families a quota group's WINDOW BUCKET ID belongs to.\n *\n * `retrieveUserQuotaSummary` labels its buckets with upstream's own counters —\n * measured ids are `gemini-5h`, `gemini-weekly`, `3p-5h`, `3p-weekly` — which\n * is the same taxonomy `modelFamilyOf` mirrors. Reading the bucket id is\n * therefore a machine-readable lookup rather than a guess at a human label.\n *\n * `3p-` maps to TWO families on purpose, and that is not a shortcut: upstream\n * counts Claude and GPT under one third-party counter, so the weekly budget\n * belongs to both. No model-id prefix rule can recover that split, which is why\n * the mapping lives here rather than in `modelFamilyOf`.\n *\n * Window-aware as of #56: a bucket id resolves a family only when the window it\n * describes is one `CachedQuota` can hold AND the id agrees with the token (see\n * `agreedWindowKind`). The prefix and the window were two independent decisions\n * once, and an unrecognized window still claimed a family.\n */\nexport function familiesForBucketId(bucketId: string, window?: string): ModelFamily[] {\n const id = bucketId.toLowerCase()\n const families: ModelFamily[] = []\n if (id.startsWith('gemini-')) families.push('google')\n else if (id.startsWith('3p-') || id.startsWith('third_party-')) families.push('anthropic', 'openai')\n else if (id.startsWith('claude-')) families.push('anthropic')\n else if (id.startsWith('gpt-') || id.startsWith('openai-')) families.push('openai')\n if (families.length === 0) return []\n // Window-aware (see `agreedWindowKind`): a bucket whose window the cache cannot\n // hold — or whose id disagrees with the token — resolves NO family. The prefix\n // alone is not enough, because the field the reading lands in is chosen from\n // the token, and an unagreed pair files a weekly fraction as a 5-hour one.\n //\n // `window` is the parsed `QuotaWindow.window` when the caller has it; the\n // bucket id's own token is the fallback, so the one-argument form is safe too.\n const declared = window === undefined ? advertisedWindowOf(id) : window.toLowerCase()\n if (declared === undefined || agreedWindowKind(id, declared) === undefined) return []\n return families\n}\n\n/**\n * The families a group's LABEL covers, consulted only when no bucket id was\n * recognizable.\n *\n * Upstream's groups are its own taxonomy, not ours, and the measured labels are\n * `Gemini Models` and `Claude and GPT models` — the second spans two families,\n * so this is a 1-to-N mapping. It is a FALLBACK, not the primary rule: matching\n * words in a display string is the fragile half, and it survives only so that a\n * bucket id upstream renames still lands somewhere sensible.\n *\n * A label naming nothing we recognize maps to NO family deliberately.\n * Attributing an unknown group's budget to `google` would block healthy\n * accounts; leaving it unmapped only means that family stays unmeasured, which\n * is exactly the state before this function existed.\n */\nexport function familiesForGroupName(name: string): ModelFamily[] {\n const label = name.toLowerCase()\n const families: ModelFamily[] = []\n if (label.includes('gemini') || label.includes('google')) families.push('google')\n const claude = label.includes('claude')\n const gpt = label.includes('gpt')\n if (claude && gpt) families.push('anthropic', 'openai')\n else if (claude) families.push('anthropic')\n else if (gpt) families.push('openai')\n // There was a fourth branch here, keyed on a `3p`/`third` substring and guarded\n // by `families.length === 0`. That guard was already false whenever the label\n // named gemini/google (the first line pushes `google`), so the branch could only\n // ever fire for a label naming a third-party group WITHOUT naming its members —\n // exactly the case where it cannot be identified. Guessing a family blocks\n // healthy accounts, and `3p-` is resolved reliably from the BUCKET ID by\n // `familiesForBucketId`, so the branch is gone rather than re-keyed.\n return families\n}\n\n/**\n * Which of the two tracked windows an upstream token names, or `undefined` for a\n * token `CachedQuota` has no field for (`daily`, `monthly`).\n *\n * Read from `QUOTA_WINDOWS` (`types.ts`), the SAME table `adapter/quota-summary`\n * orders windows by. These were two independent tables once, and the gap between\n * them was a real defect: `daily` was orderable in the parser but unknown here, so\n * the group materialized an empty record that overwrote a live measurement. One\n * table makes that drift structurally impossible rather than merely tested for.\n *\n * Lookup is exact on the measured vocabulary (`5h`, `weekly`). A variant spelling\n * therefore classifies as unknown — which is safe, because the guard in\n * `ingestQuotaGroups` then skips the record rather than emptying it, and the\n * per-model probe still supplies the 5-hour reading.\n */\nfunction windowKind(window: string): 'rolling' | 'weekly' | undefined {\n return QUOTA_WINDOWS[window.toLowerCase()]?.kind\n}\n\n/** The window token a bucket id advertises, when it ends with one upstream names. */\nfunction advertisedWindowOf(bucketId: string): string | undefined {\n const id = bucketId.toLowerCase()\n const token = id.slice(id.lastIndexOf('-') + 1)\n return QUOTA_WINDOWS[token] ? token : undefined\n}\n\n/**\n * The field a window belongs in, decided from an AGREED (id, token) pair — or\n * `undefined` when the pair cannot be agreed.\n *\n * The family used to come from the bucket id's prefix while the field came from\n * the window token, as two independent decisions nothing checked against each\n * other. A bucket whose window the cache cannot hold still claimed the family,\n * and its reading was then filed by whichever field the TOKEN named. `gemini-weekly`\n * declaring `5h` is the shape that puts a multi-day weekly fraction into\n * `remainingFraction` (the 5-hour field), where `isFamilyDrained` and\n * `parseFutureResetMs` block the account for days on a window they misread.\n *\n * Refusing an unagreed pair is deliberately the strict option: the permissive\n * mapping is exactly the coupling that let two window vocabularies drift into\n * destroying a measurement. A bucket id that advertises no recognizable token is\n * held to the declared one alone, so a rename does not silently disable the cache.\n */\nfunction agreedWindowKind(bucketId: string, window: string): 'rolling' | 'weekly' | undefined {\n const declared = window.toLowerCase()\n const kind = windowKind(declared)\n if (kind === undefined) return undefined\n const advertised = advertisedWindowOf(bucketId)\n if (advertised !== undefined && advertised !== declared) return undefined\n return kind\n}\n\n/** The families one group belongs to: its bucket ids first, its label second. */\nexport function familiesForGroup(group: QuotaGroup): ModelFamily[] {\n const families: ModelFamily[] = []\n const add = (family: ModelFamily) => {\n if (!families.includes(family)) families.push(family)\n }\n for (const window of group.windows) {\n for (const family of familiesForBucketId(window.bucketId, window.window)) add(family)\n }\n if (families.length === 0) {\n for (const family of familiesForGroupName(group.name)) add(family)\n }\n return families\n}\n\n/**\n * Aggregate the grouped `retrieveUserQuotaSummary` windows into per-family\n * records — the ONLY source of the weekly window.\n *\n * `fetchAvailableModels` cannot supply it: its `quotaInfo` carries exactly\n * `remainingFraction` and `resetTime`, with no window field at all, which is why\n * a weekly limit used to be invisible to rotation.\n *\n * Two groups can land on one family (upstream may split `3p` later), so the\n * family record keeps the MOST pressured reading of each window across those\n * groups (`Math.min` below) rather than letting the last group win — the same\n * rule the per-model merge uses.\n *\n * WITHIN one group the rule is deliberately different: `??=` keeps upstream's\n * FIRST bucket of each window, because a group is upstream's own 5h + weekly\n * pair and a duplicate would otherwise depend on upstream's ordering. \"Most\n * pressured\" therefore applies to two GROUPS landing on one family, not to two\n * buckets inside one group.\n */\nexport function ingestQuotaGroups(groups: QuotaGroup[]): Record<string, CachedQuota> {\n const families = new Map<string, CachedQuota>()\n for (const group of groups) {\n const keys = familiesForGroup(group)\n if (keys.length === 0) continue\n let rolling: QuotaWindow | undefined\n let weekly: QuotaWindow | undefined\n for (const window of group.windows) {\n // The AGREED pair, not the token alone: a mismatched bucket must write no\n // field even when the group's label resolved a family (see agreedWindowKind).\n const kind = agreedWindowKind(window.bucketId, window.window)\n if (kind === 'rolling') rolling ??= window\n else if (kind === 'weekly') weekly ??= window\n }\n for (const key of keys) {\n const current = families.get(key)\n const record: CachedQuota = { ...current }\n if (typeof rolling?.remainingFraction === 'number') {\n record.remainingFraction = current?.remainingFraction === undefined\n ? rolling.remainingFraction\n : Math.min(current.remainingFraction, rolling.remainingFraction)\n const resetTime = earliestResetTime(current?.resetTime, rolling.resetTime ?? undefined)\n if (resetTime) record.resetTime = resetTime\n }\n if (typeof weekly?.remainingFraction === 'number') {\n record.weeklyFraction = current?.weeklyFraction === undefined\n ? weekly.remainingFraction\n : Math.min(current.weeklyFraction, weekly.remainingFraction)\n const weeklyResetTime = earliestResetTime(current?.weeklyResetTime, weekly.resetTime ?? undefined)\n if (weeklyResetTime) record.weeklyResetTime = weeklyResetTime\n }\n // A group whose only windows are a kind this cache cannot hold (`daily`,\n // `monthly`) would otherwise materialize an EMPTY record and replace a real\n // reading another group already wrote for this family.\n //\n // `daily`/`monthly` are RANKED by the shared `QUOTA_WINDOWS` table (so the\n // parser still display-sorts them) but carry no `kind`, which is what makes\n // them unholdable here. One table means the parser cannot know a window the\n // scheduler does not — the earlier two-table drift is structurally gone.\n if (record.remainingFraction === undefined && record.weeklyFraction === undefined && !current) {\n continue\n }\n families.set(key, record)\n }\n }\n return Object.fromEntries(families)\n}\n\n/**\n * Aggregate a `fetchAvailableModels` response, and when available the grouped\n * `retrieveUserQuotaSummary` windows, into per-family quota records.\n *\n * The two sources describe the same counters from different angles, so they are\n * merged rather than kept apart:\n * - per MODEL `quotaInfo.remainingFraction` is the rolling 5-hour counter, and\n * the family takes its most-pressured model (the bottleneck resets first);\n * - per GROUP summary windows add the weekly budget, which has no per-model\n * representation at all.\n *\n * `previous` is the account's existing cache, and it exists so a probe that\n * reports only one of the two windows cannot ERASE the other. The measured\n * reason: `fetchQuotaSummary` returns `[]` instead of throwing, so without the\n * carry-forward one timing-out endpoint would drop a known-drained weekly window\n * and put an exhausted account straight back into rotation.\n *\n * Carrying that value is safe next to a fresh 5-hour reading because it keeps its\n * own `weeklyResetTime`: once that moment passes, `isFamilyDrained` and\n * `parseFutureResetMs` both ignore the reading, so a stale weekly cannot outlive\n * the window it describes.\n */\nexport function ingestFamilyQuotas(\n discovered: DiscoveredModels,\n groups?: QuotaGroup[] | null,\n previous?: Record<string, CachedQuota> | null,\n): Record<string, CachedQuota> {\n const families = new Map<string, CachedQuota>()\n if (groups && groups.length > 0) {\n for (const [key, quota] of Object.entries(ingestQuotaGroups(groups))) {\n families.set(key, { ...quota })\n }\n }\n for (const [modelId, entry] of Object.entries(discovered.models ?? {})) {\n const remaining = entry.quotaInfo?.remainingFraction\n if (typeof remaining !== 'number' || !Number.isFinite(remaining)) continue\n const key = familyKeyOf(modelId)\n const current = families.get(key)\n const resetTime = earliestResetTime(current?.resetTime, entry.quotaInfo?.resetTime)\n families.set(key, {\n ...current,\n remainingFraction: current?.remainingFraction === undefined\n ? remaining\n : Math.min(current.remainingFraction, remaining),\n ...(resetTime ? { resetTime } : {}),\n modelCount: (current?.modelCount ?? 0) + 1,\n })\n }\n if (previous) {\n for (const [key, prev] of Object.entries(previous)) {\n const current = families.get(key)\n // Carry the weekly reading forward and NOTHING else — never the 5-hour\n // fields. It is the one field the per-model probe cannot supply, so its\n // absence means \"this probe did not ask\", not \"the weekly budget is empty\".\n //\n // `current` is absent when the family came from NEITHER source: either the\n // model probe does not cover it at all (a Gemini-only pool never reports\n // anthropic/openai) or its request failed, and `fetchQuotaSummary` answers a\n // failure with `[]` rather than throwing, so \"no groups\" and \"endpoint down\"\n // are the SAME input here. Dropping the record in that state erases a spent\n // week: `isFamilyDrained` flips true -> false and `rankPoolCandidates` loses\n // the weekly `blockedUntil`, putting an exhausted account straight back into\n // rotation. Hence the carry even with no `current`.\n //\n // A resurrected record cannot manufacture a block, because both consumers\n // ignore a window whose reset has already passed (`resetInPast` /\n // `parseFutureResetMs`) and the weekly value keeps its own reset moment.\n // The 5-hour fields get no such protection: `requiredDrainFor` has no reset\n // guard and clamps an elapsed reset to `DRAIN_FLOOR_MS`, so carrying a dead\n // 5-hour reading forward scores MAXIMUM drain urgency (24 vs 0.1 for an\n // identical live reading) and wins the ranking outright. That is why only\n // the weekly window travels.\n if (prev.weeklyFraction === undefined) continue\n // A fresh summary reading always wins; the carry only fills a gap.\n if (current?.weeklyFraction !== undefined) continue\n families.set(key, {\n ...current,\n weeklyFraction: prev.weeklyFraction,\n ...(current?.weeklyResetTime === undefined && prev.weeklyResetTime !== undefined\n ? { weeklyResetTime: prev.weeklyResetTime }\n : {}),\n })\n }\n }\n return Object.fromEntries(families)\n}\n\n/**\n * The lowest fraction a record reports across its LIVE windows — its overall\n * pressure.\n *\n * A window whose reset has already passed describes a window that no longer\n * exists, so it does not count — the same rule `isQuotaStale` applies when it\n * sizes the TTL from the selected record. Scoring a dead window would let one\n * family's expired weekly reading win the selection and represent the account,\n * masking another family's live pressure (issue #69: a dead weekly at 0.004\n * beat a live 0.2 and stretched the refresh TTL from 5 to 15 minutes).\n *\n * `undefined` means the record carries no usable LIVE reading at all, which is\n * deliberately different from a measured zero: only a real number can size a\n * refresh interval.\n */\nfunction pressureScore(entry: CachedQuota, now: number): number | undefined {\n const fractions: number[] = []\n if (typeof entry.remainingFraction === 'number' && Number.isFinite(entry.remainingFraction)\n && !resetInPast(entry.resetTime, now)) {\n fractions.push(entry.remainingFraction)\n }\n if (typeof entry.weeklyFraction === 'number' && Number.isFinite(entry.weeklyFraction)\n && !resetInPast(entry.weeklyResetTime, now)) {\n fractions.push(entry.weeklyFraction)\n }\n return fractions.length === 0 ? undefined : Math.min(...fractions)\n}\n\n/**\n * The quota record for one family, or the most-pressured family when the model\n * is unknown.\n *\n * \"Most pressured\" compares BOTH windows and reports the record whose LOWEST\n * window is the lowest of all of them. That matters because `isQuotaStale` sizes\n * the refresh TTL from whichever record this returns: a family at 0.9 / weekly\n * 0.004 is more pressured than one at 0.2, and comparing the 5-hour fraction\n * alone picked the 0.2 record — so the weekly-driven 60s TTL was never applied\n * to the family actually being requested and a spent week was discovered late.\n *\n * A record carrying ONLY a weekly reading participates for the same reason: #48\n * produces that shape legitimately (the anthropic/openai entries of a\n * Gemini-only pool), and the old `typeof entry.remainingFraction !== 'number'`\n * guard skipped it outright, leaving the week invisible to TTL sizing.\n *\n * Selection considers LIVE windows only (`pressureScore` drops a window whose\n * reset has passed) — but the with-family path returns the record as-is either\n * way, because its consumers guard each window individually.\n *\n * @param account - the account whose cache to read.\n * @param family - the exact family's record; omit to select the most pressured.\n * @param now - current time (Unix ms), which decides whether a window is live.\n */\nexport function familyQuotaFor(account: ManagedAccount, family?: ModelFamily, now = Date.now()): CachedQuota | undefined {\n const cache = account.cachedQuota ?? {}\n if (family) return cache[family]\n let worst: CachedQuota | undefined\n let worstScore: number | undefined\n for (const entry of Object.values(cache)) {\n const score = pressureScore(entry, now)\n if (score === undefined) continue\n if (worstScore === undefined || score < worstScore) {\n worst = entry\n worstScore = score\n }\n }\n return worst\n}\n\n/** Whether the account's quota cache needs a refresh (missing, or past its health-based TTL). */\nexport function isQuotaStale(account: ManagedAccount, now = Date.now()): boolean {\n if (!account.cachedQuota || !account.cachedQuotaUpdatedAt) return true\n const mostPressured = familyQuotaFor(account, undefined, now)\n // A window whose reset has already passed describes a window that no longer\n // exists, so it must not drive the refresh INTERVAL either. A carried weekly\n // value kept returning the 60s TTL long after its reset, which re-probed both\n // endpoints every minute for as long as the summary endpoint stayed down — the\n // same reason `isFamilyDrained` ignores an expired window when it decides the\n // account is usable. Both fractions are guarded, since the 5-hour reading can\n // be carried forward by the same rule.\n const rolling = resetInPast(mostPressured?.resetTime, now) ? undefined : mostPressured?.remainingFraction\n const weekly = resetInPast(mostPressured?.weeklyResetTime, now) ? undefined : mostPressured?.weeklyFraction\n const ttl = computeSoftQuotaCacheTtlMs(rolling, weekly)\n return now - account.cachedQuotaUpdatedAt > ttl\n}\n\n/** How long a measured 5h/weekly window snapshot stays fresh. */\nexport const LIMITS_CACHE_TTL_MS = 10 * 60 * 1000\n\n/**\n * Whether the display-only windows need a refresh.\n *\n * A SEPARATE rule from `isQuotaStale` on purpose: that one keys off\n * `cachedQuota`/`cachedQuotaUpdatedAt`, which the scheduling path fills and a\n * SOLO account never does (the pool gate skips it). Reusing it here would report\n * \"stale\" on every single call for a solo account and re-probe the endpoint\n * continuously — the exact case this feature exists to serve.\n *\n * A fixed TTL is also the honest choice: the windows come from their own\n * endpoint, so there is no `remainingFraction` on hand to scale the interval by\n * without reading the very data being validated.\n *\n * @param account - the account to test.\n * @param now - current time (Unix ms).\n */\nexport function isLimitsStale(account: ManagedAccount, now = Date.now()): boolean {\n const updatedAt = account.cachedLimits?.updatedAt\n if (typeof updatedAt !== 'number' || !Number.isFinite(updatedAt)) return true\n return now - updatedAt > LIMITS_CACHE_TTL_MS\n}\n\n/** Whether a reset moment has already passed. An absent or malformed one has not. */\nfunction resetInPast(resetTime: string | undefined, now: number): boolean {\n if (!resetTime) return false\n const reset = Date.parse(resetTime)\n return !Number.isNaN(reset) && reset <= now\n}\n\n/**\n * Whether the requested family on this account is soft-quota-exhausted.\n *\n * BOTH windows are checked, because they refill on different clocks. The 5-hour\n * bucket can be nearly empty while the weekly budget is untouched, and the\n * reverse — a spent weekly budget stays spent across four 5-hour refills. Before\n * this, only the 5-hour reading was consulted, so an account whose week was over\n * kept being selected until a real request failed.\n *\n * A window whose reset has already passed is IGNORED rather than read: its\n * fraction describes a window that no longer exists, so the account stays\n * selectable until the next measurement replaces the stale value.\n */\nexport function isFamilyDrained(account: ManagedAccount, family?: ModelFamily, now = Date.now()): boolean {\n const quota = familyQuotaFor(account, family, now)\n if (!quota) return false\n if (\n typeof quota.remainingFraction === 'number'\n && !resetInPast(quota.resetTime, now)\n && quota.remainingFraction < SOFT_QUOTA_THRESHOLD\n ) {\n return true\n }\n if (\n typeof quota.weeklyFraction === 'number'\n && !resetInPast(quota.weeklyResetTime, now)\n && quota.weeklyFraction <= WEEKLY_QUOTA_THRESHOLD\n ) {\n return true\n }\n return false\n}\n\n/**\n * Required drain rate: headroomFraction / remainingHours — how fast the\n * family's remaining quota must be consumed to avoid expiring unused at its\n * reset (mirrors AuthStorage.#computeWindowRequiredDrain with a daily window).\n *\n * DELIBERATELY FIVE-HOUR ONLY, and this is a decision rather than an omission.\n * The weekly window influences `blockedUntil` and `hot` instead of the ranking\n * ORDER, because a week is not a spend-by deadline: the `DAY_MS` clamp below\n * caps the horizon at 24h, so a weekly reset ~5 days out would report ~24h of\n * urgency for a budget that cannot be spent faster to any benefit — and every\n * family would then be ranked by a number that no longer distinguishes them.\n * Making this weekly-aware therefore means replacing that clamp with a horizon\n * the weekly window can actually express, not adding a second fraction to the\n * numerator.\n */\nexport function requiredDrainFor(quota: CachedQuota | undefined, now = Date.now()): number {\n const remaining = quota?.remainingFraction\n if (typeof remaining !== 'number' || !Number.isFinite(remaining)) return 0\n // Headroom IS the remaining fraction (mirrors AuthStorage: headroom = 1 - used).\n const headroom = Math.min(Math.max(remaining, 0), 1)\n if (headroom <= 0) return 0\n let remainingMs = DAY_MS\n const resetTime = quota?.resetTime\n if (resetTime) {\n const resetAt = Date.parse(resetTime)\n if (!Number.isNaN(resetAt)) remainingMs = Math.min(remainingMs, Math.max(resetAt - now, 0))\n }\n const remainingHours = Math.max(remainingMs, DRAIN_FLOOR_MS) / (60 * 60 * 1000)\n return headroom / remainingHours\n}\n\nexport interface PoolCandidate {\n account: ManagedAccount\n index: number\n /** Cooldown/limit wall blocking this account (null when usable). */\n blockedUntil: number | null\n /** Used fraction of the requested family's quota (measured accounts only). */\n usedFraction?: number\n requiredDrain: number\n hot: boolean\n measured: boolean\n}\n\n/** Candidate with its rotation-order position, used only while sorting. */\ninterface PoolCandidateWithOrder extends PoolCandidate {\n orderPos: number\n}\n\nfunction parseFutureResetMs(resetTime: string | undefined, now: number): number | undefined {\n if (!resetTime) return undefined\n const reset = Date.parse(resetTime)\n if (Number.isNaN(reset) || reset <= now) return undefined\n return reset\n}\n\n/**\n * Whether the requested family on this account is effectively quota-exhausted\n * for account STEERING (affinity pins, the active-account preference): an\n * explicit quota-exhausted cooldown in force, a spent 5-hour window\n * (`remainingFraction <= 0` with a future reset), or a weekly window at or\n * below `WEEKLY_QUOTA_THRESHOLD` — a spent week refills in days, so its rule\n * mirrors `isFamilyDrained`'s weekly branch rather than the block wall.\n *\n * Deliberately a DIFFERENT predicate from the one in `pickAccount`'s `!picked`\n * branch, which applies a strict zero to BOTH windows to mirror\n * `rankPoolCandidates`' block wall when classifying `AgyPoolBlockedError`; and\n * deliberately NARROWER than `isFamilyDrained`'s 5-hour rule\n * (`SOFT_QUOTA_THRESHOLD`): account prioritization holds a chosen account\n * until it is actually spent, not until ranking would prefer another one.\n */\nexport function isFamilyQuotaExhausted(account: ManagedAccount, family?: ModelFamily, now = Date.now()): boolean {\n if (account.cooldownReason === 'quota-exhausted' && (account.coolingDownUntil ?? 0) > now) {\n return true\n }\n const quota = familyQuotaFor(account, family)\n if (!quota) return false\n const spent = (fraction: number | undefined, resetTime: string | undefined, threshold = 0): boolean => {\n if (typeof fraction !== 'number' || fraction > threshold || !resetTime) return false\n const resetMs = parseFutureResetMs(resetTime, now)\n return resetMs !== undefined && resetMs > now\n }\n return spent(quota.remainingFraction, quota.resetTime, 0)\n || spent(quota.weeklyFraction, quota.weeklyResetTime, WEEKLY_QUOTA_THRESHOLD)\n}\n\n/**\n * Rank pool candidates for one request, mirroring AuthStorage's antigravity\n * ordering: unblocked first (earliest unblock time among blocked), hot windows\n * last, measured usage before unmeasured, required-drain descending, then\n * used-fraction ascending. Ties preserve the rotation order seeded from\n * `startIndex` so an unmeasured pool keeps the active-account bias.\n */\nexport function rankPoolCandidates(\n entries: ReadonlyArray<{ account: ManagedAccount; index: number }>,\n modelId: string | undefined,\n now = Date.now(),\n startIndex = 0,\n): PoolCandidate[] {\n const family = modelFamilyOf(modelId)\n const activePos = entries.findIndex((e) => e.index === startIndex)\n const clampedStart = activePos >= 0 ? activePos : 0\n const ordered = entries.length === 0 ? [] : [...entries.slice(clampedStart), ...entries.slice(0, clampedStart)]\n const candidates: PoolCandidateWithOrder[] = ordered.map(({ account, index }, orderPos) => {\n const quota = familyQuotaFor(account, family, now)\n // A window whose reset has already passed describes a window that no longer\n // exists — the same rule the block decision below applies through\n // `parseFutureResetMs`. Without this guard a carried-forward weekly reading\n // (kept until the next successful probe) made the candidate `hot` for\n // `PRIMARY_WINDOW_HOT_FRACTION` on a window that had already refilled.\n const live5h = quota !== undefined && !resetInPast(quota.resetTime, now)\n const liveWeekly = quota !== undefined && !resetInPast(quota.weeklyResetTime, now)\n const remaining = live5h ? quota.remainingFraction : undefined\n const used = typeof remaining === 'number' ? Math.min(Math.max(1 - remaining, 0), 1) : undefined\n const weeklyRemaining = liveWeekly ? quota.weeklyFraction : undefined\n const weeklyUsed = typeof weeklyRemaining === 'number'\n ? Math.min(Math.max(1 - weeklyRemaining, 0), 1)\n : undefined\n\n let blockedUntil: number | null = null\n if (account.coolingDownUntil && account.coolingDownUntil > now) {\n blockedUntil = account.coolingDownUntil\n }\n const familyLimit = account.rateLimitResetTimes?.[familyKeyOf(modelId)]\n if (familyLimit !== undefined && familyLimit > now) {\n blockedUntil = blockedUntil === null ? familyLimit : Math.max(blockedUntil, familyLimit)\n }\n // A measured zero-remaining family with a future reset blocks the account\n // until the real reset (mirrors AuthStorage usage-limit blocking); drained\n // (low but non-zero) families are ranked, not blocked.\n //\n // Both windows are consulted and the LATER reset wins: an account with a\n // spent 5-hour bucket and a spent week must wait for the week, not for the\n // bucket that refills in an hour.\n if (blockedUntil === null && quota) {\n const resetMs = typeof remaining === 'number' && remaining <= 0\n ? parseFutureResetMs(quota.resetTime, now)\n : undefined\n if (resetMs !== undefined) blockedUntil = resetMs\n const weeklyResetMs = typeof weeklyRemaining === 'number' && weeklyRemaining <= 0\n ? parseFutureResetMs(quota.weeklyResetTime, now)\n : undefined\n if (weeklyResetMs !== undefined) {\n blockedUntil = blockedUntil === null ? weeklyResetMs : Math.max(blockedUntil, weeklyResetMs)\n }\n }\n\n return {\n account,\n index,\n orderPos,\n blockedUntil,\n usedFraction: used,\n requiredDrain: requiredDrainFor(quota, now),\n hot: (used !== undefined && used >= PRIMARY_WINDOW_HOT_FRACTION)\n || (weeklyUsed !== undefined && weeklyUsed >= PRIMARY_WINDOW_HOT_FRACTION),\n measured: used !== undefined || weeklyUsed !== undefined,\n }\n })\n\n candidates.sort((left, right) => {\n const leftBlocked = left.blockedUntil !== null\n const rightBlocked = right.blockedUntil !== null\n if (leftBlocked !== rightBlocked) return leftBlocked ? 1 : -1\n if (leftBlocked && rightBlocked) return (left.blockedUntil ?? 0) - (right.blockedUntil ?? 0)\n if (left.hot !== right.hot) return left.hot ? 1 : -1\n if (left.measured !== right.measured) return left.measured ? -1 : 1\n const drain = right.requiredDrain - left.requiredDrain\n if (drain !== 0) return drain\n const usedDiff = (left.usedFraction ?? 0.5) - (right.usedFraction ?? 0.5)\n if (usedDiff !== 0) return usedDiff\n return left.orderPos - right.orderPos\n })\n\n return candidates.map(({ account, index, blockedUntil, usedFraction, requiredDrain, hot, measured }) => ({\n account, index, blockedUntil, usedFraction, requiredDrain, hot, measured,\n }))\n}\n","","/**\n * Device fingerprint generation for rate-limit mitigation (two layers):\n *\n * 1. Per-request randomized headers — platform/arch/SDK-client pools.\n * 2. Per-account persistent fingerprint — deviceId/sessionToken/UA snapshot\n * with bounded history (≤5 versions, restorable), regenerated when an\n * account's capacity looks exhausted.\n *\n * All tunable pools live in fingerprint-data.json so they can be updated\n * without a code release (the reference implementation stopped at an old\n * version string once archived — that staleness is the detectable signal).\n */\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { randomBytes, randomUUID } from 'node:crypto'\nimport { AGY_IDE_TYPE, AGY_PLATFORM_ENUM, currentAgyVersion } from '../oauth/constants.ts'\nimport { migrateToAgyDir } from '../store/paths.ts'\nimport type { ClientMetadata, Fingerprint, FingerprintVersion } from '../types.ts'\nimport fingerprintData from './fingerprint-data.json'\n\nexport interface FingerprintData {\n versionPool: string[]\n platforms: string[]\n sdkClients: string[]\n ideTypes: string[]\n}\n\nexport const DEFAULT_FINGERPRINT_DATA = fingerprintData as FingerprintData\n\nexport const MAX_FINGERPRINT_HISTORY = 5\n\n/** The override file's NAME (inside the agy data dir), for messages and tests. */\nexport const FINGERPRINT_OVERRIDE_FILE = 'agy-fingerprint-data.json'\n\n/**\n * Effective fingerprint data: a user override at `$DSH_HOME/agy/agy-fingerprint-data.json`\n * wins when present and parseable (hot-updatable without a code release — the\n * bundled copy is compiled in), otherwise the bundled defaults. The legacy\n * `$DSH_HOME/agy-fingerprint-data.json` location is migrated once by rename.\n */\nlet cachedData: FingerprintData | null = null\nexport function getFingerprintData(): FingerprintData {\n if (cachedData) return cachedData\n try {\n // NOT `resolveDshHome()`-based: see the note on `getFingerprintData` above.\n const dshHome = process.env.DSH_HOME ? process.env.DSH_HOME : join(process.env.HOME ?? '.', '.dsh')\n const overrideFile = migrateToAgyDir('agy-fingerprint-data.json', dshHome).file\n if (existsSync(overrideFile)) {\n const parsed = JSON.parse(readFileSync(overrideFile, 'utf8')) as FingerprintData\n if (parsed && Array.isArray(parsed.versionPool) && parsed.versionPool.length > 0) {\n cachedData = parsed\n return cachedData\n }\n }\n } catch {\n // unreadable override — fall through to bundled defaults\n }\n cachedData = DEFAULT_FINGERPRINT_DATA\n return cachedData\n}\n\nfunction randomFrom<T>(arr: readonly T[]): T {\n return arr[Math.floor(Math.random() * arr.length)]!\n}\n\n/**\n * Replace the pool data for one test, and restore the real source with `undefined`.\n *\n * `getFingerprintData()` caches its result in-process, so a test cannot exercise a\n * user override file (`$DSH_HOME/agy/agy-fingerprint-data.json`) without a way to drop\n * that cache.\n */\nexport function _setFingerprintDataForTest(data: FingerprintData | undefined): void {\n cachedData = data ?? null\n}\n\n/** Generate a randomized device fingerprint representing one apparent device. */\nexport function generateFingerprint(\n data: FingerprintData = getFingerprintData(),\n version = randomFrom(data.versionPool),\n): Fingerprint {\n const platform = randomFrom(data.platforms)\n return {\n deviceId: randomUUID(),\n sessionToken: randomBytes(16).toString('hex'),\n userAgent: `antigravity/${version} ${platform}`,\n apiClient: randomFrom(data.sdkClients),\n // The metadata message, not a header (see `ClientMetadata` in types.ts). The\n // version is stamped here as well as in the UA because the official message\n // carries `ide_version` — a UA the SDK overwrites would otherwise be the only\n // place this client states its version.\n clientMetadata: {\n ideType: randomFrom(data.ideTypes),\n ideVersion: version,\n platform: AGY_PLATFORM_ENUM,\n },\n createdAt: Date.now(),\n }\n}\n\n/**\n * Per-request randomized headers (platform + SDK client pools).\n *\n * No longer used on any production path: the account's identity is created once\n * on first use and reused ({@link generateFingerprint}), so nothing re-rolls a\n * platform per request. Kept as the pool-level primitive that\n * {@link getStableHeaders} and the fingerprint tests are built on.\n */\n/** The impersonation surface a fingerprint produces: two headers and the body message. */\nexport interface ImpersonationHeaders {\n 'User-Agent': string\n 'X-Goog-Api-Client': string\n clientMetadata: ClientMetadata\n}\n\nexport function getRandomizedHeaders(\n data: FingerprintData = getFingerprintData(),\n version = randomFrom(data.versionPool),\n): ImpersonationHeaders {\n const platform = randomFrom(data.platforms)\n return {\n 'User-Agent': `antigravity/${version} ${platform}`,\n 'X-Goog-Api-Client': randomFrom(data.sdkClients),\n clientMetadata: {\n ideType: randomFrom(data.ideTypes),\n ideVersion: version,\n platform: AGY_PLATFORM_ENUM,\n },\n }\n}\n\n/**\n * Deterministic fallback headers for the `stable` fingerprint mode: the first\n * entry of each pool, every call — one fixed client identity instead of\n * per-request randomization (OMP-style fixed-client posture).\n */\nexport function getStableHeaders(\n data: FingerprintData = getFingerprintData(),\n version = data.versionPool[0] ?? '',\n): ImpersonationHeaders {\n // `darwin/arm64` is the UA's platform TOKEN, a different vocabulary from the\n // `DARWIN_ARM64` enum the metadata message takes. The UA token has no official\n // confirmation (see docs/official-identity.json); the enum does.\n const platform = data.platforms[0] ?? 'darwin/arm64'\n // `currentAgyVersion()`, never a literal: the resolved live version or the\n // pinned fallback. A frozen string here would outlive the release it names\n // (that staleness is the detectable signal this module exists to avoid).\n const resolved = version || currentAgyVersion()\n return {\n 'User-Agent': `antigravity/${resolved} ${platform}`,\n 'X-Goog-Api-Client': data.sdkClients[0] ?? '',\n clientMetadata: {\n ideType: data.ideTypes[0] ?? AGY_IDE_TYPE,\n ideVersion: resolved,\n platform: AGY_PLATFORM_ENUM,\n },\n }\n}\n\n/**\n * Rewrite the version inside a fingerprint UA; reports whether it changed.\n *\n * Carries the metadata message's `ideVersion` along: they state the same fact,\n * and letting them drift is how one account comes to look like two clients.\n */\nexport function updateFingerprintVersion(fingerprint: Fingerprint, version: string): boolean {\n const pattern = /^(antigravity\\/)([\\d.]+)/\n const match = fingerprint.userAgent.match(pattern)\n const uaChanged = match !== null && match[2] !== version\n if (uaChanged) fingerprint.userAgent = fingerprint.userAgent.replace(pattern, `$1${version}`)\n const metaChanged = fingerprint.clientMetadata.ideVersion !== version\n if (metaChanged) fingerprint.clientMetadata.ideVersion = version\n return uaChanged || metaChanged\n}\n\n/** Append a fingerprint to the account history (bounded), then use it as current. */\nexport function recordFingerprintVersion(\n history: FingerprintVersion[] | undefined,\n fingerprint: Fingerprint,\n reason: FingerprintVersion['reason'],\n): FingerprintVersion[] {\n const next = [...(history ?? []), { fingerprint, timestamp: Date.now(), reason }]\n return next.slice(-MAX_FINGERPRINT_HISTORY)\n}\n","/**\n * Risk-control switches (compliance posture): env-gated behaviors that keep\n * the grey-zone product honest —\n *\n * - DSH_AGY_DISABLE=1: global kill switch, the plugin registers nothing.\n * - DSH_AGY_FINGERPRINT_MODE=stable: one identity per account, never\n * regenerated. `dynamic` (the default) regenerates the identity after\n * repeated rate-limits — that is now the flag's ONLY effect. It no longer\n * selects per-request header randomization: an account with no fingerprint yet\n * presents one fixed identity either way, because re-rolling a platform per\n * request is the anomaly that posture was meant to remove.\n *\n * The BYO OAuth app escape hatch (AGY_CLIENT_ID / AGY_CLIENT_SECRET) lives in\n * oauth/constants.ts (resolveAgyClientCredentials) — oauth/ is a dependency\n * leaf and must not import runtime/.\n */\n\nexport type FingerprintMode = 'dynamic' | 'stable'\n\nconst TRUE_VALUES = new Set(['1', 'true', 'yes', 'on'])\n\nfunction envFlag(name: string): boolean {\n const value = process.env[name]\n return value !== undefined && TRUE_VALUES.has(value.trim().toLowerCase())\n}\n\n/** Global kill switch: DSH_AGY_DISABLE=1 keeps the plugin from registering anything. */\nexport function isAgyDisabled(): boolean {\n return envFlag('DSH_AGY_DISABLE')\n}\n\n/**\n * Fingerprint strategy: `dynamic` (default, identity regenerated after repeated\n * rate-limits) or `stable` (one identity per account, never regenerated —\n * mirrors OMP's fixed-client posture). Only the regeneration step differs; both\n * modes present a single fixed identity when no fingerprint exists yet.\n */\nexport function fingerprintMode(): FingerprintMode {\n return process.env.DSH_AGY_FINGERPRINT_MODE === 'stable' ? 'stable' : 'dynamic'\n}\n","/**\n * Account session manager: the shared runtime glue between the store, the\n * OAuth refresh path, rotation/fingerprint logic, and the adapter. Used by the\n * in-harness plugin shell, the CLI, and the web routes.\n */\n\nimport { AgyAuthError, AgyPoolBlockedError } from './types.ts'\nimport type { AccountStorageV4, AgyAccountSession, CachedQuota, FailureKind, ManagedAccount, OAuthAuthDetails, QuotaGroup } from './types.ts'\nimport { refreshAccessToken } from './oauth/refresh.ts'\nimport { accessTokenExpired, formatRefreshParts, parseRefreshParts } from './oauth/auth.ts'\nimport type { AccountStore } from './store/accounts.ts'\nimport {\n IN_FLIGHT_STALE_MS,\n MAX_IN_FLIGHT_PER_ACCOUNT,\n MAX_RATE_LIMIT_COOLDOWN_MS,\n RATE_LIMIT_COOLDOWN_MS,\n clearExpiredState,\n decideRotation,\n isCoolingDown,\n isFamilyRateLimited,\n parseFutureResetMs,\n pickNextAccountIndex,\n recordRateLimit,\n} from './runtime/rotation.ts'\nimport {\n familyKeyOf,\n familyQuotaFor,\n ingestFamilyQuotas,\n isFamilyDrained,\n isFamilyQuotaExhausted,\n isLimitsStale,\n isQuotaStale,\n modelFamilyOf,\n rankPoolCandidates,\n} from './runtime/quota.ts'\nimport {\n generateFingerprint,\n getFingerprintData,\n getStableHeaders,\n recordFingerprintVersion,\n updateFingerprintVersion,\n} from './runtime/fingerprint.ts'\nimport { deriveAntigravitySessionId, generateAntigravityRequestId } from './runtime/identity.ts'\nimport { fingerprintMode } from './runtime/risk.ts'\nimport { peekCachedAntigravityVersion, resolveAntigravityVersionBounded } from './runtime/version.ts'\nimport { currentAgyVersion } from './oauth/constants.ts'\nimport { accountFetch, isProxyUnreachableError, probeFetch, proxiedFetch } from './proxy.ts'\nimport { describeFetchError } from './runtime/classify.ts'\nimport type { Fingerprint } from './types.ts'\n\nexport interface SessionManagerOptions {\n store: AccountStore\n /** Called after rotation changes the active index (for logging/UI). */\n onRotate?: (fromIndex: number, toIndex: number, reason: FailureKind) => void\n /** Called after a health check finishes (batch probe results). */\n onHealthReport?: (results: AccountHealthResult[]) => void\n /**\n * Receives one usage record per account-scoped upstream call made outside the\n * chat path (verification, test calls, CLI invocations).\n *\n * This is the half of the ledger DSH structurally cannot supply: those calls\n * consume upstream quota without ever producing a session event, so agy is\n * the only party that can count them. Chat generations are recorded by the\n * adapter, which sees their token usage.\n */\n recordUsage?: (record: UsageRecord) => void\n}\n\n/** A usage record emitted by the session manager's non-chat call paths. */\nexport interface UsageRecord {\n account?: string\n model?: string\n source: 'chat' | 'cli' | 'verify' | 'test'\n ok: boolean\n rateLimited?: boolean\n /** Token usage, when the call parsed an upstream stream. */\n usage?: { input: number; output: number; cacheRead: number; cacheWrite: number }\n latencyMs?: number\n ttftMs?: number\n /**\n * A pool-level event (a rotation) rather than a request of its own.\n * The adapter has already recorded the request that failed, so this must\n * move only the pool counters or the request would be counted twice.\n */\n poolEvent?: boolean\n /** Marks a rotation in a pool event. */\n rotated?: boolean\n /**\n * The failure classification ('rate-limit', 'network-error', ...), when the\n * record failed. Diagnostics only — the ledger counters ignore it.\n */\n reason?: string\n}\n\n/** One account's health check result (refresh + userinfo). */\nexport interface AccountHealthResult {\n index: number\n email?: string\n ok: boolean\n error?: string\n}\n\n/**\n * What one 5h/weekly window refresh actually did.\n *\n * Exists so an EXPLICIT refresh can report its outcome. The three lists are\n * separate because they read identically on screen otherwise: \"nothing was due\n * (TTL)\" and \"every probe failed\" both leave the displayed numbers untouched,\n * so a caller that cannot tell them apart cannot say anything truthful.\n */\nexport interface LimitsRefreshResult {\n /** Account keys whose windows were measured and written. */\n measured: string[]\n /** Account keys whose probe was attempted and failed. */\n failed: string[]\n /** Accounts not probed because their snapshot was still fresh. */\n skipped: number\n}\n\n/**\n * Session affinity window: reuse the last-used account for new requests within\n * this window (proxy for one DSH conversation, which exposes no id). After the\n * window or on failure the pool re-balances.\n */\nexport const SESSION_AFFINITY_WINDOW_MS = 10 * 60 * 1000\n\n/**\n * One quota-fraction snapshot, for the burn rate. Memory only.\n *\n * Upstream reports windows as a remaining FRACTION with no capacity, so the\n * only honest way to a \"how long until this runs dry\" figure is the fraction's\n * own rate of change: keep the previous snapshot, and the next one yields\n * fraction-per-hour.\n */\nexport interface LimitSample {\n at: number\n /** bucketId -> remaining fraction; null fractions are omitted. */\n fractions: Record<string, number>\n}\n\n/** Two samples closer than this are noise (a forced refresh seconds apart). */\nexport const MIN_BURN_SAMPLE_MS = 5 * 60 * 1000\n\n/**\n * Fraction-per-hour per window bucket, from two snapshots.\n *\n * Pure and exported for a direct unit test. Rules:\n * - the FIRST sample yields nothing (no baseline yet), and so does a pair\n * closer than `MIN_BURN_SAMPLE_MS` — two probes seconds apart measure the\n * probe jitter, not the burn;\n * - a RISING fraction is a reset (or refresh noise), never negative burn;\n * - a bucket with no baseline simply has no rate yet.\n *\n * Returns null when no bucket produced a rate, so the caller can keep the\n * previous rates instead of overwriting them with an empty read.\n */\nexport function sampleLimitBurn(prev: LimitSample | undefined, next: LimitSample): Record<string, number> | null {\n if (prev === undefined) return null\n const elapsedH = (next.at - prev.at) / 3_600_000\n if (elapsedH < MIN_BURN_SAMPLE_MS / 3_600_000) return null\n const perHour: Record<string, number> = {}\n for (const [bucketId, fraction] of Object.entries(next.fractions)) {\n const before = prev.fractions[bucketId]\n if (before !== undefined && before > fraction) {\n perHour[bucketId] = (before - fraction) / elapsedH\n }\n }\n return Object.keys(perHour).length > 0 ? perHour : null\n}\n\ninterface TokenCacheEntry {\n access: string\n expires: number\n}\n\ninterface QuotaRefreshResult {\n key: string\n quotas: Record<string, CachedQuota>\n updatedAt: number\n}\n\n/**\n * Resolve the impersonation headers for one request from the account's\n * persistent fingerprint (stable identity).\n *\n * The fallback below is only reachable when an account has no stored identity —\n * normal operation creates one on first use (see `getSession`). It is therefore\n * deliberately DETERMINISTIC rather than randomized: a per-request platform or\n * version would make one account appear to be several different machines, which\n * is the anomaly this identity exists to avoid. It reads\n * {@link getFingerprintData} so a `$DSH_HOME/agy/agy-fingerprint-data.json` override\n * still applies on this path.\n */\nexport function impersonationHeadersFor(account: ManagedAccount): AgyAccountSession['impersonation'] {\n const fingerprint = account.fingerprint\n if (fingerprint) {\n return {\n 'User-Agent': fingerprint.userAgent,\n 'X-Goog-Api-Client': fingerprint.apiClient,\n clientMetadata: fingerprint.clientMetadata,\n }\n }\n return getStableHeaders(getFingerprintData(), currentAgyVersion())\n}\n\nexport class AgySessionManager {\n private readonly store: AccountStore\n private readonly onRotate: SessionManagerOptions['onRotate']\n private readonly onHealthReport: SessionManagerOptions['onHealthReport']\n private readonly recordUsageOption: SessionManagerOptions['recordUsage']\n private readonly tokenCache = new Map<string, TokenCacheEntry>()\n /** In-flight refresh promises keyed by account: concurrent requests share one refresh. */\n private readonly refreshInFlight = new Map<string, Promise<OAuthAuthDetails | undefined>>()\n /** In-flight quota fetches keyed by account: concurrent selections share one fetchAvailableModels call. */\n private readonly quotaRefreshInFlight = new Map<string, Promise<QuotaRefreshResult | null>>()\n /**\n * In-flight `retrieveUserQuotaSummary` probes keyed by account.\n *\n * ONE promise per account per cycle, shared by BOTH consumers:\n * `refreshLimits` (display, writes `cachedLimits`) and `refreshQuotaCache`\n * (scheduling, writes `cachedQuota`). Since #48 both read the same endpoint for\n * the same account, so a stale account paid two round trips where one would\n * do; `quotaRefreshInFlight` de-duplicated only WITHIN the scheduling path.\n * Sharing makes the two caches agree on what upstream actually said.\n *\n * It carries the FETCH and nothing else: the invariant that `refreshLimits`\n * must never write `cachedQuota` (which `rankPoolCandidates` turns into a\n * `blockedUntil`) is about WHERE a result lands, and each path still decides\n * that for itself.\n */\n private readonly quotaSummaryInFlight = new Map<string, Promise<QuotaGroup[]>>()\n private readonly failureCounts = new Map<string, number>()\n /** Accounts whose request-time project discovery already failed (no retry per request). */\n private readonly projectRetryFailed = new Set<string>()\n\n /** Bound for one quota poll so selection never stalls on a hung endpoint. */\n private static readonly QUOTA_FETCH_TIMEOUT_MS = 3_000\n /** Refresh the token this far ahead of expiry so a request never blocks on the token endpoint. */\n private static readonly REFRESH_SKEW_MS = 2 * 60 * 1000\n\n /**\n * Per-conversation account affinity: the account one conversation is pinned\n * to, so its turns stay together (upstream prefix cache + `sessionId`\n * continuity) instead of re-ranking per request.\n *\n * Keyed by the conversation — `GenerateOptions.sessionId`, which the DSH agent\n * loop stamps — rather than by a single \"last used\" slot. That slot was a\n * stand-in for a conversation id the code assumed DSH did not expose, and with\n * two concurrent conversations the second overwrote the first's pin, so both\n * drifted across the pool independently of which account they started on.\n * Entries expire after {@link SESSION_AFFINITY_WINDOW_MS}.\n */\n private readonly affinity = new Map<string, { key: string; at: number }>()\n\n /** Bound on tracked conversations; least-recently-pinned entries are evicted. */\n private static readonly MAX_AFFINITY_ENTRIES = 64\n\n /** Bucket for callers with no session identity (standalone CLI, one-shots). */\n private static readonly ANONYMOUS_CONVERSATION = '\\u0000anonymous'\n\n /**\n * In-flight upstream requests per account, so selection can spread concurrent\n * fan-out across the pool instead of stacking every stream on one account.\n *\n * Deliberately a PREFERENCE, not a hard gate: when every eligible account is at\n * the cap, selection proceeds with the best-ranked one rather than waiting.\n * Blocking would need a reliable release on every path (including an abandoned\n * stream) and a bounded wait to avoid deadlock, and a leaked counter would then\n * stall real requests — a worse failure than the burst it prevents. The\n * documented limitation is therefore: this spreads load across a multi-account\n * pool, and cannot throttle a single-account one.\n */\n private readonly inFlight = new Map<string, { count: number; at: number }>()\n\n /** In-flight count for one account, discarding a leaked entry past its TTL. */\n private inFlightCount(accountKey: string, now: number): number {\n const entry = this.inFlight.get(accountKey)\n if (entry === undefined) return 0\n if (now - entry.at > IN_FLIGHT_STALE_MS) {\n this.inFlight.delete(accountKey)\n return 0\n }\n return entry.count\n }\n\n /** Record that one upstream request for this account has started. */\n noteRequestStarted(account: ManagedAccount): void {\n const key = this.accountKey(account)\n const now = Date.now()\n this.inFlight.set(key, { count: this.inFlightCount(key, now) + 1, at: now })\n }\n\n /** Record that one upstream request for this account has settled, on any path. */\n noteRequestSettled(account: ManagedAccount): void {\n const key = this.accountKey(account)\n const now = Date.now()\n const count = this.inFlightCount(key, now)\n if (count <= 1) this.inFlight.delete(key)\n else this.inFlight.set(key, { count: count - 1, at: now })\n }\n\n /**\n * Which accounts have upstream requests in flight, for the UI's live line.\n *\n * The join runs HERE, not in the RPC layer: the in-flight map is keyed by\n * `accountKey` (id ?? email ?? idx-<refresh>), a derivation the management\n * surface must not have to replicate — its own email-first key would miss\n * every account carrying both fields. Counts go through `inFlightCount`, so\n * an entry leaked past its TTL reads as idle instead of busy forever.\n */\n async inFlightAccounts(): Promise<Array<{ index: number; email: string | null; count: number }>> {\n const storage = await this.store.load()\n const now = Date.now()\n const busy: Array<{ index: number; email: string | null; count: number }> = []\n for (const [index, account] of storage.accounts.entries()) {\n const count = this.inFlightCount(this.accountKey(account), now)\n if (count > 0) busy.push({ index, email: account.email ?? null, count })\n }\n return busy\n }\n\n /** Last fraction snapshot per account, for `sampleLimitBurn` (memory only). */\n private readonly limitFractions = new Map<string, LimitSample>()\n /** Latest burn rate per account: bucketId -> fraction/hour (memory only). */\n private readonly limitBurn = new Map<string, { perHour: Record<string, number> }>()\n\n private noteLimitSample(key: string, groups: QuotaGroup[], at: number): void {\n const fractions: Record<string, number> = {}\n for (const group of groups) {\n for (const window of group.windows) {\n if (window.remainingFraction !== null) fractions[window.bucketId] = window.remainingFraction\n }\n }\n const next: LimitSample = { at, fractions }\n const burn = sampleLimitBurn(this.limitFractions.get(key), next)\n // A null result keeps the previous rate: \"no drop since the last sample\"\n // is not evidence the burn stopped, just that these two samples saw none.\n if (burn !== null) this.limitBurn.set(key, { perHour: burn })\n this.limitFractions.set(key, next)\n }\n\n /**\n * Burn rates per account INDEX, for the `account.limits` reply.\n *\n * The index join runs here, like `inFlightAccounts`: the rate map is keyed\n * by `accountKey`, a derivation the management surface must not replicate.\n */\n async limitBurnRates(): Promise<Array<{ index: number; perHour: Record<string, number> }>> {\n const storage = await this.store.load()\n const out: Array<{ index: number; perHour: Record<string, number> }> = []\n for (const [index, account] of storage.accounts.entries()) {\n const entry = this.limitBurn.get(this.accountKey(account))\n if (entry !== undefined) out.push({ index, perHour: entry.perHour })\n }\n return out\n }\n\n /** The map key for a conversation; anonymous callers share one bucket. */\n private conversationKeyFor(conversationKey?: string): string {\n const trimmed = conversationKey?.trim()\n return trimmed && trimmed.length > 0 ? trimmed : AgySessionManager.ANONYMOUS_CONVERSATION\n }\n\n /** Drop expired pins and keep the map bounded. */\n private pruneAffinity(now: number): void {\n for (const [conversation, pin] of this.affinity) {\n if (now - pin.at >= SESSION_AFFINITY_WINDOW_MS) this.affinity.delete(conversation)\n }\n while (this.affinity.size > AgySessionManager.MAX_AFFINITY_ENTRIES) {\n const oldest = this.affinity.keys().next()\n if (oldest.done) break\n this.affinity.delete(oldest.value)\n }\n }\n\n /** The account key this conversation is pinned to, when the pin is still fresh. */\n private affinityFor(conversationKey: string | undefined, now: number): string | null {\n const pin = this.affinity.get(this.conversationKeyFor(conversationKey))\n if (!pin || now - pin.at >= SESSION_AFFINITY_WINDOW_MS) return null\n return pin.key\n }\n\n /** Pin one conversation to one account. Re-inserts so eviction stays LRU. */\n private setAffinity(conversationKey: string | undefined, accountKey: string, now: number): void {\n const conversation = this.conversationKeyFor(conversationKey)\n this.affinity.delete(conversation)\n this.affinity.set(conversation, { key: accountKey, at: now })\n this.pruneAffinity(now)\n }\n\n /**\n * Drop every pin pointing at one account.\n *\n * A rotation or a skip means the account just proved unusable for the\n * conversation that was pinned to it; other conversations pinned elsewhere keep\n * their pins, which the single-slot version could not express.\n */\n private clearAffinityForAccount(accountKey: string): void {\n for (const [conversation, pin] of this.affinity) {\n if (pin.key === accountKey) this.affinity.delete(conversation)\n }\n }\n\n /**\n * Clear all session affinity pins.\n *\n * Called when an account is explicitly activated by the user, so all\n * ongoing and upcoming conversations immediately steer to the newly\n * activated account rather than staying pinned to the previous one.\n */\n clearAllAffinity(): void {\n this.affinity.clear()\n }\n\n /**\n * Explicitly activate an account by index: sets `storage.activeIndex = index`,\n * clears session affinity pins so conversations immediately switch to the\n * activated account, and resets transient failure counts.\n */\n async activateAccount(index: number): Promise<void> {\n await this.store.mutate((storage) => {\n if (index < 0 || index >= storage.accounts.length) {\n throw new Error('account not found')\n }\n storage.activeIndex = index\n })\n this.clearAllAffinity()\n }\n\n constructor(options: SessionManagerOptions) {\n this.store = options.store\n this.onRotate = options.onRotate\n this.onHealthReport = options.onHealthReport\n this.recordUsageOption = options.recordUsage\n }\n\n /** Emit one non-chat usage record; a ledger failure never breaks the call. */\n private emitUsage(record: UsageRecord): void {\n try {\n this.recordUsageOption?.(record)\n } catch {\n // Swallowed by design: statistics are diagnostics.\n }\n }\n\n private accountKey(account: ManagedAccount): string {\n return account.id ?? account.email ?? `idx-${account.refresh}`\n }\n\n /**\n * Refresh the account's access token (single-flight per account). A transient\n * refresh failure keeps the cached token in place (retain-last-good): the old\n * token stays valid until its own expiry and a later request retries the\n * refresh. Only `invalid_grant` drops the cache.\n */\n private refreshToken(key: string, account: ManagedAccount, cached?: TokenCacheEntry): Promise<OAuthAuthDetails | undefined> {\n const inFlight = this.refreshInFlight.get(key)\n if (inFlight) return inFlight\n\n const refreshing = (async (): Promise<OAuthAuthDetails | undefined> => {\n const result = await refreshAccessToken(\n { access: cached?.access ?? '', expires: cached?.expires ?? 0, refresh: account.refresh },\n { clientId: account.clientId, proxyUrl: account.proxy },\n )\n if (result.type === 'success') {\n if (!account.clientId && result.clientId) {\n await this.store.mutate((s) => {\n const target = s.accounts.find((candidate) => this.accountKey(candidate) === key)\n if (!target) throw new Error(`Account ${key} not found during clientId migration`)\n target.clientId = result.clientId\n })\n account.clientId = result.clientId\n }\n // Cache token strictly AFTER disk persistence has succeeded\n this.tokenCache.set(key, { access: result.auth.access, expires: result.auth.expires })\n return result.auth\n }\n if (result.type === 'failed') {\n if (cached && !accessTokenExpired({ access: cached.access, expires: cached.expires, refresh: account.refresh })) {\n return { access: cached.access, expires: cached.expires, refresh: account.refresh }\n }\n // Only an explicit per-account proxy may be judged proxy-unreachable.\n // Without one a bare ECONNRESET is a transient network error, and\n // reporting it as proxy_unreachable made a healthy single account look\n // like a dead proxy (issue #29).\n if (account.proxy && isProxyUnreachableError(result.error)) {\n throw new AgyAuthError('transport', 'proxy_unreachable', { cause: result.error })\n }\n const kind = result.error.status === 429\n ? 'rate-limit'\n : result.error.status === 0 || result.error.status === 408 || result.error.status >= 500\n ? 'transport'\n : 'invalid-credential'\n throw new AgyAuthError(kind, describeFetchError(result.error), { cause: result.error })\n }\n if (result.type === 'revoked') {\n // Account credentials are dead — mark it disabled and verificationRequired in the store\n this.tokenCache.delete(key)\n await this.store.mutate((s) => {\n const target = s.accounts.find((candidate) => this.accountKey(candidate) === key)\n if (target) {\n target.enabled = false\n target.verificationRequired = true\n target.verificationRequiredAt = Date.now()\n target.verificationRequiredReason = 'auth-failure'\n }\n })\n account.enabled = false\n account.verificationRequired = true\n account.verificationRequiredAt = Date.now()\n account.verificationRequiredReason = 'auth-failure'\n return undefined\n }\n return undefined\n })()\n this.refreshInFlight.set(key, refreshing)\n refreshing.then(\n () => this.refreshInFlight.delete(key),\n () => this.refreshInFlight.delete(key),\n )\n return refreshing\n }\n\n /** Resolve a usable access token for the account, pre-emptively refreshing near expiry. */\n private async accessTokenFor(account: ManagedAccount): Promise<OAuthAuthDetails | undefined> {\n const key = this.accountKey(account)\n const cached = this.tokenCache.get(key)\n const now = Date.now()\n if (cached && !accessTokenExpired({ access: cached.access, expires: cached.expires, refresh: account.refresh })) {\n // Pre-emptive refresh within the skew: serve the still-valid token and\n // refresh in the background (OMP-style refresh skew).\n if (cached.expires <= now + AgySessionManager.REFRESH_SKEW_MS) {\n void this.refreshToken(key, account, cached)\n }\n return { access: cached.access, expires: cached.expires, refresh: account.refresh }\n }\n return this.refreshToken(key, account, cached)\n }\n\n /**\n * Probe `retrieveUserQuotaSummary` for one account, sharing ONE in-flight\n * promise with every other caller in this cycle.\n *\n * Returns `[]` on any failure — the fetch itself already answers a dead\n * endpoint with an empty list — so both consumers read \"no groups\" the same\n * way. The promise is dropped as soon as it settles, so the NEXT cycle\n * re-probes.\n *\n * The account's proxy is honoured here or the call would egress direct and\n * leak the account's real IP, and one timeout bounds the whole endpoint\n * fallback chain.\n */\n private probeQuotaSummary(account: ManagedAccount): Promise<QuotaGroup[]> {\n const key = this.accountKey(account)\n const existing = this.quotaSummaryInFlight.get(key)\n if (existing) return existing\n const probe = (async (): Promise<QuotaGroup[]> => {\n try {\n const auth = await this.accessTokenFor(account)\n if (!auth) return []\n const { fetchQuotaSummary } = await import('./adapter/quota-summary.ts')\n const routed = accountFetch({ proxyUrl: account.proxy })\n const bounded = (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS)\n const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout\n return routed(input, { ...init, signal })\n }\n return await fetchQuotaSummary(auth.access, account.projectId, bounded)\n } catch {\n // Best-effort: an empty result is the caller's \"unknown\", never a throw.\n return []\n }\n })()\n this.quotaSummaryInFlight.set(key, probe)\n const release = (): void => {\n // Only drop the promise we created, so a slow settle cannot remove a later\n // cycle's probe.\n if (this.quotaSummaryInFlight.get(key) === probe) this.quotaSummaryInFlight.delete(key)\n }\n void probe.then(release, release)\n return probe\n }\n\n /**\n * Refresh the display-only 5h/weekly windows for every enabled account.\n *\n * SEPARATE from `refreshQuotaCache`, and deliberately so. That method writes\n * `cachedQuota`, which feeds the scheduling path: `rankPoolCandidates` turns a\n * measured `remainingFraction <= 0` into a `blockedUntil`, so measuring a\n * pool's quota can BLOCK an account. That is safe only with a fallback, which\n * is why `getSession` gates it on `eligible.length > 1` — and that gate is also\n * why a solo account never had `cachedLimits` populated and the limits card\n * showed \"not measured yet\" forever.\n *\n * Dropping the gate instead would have been a real outage, not a display fix:\n * a solo account whose family is measured at 0 gets `AgyPoolBlockedError` with\n * no other account to serve the request (verified). So the windows are fetched\n * here WITHOUT writing `cachedQuota`, which makes them safe to measure for a\n * pool of any size — a solo account included.\n *\n * Cost is bounded by the same TTL the scheduling path uses, so this runs at\n * most once per window rather than per request.\n *\n * KNOWN LIMITATION (deliberate, not overlooked): a FAILED probe writes no\n * marker, so \"probe failed\" and \"never probed\" are indistinguishable and\n * `isLimitsStale` reports stale again on the next call. The practical effect is\n * that an account whose endpoint never returns groups is re-probed each time\n * the accounts page is opened, with no backoff. Acceptable today — the call is\n * display-only, TTL-bounded, and triggered by opening a tab rather than by a\n * poll — so no negative-TTL field is added yet. Revisit if the trigger becomes\n * frequent or the pool grows enough that the extra calls matter.\n *\n * @param storage - the loaded storage document, overlaid in memory on success.\n * @param options - `force` re-probes inside the TTL.\n * @returns which accounts were measured, so a caller that asked for a refresh\n * can REPORT it. Without this a forced refresh that failed was completely\n * silent: no cache write, no `updatedAt` change, nothing on screen — the\n * same \"clicked it, saw a flicker\" defect as an unread RPC verdict.\n */\n async refreshLimits(\n storage: AccountStorageV4,\n options: { force?: boolean } = {},\n ): Promise<LimitsRefreshResult> {\n const now = Date.now()\n // A separate staleness rule: `isQuotaStale` is driven by the SCHEDULING\n // cache, which a solo account never fills, so reusing it would report\n // \"stale\" on every call and re-probe continuously. `force` bypasses it for\n // an explicit user request, which is the only thing that may spend an\n // upstream call inside the TTL.\n const candidates = storage.accounts.filter((account) => account.enabled !== false)\n const targets = options.force === true\n ? candidates\n : candidates.filter((account) => isLimitsStale(account, now))\n if (targets.length === 0) {\n return { measured: [], failed: [], skipped: candidates.length }\n }\n\n const probes = await Promise.all(targets.map(async (account) => {\n const key = this.accountKey(account)\n try {\n // The SHARED probe: the scheduling path joins the same in-flight promise\n // for this account, so one cycle pays one round trip and both caches read\n // the same groups. It only carries the FETCH — this path still writes\n // `cachedLimits` and nothing else.\n const groups = await this.probeQuotaSummary(account)\n if (groups.length === 0) return { key, ok: false as const }\n return { key, ok: true as const, groups, updatedAt: Date.now() }\n } catch {\n // Best-effort: a failed probe leaves the previous windows in place.\n return { key, ok: false as const }\n }\n }))\n\n const updates = probes.filter((probe): probe is\n { key: string, ok: true, groups: QuotaGroup[], updatedAt: number } => probe.ok)\n const failed = probes.filter((probe) => !probe.ok).map((probe) => probe.key)\n // Report the skip count so a caller can distinguish \"nothing was due\" from\n // \"everything failed\" — the two read identically on screen otherwise.\n // Counted over the same eligible set the early return uses, so a disabled\n // account never inflates it into a phantom \"still fresh\".\n const result: LimitsRefreshResult = {\n measured: updates.map((update) => update.key),\n failed,\n skipped: candidates.length - targets.length,\n }\n if (updates.length === 0) return result\n\n for (const update of updates) {\n const target = storage.accounts.find((candidate) => this.accountKey(candidate) === update.key)\n // Only `cachedLimits` — never `cachedQuota`, which is what keeps this\n // incapable of blocking an account.\n if (target) {\n target.cachedLimits = { groups: update.groups, updatedAt: update.updatedAt }\n // Burn-rate sampling rides the same successful probe: memory only, so\n // it cannot affect the store or the scheduling path.\n this.noteLimitSample(update.key, update.groups, update.updatedAt)\n }\n }\n // Drop samples for accounts that no longer exist, so the maps stay bounded\n // by the live pool rather than by every account ever seen.\n const live = new Set(storage.accounts.map((account) => this.accountKey(account)))\n for (const key of this.limitFractions.keys()) {\n if (!live.has(key)) this.limitFractions.delete(key)\n }\n for (const key of this.limitBurn.keys()) {\n if (!live.has(key)) this.limitBurn.delete(key)\n }\n try {\n await this.store.mutate((s) => {\n for (const update of updates) {\n const target = s.accounts.find((candidate) => this.accountKey(candidate) === update.key)\n if (target) target.cachedLimits = { groups: update.groups, updatedAt: update.updatedAt }\n }\n })\n } catch {\n // A derived display cache; a failed write degrades gracefully.\n }\n return result\n }\n\n /**\n * Refresh stale per-account quota caches (family-scoped, health-based TTL).\n * Failures leave the account unmeasured: ranking treats it as a fallback\n * instead of blocking selection on a hung endpoint.\n */\n private async refreshQuotaCache(storage: AccountStorageV4): Promise<void> {\n const now = Date.now()\n const stale = storage.accounts.filter((account) => account.enabled !== false && isQuotaStale(account, now))\n if (stale.length === 0) return\n\n const results = await Promise.all(stale.map(async (account) => {\n const key = this.accountKey(account)\n if (this.quotaRefreshInFlight.has(key)) return this.quotaRefreshInFlight.get(key)\n const refresh = (async () => {\n try {\n const auth = await this.accessTokenFor(account)\n if (!auth) return null\n const { fetchAvailableModels } = await import('./adapter/models.ts')\n const routed = accountFetch({ proxyUrl: account.proxy })\n // ONE timeout per probe, both bounded by the same budget the model\n // probe already used; the account's proxy is honoured on both, or the\n // summary call would egress direct and leak the account's real IP.\n const bounded: typeof fetch = (input, init) => {\n const timeout = AbortSignal.timeout(AgySessionManager.QUOTA_FETCH_TIMEOUT_MS)\n const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout\n return routed(input, { ...init, signal })\n }\n // The weekly window comes ONLY from the summary endpoint. It is read\n // through the SHARED probe, so the display path and this one pay one\n // round trip per account instead of two and cannot disagree about what\n // upstream said. This path still decides WHERE the result lands —\n // `cachedQuota`, the scheduling cache `refreshLimits` must never touch.\n const [discovered, groups] = await Promise.all([\n fetchAvailableModels(auth.access, account.projectId, bounded),\n this.probeQuotaSummary(account),\n ])\n const quotas = ingestFamilyQuotas(discovered, groups, account.cachedQuota)\n return { key, quotas, updatedAt: Date.now() }\n } catch {\n return null\n }\n })()\n this.quotaRefreshInFlight.set(key, refresh)\n try {\n return await refresh\n } finally {\n this.quotaRefreshInFlight.delete(key)\n }\n }))\n\n const updates = results.filter((r): r is { key: string; quotas: Record<string, CachedQuota>; updatedAt: number } => Boolean(r && Object.keys(r.quotas).length > 0))\n if (updates.length > 0) {\n for (const update of updates) {\n const target = storage.accounts.find((candidate) => this.accountKey(candidate) === update.key)\n if (target) {\n target.cachedQuota = update.quotas\n target.cachedQuotaUpdatedAt = update.updatedAt\n }\n }\n try {\n await this.store.mutate((s) => {\n for (const update of updates) {\n const target = s.accounts.find((candidate) => this.accountKey(candidate) === update.key)\n if (target) {\n target.cachedQuota = update.quotas\n target.cachedQuotaUpdatedAt = update.updatedAt\n }\n }\n })\n } catch {\n // Quota is a derived cache — a failed write degrades gracefully without\n // failing the active request (next refresh cycle re-ingests).\n }\n }\n }\n\n /**\n * Pick the account for one request: the affinity pin wins while it is fresh,\n * healthy, and not quota-exhausted for the requested model; the explicitly\n * activated account wins next while it is usable; otherwise the pool is\n * ranked by family-scoped usage (OMP-aligned) and the best candidate wins.\n */\n private async pickAccount(\n storage: AccountStorageV4,\n model?: string,\n conversationKey?: string,\n ): Promise<{ account: ManagedAccount; index: number } | undefined> {\n const now = Date.now()\n for (const account of storage.accounts) clearExpiredState(account, now)\n\n const family = modelFamilyOf(model)\n const familyKey = familyKeyOf(model)\n // Conversation affinity: reuse the account this conversation is already\n // pinned to while it is fresh and healthy, so one conversation stays on one\n // account. A quota-exhausted family or a cooldown breaks the pin and\n // re-ranks, mirroring OMP's pinned-until-unusable: the pin holds until the\n // account is actually spent (a measured zero, not the soft-drain threshold\n // `isFamilyDrained` applies when ranking), so the pin and the active-account\n // preference below agree on when to let go.\n const pinnedKey = this.affinityFor(conversationKey, now)\n if (pinnedKey !== null) {\n const lastIndex = storage.accounts.findIndex((a) => this.accountKey(a) === pinnedKey)\n if (lastIndex !== -1) {\n const last = storage.accounts[lastIndex]!\n if (\n last.enabled !== false &&\n !isCoolingDown(last, now) &&\n !isFamilyRateLimited(last, familyKey, now) &&\n !isFamilyQuotaExhausted(last, family, now)\n ) {\n return { account: last, index: lastIndex }\n }\n }\n }\n const eligible = storage.accounts\n .map((account, index) => ({ account, index }))\n .filter(({ account }) => account.enabled !== false)\n if (eligible.length === 0) return undefined\n\n // Prioritize the active account as long as it is enabled, not cooling down,\n // not family rate-limited, and not quota-exhausted.\n const active = storage.accounts[storage.activeIndex]\n const isActiveUsable = (acc: ManagedAccount | undefined): acc is ManagedAccount =>\n acc !== undefined\n && acc.enabled !== false\n && !isCoolingDown(acc, now)\n && !isFamilyRateLimited(acc, familyKey, now)\n && !isFamilyQuotaExhausted(acc, family, now)\n\n if (isActiveUsable(active) && this.inFlightCount(this.accountKey(active), now) < MAX_IN_FLIGHT_PER_ACCOUNT) {\n return { account: active, index: storage.activeIndex }\n }\n\n const ranked = rankPoolCandidates(eligible, model, now, storage.activeIndex)\n // Prefer an account with in-flight headroom so concurrent conversations\n // spread across the pool; fall back to plain ranking when every candidate is\n // saturated (see `inFlight` for why this is a preference, not a gate).\n const picked =\n ranked.find((candidate) =>\n candidate.blockedUntil === null\n && this.inFlightCount(this.accountKey(candidate.account), now) < MAX_IN_FLIGHT_PER_ACCOUNT)\n ?? ranked.find((candidate) => candidate.blockedUntil === null)\n if (!picked) {\n const quotaExhausted = (account: ManagedAccount): boolean => {\n if (account.cooldownReason === 'quota-exhausted' && (account.coolingDownUntil ?? 0) > now) return true\n const quota = familyQuotaFor(account, family, now)\n if (!quota) return false\n // BOTH windows, matching `rankPoolCandidates`, which blocks on a spent week\n // as well as on a spent 5-hour bucket. Reading only `remainingFraction`\n // here classified a weekly-decided block as retryable, and that path ends\n // at `RATE_LIMIT` + a ~5-day `providerRetryAfterMs`, which DSH's 10s retry\n // cap turns into giving up on the turn — a quota condition reported to the\n // user as a rate limit.\n const spent = (fraction: number | undefined, resetTime: string | undefined): boolean => {\n if (typeof fraction !== 'number' || fraction > 0 || !resetTime) return false\n const resetAt = Date.parse(resetTime)\n return !Number.isNaN(resetAt) && resetAt > now\n }\n return spent(quota.remainingFraction, quota.resetTime)\n || spent(quota.weeklyFraction, quota.weeklyResetTime)\n }\n const retryable = ranked.filter((candidate) => !quotaExhausted(candidate.account))\n const blocked = retryable.length > 0 ? retryable : ranked\n const blockedUntil = Math.min(...blocked.map((candidate) => candidate.blockedUntil ?? now))\n throw new AgyPoolBlockedError(retryable.length > 0 ? 'retryable' : 'quota-exhausted', blockedUntil)\n }\n if (picked.index !== storage.activeIndex) {\n storage.activeIndex = picked.index\n await this.store.mutate((s) => {\n s.activeIndex = picked.index\n })\n }\n return { account: picked.account, index: picked.index }\n }\n /**\n * Adapter hook: resolve the active session (refresh if needed), healing a\n * missing projectId at request time — the OAuth-time loadCodeAssist may have\n * transiently failed even when the Google account owns a Cloud Code project\n * (mirrors OmniRoute's ensureAntigravityProjectAssigned + persistence).\n * @param model - requested model id; drives family-scoped quota ranking.\n * @param accountIndex - resolve this exact account instead of ranking the pool.\n * Used by the management \"Test call\" action, where testing a different\n * account than the one the user clicked would report a result for the wrong\n * account. Deliberately does NOT update the affinity pin: a one-shot test\n * must not steer the next real conversation onto the account it probed.\n * @param conversationKey - the conversation's identity (`GenerateOptions.sessionId`),\n * which scopes account affinity. Concurrent conversations therefore hold\n * independent pins. Omitted by callers with no conversation (CLI, probes),\n * which share one anonymous bucket.\n */\n async getSession(\n model?: string,\n accountIndex?: number,\n conversationKey?: string,\n ): Promise<AgyAccountSession | undefined> {\n let storage = await this.store.load()\n if (accountIndex !== undefined) {\n // Fail loudly rather than silently falling back to the pool: a test that\n // reports on an account it was not asked about is worse than an error.\n const account = storage.accounts[accountIndex]\n if (!account) throw new Error(`account #${accountIndex} not found`)\n if (account.enabled === false) throw new Error(`account #${accountIndex} is disabled`)\n }\n const maxAttempts = accountIndex === undefined\n ? storage.accounts.filter((account) => account.enabled !== false).length\n : 1\n let proxyUnreachableCount = 0\n /**\n * Last transport failure seen on a *proxyless* account. Such an account is\n * skipped (another enabled account may be healthy) without writing a\n * cooldown: a cooldown would surface as AgyPoolBlockedError — i.e. RATE_LIMIT\n * for a plain network error — and would block a solo pool outright. It is\n * remembered so the request still reports its real cause rather than\n * degrading into \"no account configured\" when every account fails this way.\n */\n let lastTransportError: unknown\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const eligible = storage.accounts.filter((account) => account.enabled !== false)\n if (accountIndex === undefined && eligible.length > 1) {\n await this.refreshQuotaCache(storage)\n // In-memory overlay on storage already took place in refreshQuotaCache.\n }\n const picked = accountIndex === undefined\n ? await this.pickAccount(storage, model, conversationKey)\n : (() => {\n const account = storage.accounts[accountIndex]\n if (!account) throw new Error(`account #${accountIndex} not found`)\n if (account.enabled === false) throw new Error(`account #${accountIndex} is disabled`)\n return { account, index: accountIndex }\n })()\n if (!picked) return undefined\n let auth: OAuthAuthDetails | undefined\n try {\n auth = await this.accessTokenFor(picked.account)\n } catch (error) {\n if (Boolean(picked.account.proxy) && isProxyUnreachableError(error)) {\n // Fail-closed for per-account proxy: skip this account for this request\n // without a cooldown (the proxy may recover; a cooldown would also hide\n // the real cause and block a solo proxied pool).\n proxyUnreachableCount++\n // A pinned caller asked about ONE account: falling over to a different\n // one would answer a question nobody asked, and mutating the pool\n // cursor for a diagnostic probe would be a side effect the user did\n // not request. Surface the failure instead.\n if (accountIndex !== undefined) throw error\n storage = await this.skipAccount(storage, picked.account)\n continue\n }\n // Otherwise the same socket codes describe the connection, not a proxy\n // (issue #29): fall over to the next enabled account, remembering the\n // failure in case none of them succeeds.\n lastTransportError = error\n if (accountIndex !== undefined) throw error\n storage = await this.skipAccount(storage, picked.account)\n continue\n }\n if (!auth) {\n // The selected credential was revoked and disabled by accessTokenFor.\n if (accountIndex !== undefined) {\n throw new Error(`account #${accountIndex} credential is no longer valid — run \\`dsh-agy login\\``)\n }\n // Re-read and select another enabled account within this same request.\n this.clearAffinityForAccount(this.accountKey(picked.account))\n storage = await this.store.load()\n continue\n }\n\n const key = this.accountKey(picked.account)\n if (!picked.account.projectId && !this.projectRetryFailed.has(key)) {\n try {\n const { loadCodeAssist } = await import('./oauth/exchange.ts')\n const { projectId } = await loadCodeAssist(auth.access, { proxyUrl: picked.account.proxy })\n if (projectId) {\n await this.store.mutate((s) => {\n const account = s.accounts.find((candidate) => this.accountKey(candidate) === key)\n if (account) {\n account.projectId = projectId\n // Keep the packed refresh string in sync.\n const parts = parseRefreshParts(account.refresh)\n account.refresh = formatRefreshParts({\n refreshToken: parts.refreshToken,\n projectId,\n managedProjectId: parts.managedProjectId,\n })\n }\n })\n picked.account.projectId = projectId\n } else {\n this.projectRetryFailed.add(key)\n }\n } catch {\n this.projectRetryFailed.add(key)\n }\n }\n\n // Create the account's device identity on first USE, not on first\n // rate-limit. Platform, version and SDK-client are chosen once here and\n // frozen, so one account presents one coherent device for its whole life.\n //\n // Previously the identity did not exist until the account's first 429, and\n // the pre-fingerprint fallback re-picked a platform per request — so a\n // single account's consecutive calls claimed `windows/amd64` and then\n // `darwin/arm64`. An OS that changes between two requests of one session is\n // a stronger anomaly than a stale version, and it was present on the most\n // common path (every fresh account, until it happened to hit a limit).\n if (!picked.account.fingerprint) {\n const fingerprint = generateFingerprint(undefined, currentAgyVersion())\n const history = recordFingerprintVersion(picked.account.fingerprintHistory, fingerprint, 'initial')\n await this.store.mutate((s) => {\n const account = s.accounts.find((candidate) => this.accountKey(candidate) === key)\n // Re-check under the lock: a concurrent request may have created one.\n if (account && !account.fingerprint) {\n account.fingerprint = fingerprint\n account.fingerprintHistory = history\n }\n })\n picked.account.fingerprint = fingerprint\n picked.account.fingerprintHistory = history\n }\n\n // A pinned (test) call must not touch the affinity pin: probing an account\n // is not \"using\" it, and pinning would steer the next real conversation.\n if (accountIndex === undefined) this.setAffinity(conversationKey, key, Date.now())\n return {\n auth,\n account: picked.account,\n index: picked.index,\n impersonation: impersonationHeadersFor(picked.account),\n }\n }\n\n if (proxyUnreachableCount === maxAttempts && maxAttempts > 0) {\n throw new AgyAuthError('transport', 'proxy_unreachable')\n }\n // Every account failed for a non-proxy reason: surface the real cause rather\n // than `undefined`, which the adapter would report as NO_CREDENTIAL\n // (\"no account configured\") — misleading, and wrong for a transient blip.\n if (lastTransportError !== undefined) throw lastTransportError\n return undefined\n }\n\n /**\n * Drop the session pin and move the pool cursor off an account that just\n * failed, so the next `pickAccount` in this same request tries another one.\n * State on the account itself is deliberately left untouched (no cooldown).\n */\n private async skipAccount(storage: AccountStorageV4, account: ManagedAccount): Promise<AccountStorageV4> {\n this.clearAffinityForAccount(this.accountKey(account))\n const key = this.accountKey(account)\n const deadIndex = storage.accounts.findIndex((candidate) => this.accountKey(candidate) === key)\n if (deadIndex !== -1) {\n const next = pickNextAccountIndex(storage.accounts, deadIndex, Date.now())\n if (next !== storage.activeIndex) {\n storage.activeIndex = next\n await this.store.mutate((s) => { s.activeIndex = next }).catch(() => {})\n }\n }\n return this.store.load()\n }\n\n /** Adapter hook: apply rotation decisions and fingerprint regeneration. */\n async reportFailure(\n kind: FailureKind,\n session: AgyAccountSession,\n info?: {\n retryAfterMs?: number\n status?: number\n rateLimitCategory?: import('./runtime/classify.ts').RateLimitCategory\n /** Server-reported absolute reset time; drives precise cooldowns. */\n resetTime?: string\n /** Requested model id; drives family-scoped rate-limit bookkeeping. */\n model?: string\n /** Appeal link from a `verification-required` body; surfaced to the user. */\n verificationUrl?: string\n },\n ): Promise<void> {\n if (!session?.account) return\n const key = this.accountKey(session.account)\n const consecutive = (this.failureCounts.get(key) ?? 0) + 1\n this.failureCounts.set(key, consecutive)\n let nextIndexToRotate: number | null = null\n const fpCachedVersion = kind === 'rate-limit' ? peekCachedAntigravityVersion() : null\n // The version a newly generated fingerprint advertises. Cache first (no I/O),\n // then a bounded live resolve, then `currentAgyVersion()` (the resolved version\n // or the pinned fallback) — and never `generateFingerprint`'s own default,\n // which picks a RANDOM entry from `versionPool`: whatever is chosen here is\n // frozen into the account's identity for its lifetime, so a cold start with an\n // unreachable feed could otherwise advertise a two-minor-old client forever.\n // The probe rides the FAILING account's egress: the feeds belong to no account,\n // but the request still egresses the host, and the boot-time probe already\n // routes this way (see `probeFetch`).\n const fpResolvedVersion = kind === 'rate-limit'\n ? (fpCachedVersion ?? (await resolveAntigravityVersionBounded(750, probeFetch(session.account.proxy))) ?? currentAgyVersion())\n : currentAgyVersion()\n\n await this.store.mutate((storage) => {\n const account = storage.accounts.find((a) => this.accountKey(a) === key)\n if (!account) return\n\n const decision = decideRotation(kind, account, consecutive, info?.retryAfterMs, info?.rateLimitCategory, info?.resetTime)\n\n // Keep the appeal link beside the challenge state so a user can act on it.\n // Only overwritten when the upstream actually supplied one, so a later\n // challenge without a URL does not erase a previously captured link.\n if (kind === 'verification-required' && info?.verificationUrl) {\n account.verificationUrl = info.verificationUrl\n }\n\n if (kind === 'rate-limit' && info?.rateLimitCategory !== 'soft_rate_limit') {\n // Family-scoped bookkeeping of the real reset (display + ranking wall).\n // Soft rate limits are transient bursts handled via immediate retry — do not block the family.\n const familyKey = familyKeyOf(info?.model)\n const resetMs = parseFutureResetMs(info?.resetTime, Date.now()) ??\n (Date.now() + Math.min(info?.retryAfterMs ?? RATE_LIMIT_COOLDOWN_MS, MAX_RATE_LIMIT_COOLDOWN_MS))\n recordRateLimit(account, familyKey, resetMs)\n }\n\n if (decision.action === 'revoke') {\n this.tokenCache.delete(key)\n this.failureCounts.delete(key)\n const currentIndex = storage.accounts.findIndex((a) => this.accountKey(a) === key)\n const nextIndex = pickNextAccountIndex(storage.accounts, currentIndex >= 0 ? currentIndex : storage.activeIndex, Date.now())\n if (nextIndex !== storage.activeIndex) {\n storage.activeIndex = nextIndex\n nextIndexToRotate = nextIndex\n }\n this.clearAffinityForAccount(key)\n return\n }\n\n // Fingerprint lifecycle: create on first rate-limit, regenerate after\n // repeated failures (bounded by history inside recordFingerprintVersion).\n // UA versions come from the version resolver (bounded, cached 6h) so\n // fingerprints never pin a stale Antigravity client version. The `stable`\n // risk mode pins one identity per account: create once, never regenerate.\n //\n // KNOWN DEFECT (recorded, deliberately not changed): this block is gated\n // on `kind === 'rate-limit'`, so the identity is rebuilt exactly when\n // quota runs out — and NEVER on `verification-required`, i.e. not when\n // upstream actually gates the account. The coupling is inverted with\n // respect to intent. Two further reasons the rebuild is weaker than it\n // looks: `consecutive` is counted per accountKey while the failing account\n // has just been rotated away (so reaching 2 requires it to be picked\n // again first), and of the five `Fingerprint` fields only `userAgent` and\n // `apiClient` ever reach a request header (`buildRequestHeaders`;\n // `deviceId`/`sessionToken` are never sent, `clientMetadata` only rides\n // the control-plane calls). So a \"new identity\" re-rolls two header\n // values, one of them a UA shape no artifact confirms. Fixing the gate\n // alone would not make the mechanism load-bearing — decide what identity\n // is actually transmitted before widening it.\n if (kind === 'rate-limit' && info?.rateLimitCategory !== 'soft_rate_limit') {\n if (!account.fingerprint) {\n account.fingerprint = generateFingerprint(undefined, fpResolvedVersion)\n account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, account.fingerprint, 'initial')\n } else {\n if (fpCachedVersion) updateFingerprintVersion(account.fingerprint, fpCachedVersion)\n if (fingerprintMode() !== 'stable' && consecutive >= 2) {\n const fresh = generateFingerprint(undefined, fpResolvedVersion)\n account.fingerprintHistory = recordFingerprintVersion(account.fingerprintHistory, fresh, 'regenerated')\n account.fingerprint = fresh\n }\n }\n }\n\n if (decision.action === 'rotate' || decision.action === 'cool') {\n const currentIndex = storage.accounts.findIndex((a) => this.accountKey(a) === key)\n const familyKey = familyKeyOf(info?.model)\n const nextIndex = pickNextAccountIndex(storage.accounts, currentIndex >= 0 ? currentIndex : storage.activeIndex, Date.now(), familyKey)\n if (nextIndex !== storage.activeIndex) {\n storage.activeIndex = nextIndex\n nextIndexToRotate = nextIndex\n }\n // Only conversations pinned to the failed account are freed; a pin on\n // another account was never this failure's business.\n this.clearAffinityForAccount(key)\n }\n })\n\n if (nextIndexToRotate !== null) {\n this.onRotate?.(session.index, nextIndexToRotate, kind)\n // Counted as a pool event, not a request: the adapter already recorded\n // the request that failed, and counting it again here would inflate it.\n this.emitUsage({\n ...(session.account.email === undefined && session.account.id === undefined\n ? {}\n : { account: session.account.email ?? session.account.id }),\n source: 'chat',\n ok: false,\n rotated: true,\n poolEvent: true,\n // The rotation's cause IS the answer the recent list exists for.\n reason: kind,\n })\n }\n }\n /** Adapter hook: reset the failure counter after a clean completion. */\n async markSuccess(session: AgyAccountSession): Promise<void> {\n const account = session.account\n const key = this.accountKey(account)\n this.failureCounts.delete(key)\n }\n\n /**\n * Test call: one short streaming request against the live backend.\n * Returns the collected text or a structured error message.\n * @param model - model id to exercise.\n * @param options - probe overrides.\n * - `prompt` / `maxTokens`: the request shape (defaults to a one-token reply).\n * - `accountIndex`: test this exact account instead of letting the pool rank\n * one. The management UI exposes \"Test call\" per account row, so without\n * this the probe ran on whichever account affinity picked, and its result\n * was both returned and recorded against that other account.\n */\n async testCall(\n model: string,\n options: { prompt?: string; maxTokens?: number; accountIndex?: number } = {},\n ): Promise<{ ok: boolean; text?: string; error?: string }> {\n const prompt = options.prompt ?? 'Reply with exactly: OK'\n const maxTokens = options.maxTokens ?? 1024\n // Session resolution happens OUTSIDE the recorded region: a rejected pin\n // (\"account #9 not found\") or a refresh failure means no upstream model call\n // was made, and counting it as a request would inflate a ledger whose whole\n // purpose is to reflect what actually consumed quota. The exception is a\n // refresh that reached the token endpoint — that is still recorded below,\n // because it did touch the account's identity.\n const startedAt = Date.now()\n let session: AgyAccountSession | undefined\n try {\n session = await this.getSession(model, options.accountIndex)\n } catch (error) {\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n if (!session) return { ok: false, error: 'No agy account configured — run `dsh-agy login` first.' }\n try {\n const account = session.account.email ?? session.account.id\n const { toAgyRequestBody } = await import('./adapter/translate.ts')\n const { fetchAgyFirstOk } = await import('./oauth/constants.ts')\n const { parseAgySse } = await import('./adapter/parse.ts')\n // Same id in the body and the header, exactly as the generation path does:\n // a diagnostic request should not carry a shape the real one never sends.\n const requestId = generateAntigravityRequestId()\n const body = toAgyRequestBody(\n {\n provider: 'agy',\n model,\n messages: [{ id: 'test-1', role: 'user', content: [{ type: 'text', text: prompt }] }],\n maxTokens,\n } as never,\n {\n projectId: session.account.projectId,\n sessionId: deriveAntigravitySessionId(session.account.email) ?? undefined,\n requestId,\n },\n )\n const headers = {\n authorization: `Bearer ${session.auth.access}`,\n 'content-type': 'application/json',\n accept: 'text/event-stream',\n 'User-Agent': session.impersonation['User-Agent'],\n 'X-Goog-Api-Client': session.impersonation['X-Goog-Api-Client'],\n }\n const routing = { proxyUrl: session.account.proxy, streaming: true }\n const response = await fetchAgyFirstOk(\n '/v1internal:streamGenerateContent?alt=sse',\n {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n },\n accountFetch(routing),\n routing,\n )\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n this.emitUsage({ account, model, source: 'test', ok: false, latencyMs: Date.now() - startedAt })\n return { ok: false, error: `HTTP ${response.status}: ${text.slice(0, 300)}` }\n }\n if (!response.body) {\n this.emitUsage({ account, model, source: 'test', ok: false, latencyMs: Date.now() - startedAt })\n return { ok: false, error: 'no response body' }\n }\n const text: string[] = []\n let usage: UsageRecord['usage']\n let ttftMs: number | undefined\n for await (const chunk of parseAgySse(response.body)) {\n if (chunk.type === 'usage') {\n usage = {\n input: chunk.usage.inputTokens,\n output: chunk.usage.outputTokens,\n cacheRead: chunk.usage.cacheReadTokens ?? 0,\n cacheWrite: chunk.usage.cacheWriteTokens ?? 0,\n }\n } else if (chunk.type === 'text-delta') {\n if (ttftMs === undefined) ttftMs = Date.now() - startedAt\n text.push(chunk.text)\n }\n }\n const ok = text.length > 0\n this.emitUsage({\n account, model, source: 'test', ok,\n ...(usage === undefined ? {} : { usage }),\n ...(ttftMs === undefined ? {} : { ttftMs }),\n latencyMs: Date.now() - startedAt,\n })\n return { ok, text: text.join(''), error: ok ? undefined : 'empty response' }\n } catch (error) {\n this.emitUsage({ source: 'test', ok: false, latencyMs: Date.now() - startedAt })\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n }\n\n /** Export one account as a paste-credential blob (for migration to another host). */\n async exportBlob(index: number): Promise<{ blob?: string; error?: string }> {\n const storage = await this.store.load()\n const account = storage.accounts[index]\n if (!account) return { error: 'account not found' }\n try {\n const auth = await this.accessTokenFor(account)\n if (!auth) return { error: 'refresh failed (revoked?)' }\n const { encodeCredentialBlob } = await import('./oauth/blob.ts')\n const parts = parseRefreshParts(account.refresh)\n return {\n blob: encodeCredentialBlob('agy', {\n access_token: auth.access,\n refresh_token: parts.refreshToken,\n expires_in: Math.max(0, Math.round((auth.expires - Date.now()) / 1000)),\n }),\n }\n } catch (error) {\n return { error: error instanceof Error ? error.message : String(error) }\n }\n }\n\n /** Probe one account: refresh + userinfo; a live credential re-enables the account. */\n private async probeAccount(index: number, account: ManagedAccount): Promise<{ ok: boolean; email?: string; error?: string }> {\n const startedAt = Date.now()\n const key = account.email ?? account.id\n try {\n const auth = await this.accessTokenFor(account)\n if (!auth) {\n this.emitUsage({ account: key, source: 'verify', ok: false, latencyMs: Date.now() - startedAt })\n return { ok: false, error: 'refresh failed (revoked?)' }\n }\n const response = await proxiedFetch('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', {\n headers: { Authorization: `Bearer ${auth.access}` },\n }, account.proxy ? { proxyUrl: account.proxy } : undefined)\n if (!response.ok) {\n this.emitUsage({ account: key, source: 'verify', ok: false, latencyMs: Date.now() - startedAt })\n return { ok: false, error: `userinfo ${response.status}` }\n }\n const info = (await response.json()) as { email?: string }\n // Credentials are live again — clear any auth-failure disable so the\n // account re-enters rotation without a manual re-import.\n await this.store.mutate((s) => {\n const target = s.accounts[index]\n if (target) {\n target.enabled = true\n target.verificationRequired = false\n target.verificationRequiredAt = undefined\n target.verificationRequiredReason = undefined\n target.verificationUrl = undefined\n }\n })\n // No model tokens are billed by a userinfo probe, but the call is a real\n // account-scoped request and belongs in the ledger as such.\n this.emitUsage({ account: info.email ?? key, source: 'verify', ok: true, latencyMs: Date.now() - startedAt })\n return { ok: true, email: info.email }\n } catch (error) {\n this.emitUsage({ account: key, source: 'verify', ok: false, latencyMs: Date.now() - startedAt })\n return { ok: false, error: error instanceof Error ? error.message : String(error) }\n }\n }\n\n /** CLI/web helper: verify an account's credentials (refresh + userinfo). */\n async verifyAccount(index: number): Promise<{ ok: boolean; email?: string; error?: string }> {\n const storage = await this.store.load()\n const account = storage.accounts[index]\n if (!account) return { ok: false, error: 'account not found' }\n return this.probeAccount(index, account)\n }\n\n /**\n * Batch health check over all enabled accounts (or the given indices):\n * refresh + userinfo per account, live credentials re-enable the account.\n * Reports results through onHealthReport when a listener is registered.\n */\n async checkAccounts(indices?: number[]): Promise<AccountHealthResult[]> {\n const storage = await this.store.load()\n const targets = indices !== undefined\n ? indices.filter((index) => storage.accounts[index])\n : storage.accounts.map((_, index) => index).filter((index) => storage.accounts[index]!.enabled !== false)\n\n const results: AccountHealthResult[] = await Promise.all(targets.map(async (index) => {\n const probed = await this.probeAccount(index, storage.accounts[index]!)\n return { index, ...probed }\n }))\n this.onHealthReport?.(results)\n return results\n }\n\n /**\n * Start a background health probe on an interval (disposable stop handle).\n * The timer is unref'd unless told otherwise so harness processes can still\n * exit; the CLI loop mode passes `unref: false`.\n */\n startHealthProbe(intervalMs: number, options: { unref?: boolean } = {}): () => void {\n if (!Number.isFinite(intervalMs) || intervalMs <= 0) return () => {}\n const timer = setInterval(() => {\n void this.checkAccounts().catch(() => {})\n }, intervalMs)\n if (options.unref !== false) timer.unref?.()\n return () => clearInterval(timer)\n }\n}\n\nexport type { Fingerprint }\n","/**\n * Account pool store: an interface plus a JSON-file implementation\n * (proper-lockfile cross-process mutual exclusion, 0600 permissions, versioned\n * migrations, AES-encrypted refresh tokens) and an in-memory fake for tests.\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport lockfile from 'proper-lockfile'\nimport type {\n AccountStorage,\n AccountStorageV1,\n AccountStorageV2,\n AccountStorageV3,\n AccountStorageV4,\n ManagedAccount,\n} from '../types.ts'\nimport { assertOwnerOnly } from './keyring.ts'\nimport type { SecretCodec } from './keyring.ts'\n\nexport const CURRENT_STORAGE_VERSION = 4\nconst ENC_PREFIX = 'enc:v1:'\n\n/** Cross-process file lock adapter (proper-lockfile behind a small seam for tests). */\nexport interface FileLock {\n withLock<T>(file: string, fn: () => Promise<T>): Promise<T>\n}\n\nexport const properFileLock: FileLock = {\n async withLock<T>(file: string, fn: () => Promise<T>): Promise<T> {\n const release = await lockfile.lock(file, {\n stale: 30_000,\n update: 10_000,\n retries: { retries: 10, factor: 1.5, minTimeout: 50, maxTimeout: 2_000 },\n })\n try {\n return await fn()\n } finally {\n await release()\n }\n },\n}\n\n/** No-op lock for tests / single-process use. */\nexport const noopFileLock: FileLock = {\n async withLock<T>(_file: string, fn: () => Promise<T>): Promise<T> {\n return fn()\n },\n}\n\nexport interface AccountStore {\n /** Load the current (migrated) storage document. */\n load(): Promise<AccountStorageV4>\n /** Persist the full document (encrypting secrets). */\n save(storage: AccountStorageV4): Promise<void>\n /** Load-mutate-save atomically under the file lock; returns the mutation result. */\n mutate<T>(fn: (storage: AccountStorageV4) => T | Promise<T>): Promise<T>\n}\n\nexport interface JsonAccountStoreOptions {\n file: string\n codec: SecretCodec\n lock?: FileLock\n}\n\nfunction isEncrypted(value: string): boolean {\n return value.startsWith(ENC_PREFIX)\n}\n\n/** Decrypt one account's refresh/proxy fields when encrypted; plaintext passes through (legacy). */\nfunction decryptAccount(account: ManagedAccount, codec: SecretCodec): ManagedAccount {\n let out = account\n if (isEncrypted(out.refresh)) {\n out = { ...out, refresh: codec.decrypt(out.refresh) }\n }\n if (out.proxy && isEncrypted(out.proxy)) {\n out = { ...out, proxy: codec.decrypt(out.proxy) }\n }\n return out\n}\n\n/** Encrypt one account's refresh/proxy fields (plaintext stays plaintext if no codec change). */\nfunction encryptAccount(account: ManagedAccount, codec: SecretCodec): ManagedAccount {\n let out = account\n if (!isEncrypted(out.refresh)) {\n out = { ...out, refresh: codec.encrypt(out.refresh) }\n }\n if (out.proxy && !isEncrypted(out.proxy)) {\n out = { ...out, proxy: codec.encrypt(out.proxy) }\n }\n return out\n}\n\n/** Mask a proxy URL for display/logs: protocol//host:port (no credentials, no query). */\nexport function maskProxyUrl(proxyUrl?: string): string | null {\n if (!proxyUrl) return null\n try {\n const u = new URL(proxyUrl)\n const port = u.port || (u.protocol === 'https:' ? '443' : u.protocol === 'socks5:' || u.protocol === 'socks5h:' ? '1080' : '8080')\n return `${u.protocol}//${u.hostname}:${port}`\n } catch {\n return null\n }\n}\n\n/** Normalize empty-string proxy to undefined (migration helper). */\nexport function normalizeProxyValue(proxy?: string): string | undefined {\n if (!proxy || !proxy.trim()) return undefined\n return proxy.trim()\n}\n\nexport function ensureAccountIds(storage: AccountStorageV4): { storage: AccountStorageV4; mutated: boolean } {\n let mutated = false\n const seenIds = new Set<string>()\n for (const account of storage.accounts) {\n if (!account.id || seenIds.has(account.id)) {\n account.id = randomUUID()\n mutated = true\n }\n seenIds.add(account.id)\n }\n return { storage, mutated }\n}\n\nexport function decryptStorage(storage: AccountStorageV4, codec: SecretCodec): AccountStorageV4 {\n return { ...storage, accounts: storage.accounts.map((a) => decryptAccount(a, codec)) }\n}\n\nexport function encryptStorage(storage: AccountStorageV4, codec: SecretCodec): AccountStorageV4 {\n return { ...storage, accounts: storage.accounts.map((a) => encryptAccount(a, codec)) }\n}\n// ──── Migrations ────────────────────────────────────────────────────────────\n\nexport function migrateV1ToV2(v1: AccountStorageV1): AccountStorageV2 {\n return {\n version: 2,\n accounts: v1.accounts.map((acc) => ({\n email: acc.email,\n refreshToken: acc.refreshToken,\n projectId: acc.projectId,\n managedProjectId: acc.managedProjectId,\n addedAt: acc.addedAt,\n lastUsed: acc.lastUsed,\n lastSwitchReason: acc.lastSwitchReason,\n rateLimitResetTimes: acc.isRateLimited && acc.rateLimitResetTime\n ? { default: acc.rateLimitResetTime }\n : undefined,\n })),\n activeIndex: v1.activeIndex,\n }\n}\n\nexport function migrateV2ToV3(v2: AccountStorageV2): AccountStorageV3 {\n return {\n version: 3,\n accounts: v2.accounts.map((acc) => ({\n email: acc.email,\n refresh: `${acc.refreshToken}|${acc.projectId ?? ''}|${acc.managedProjectId ?? ''}`,\n projectId: acc.projectId,\n managedProjectId: acc.managedProjectId,\n addedAt: acc.addedAt,\n lastUsed: acc.lastUsed,\n enabled: true,\n lastSwitchReason: acc.lastSwitchReason,\n rateLimitResetTimes: acc.rateLimitResetTimes,\n })),\n activeIndex: v2.activeIndex,\n }\n}\n\nexport function migrateV3ToV4(v3: AccountStorageV3): AccountStorageV4 {\n return { version: 4, accounts: v3.accounts, activeIndex: v3.activeIndex }\n}\n\nexport function migrateStorage(raw: AccountStorage): AccountStorageV4 {\n switch (raw.version) {\n case 1:\n return migrateStorage(migrateV1ToV2(raw))\n case 2:\n return migrateStorage(migrateV2ToV3(raw))\n case 3:\n return migrateStorage(migrateV3ToV4(raw))\n case 4:\n return raw\n default:\n throw new Error(`migrateStorage: unsupported storage version ${(raw as { version: number }).version}`)\n }\n}\n\n// ──── JSON file implementation ──────────────────────────────────────────────\n\nexport class JsonAccountStore implements AccountStore {\n private readonly file: string\n private readonly codec: SecretCodec\n private readonly lock: FileLock\n\n constructor(options: JsonAccountStoreOptions) {\n this.file = options.file\n this.codec = options.codec\n this.lock = options.lock ?? properFileLock\n }\n\n private ensureFile(): void {\n if (existsSync(this.file)) return\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp-init`\n writeFileSync(tmp, JSON.stringify({ version: CURRENT_STORAGE_VERSION, accounts: [], activeIndex: 0 }) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n private readAndMigrateUnlocked(): { storage: AccountStorageV4; mutated: boolean } {\n let text: string\n try {\n text = readFileSync(this.file, 'utf8')\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return { storage: { version: CURRENT_STORAGE_VERSION, accounts: [], activeIndex: 0 }, mutated: false }\n }\n throw error\n }\n assertOwnerOnly(this.file)\n const raw = JSON.parse(text) as AccountStorage\n const migrated = migrateStorage(raw)\n const decrypted = decryptStorage(migrated, this.codec)\n return ensureAccountIds(decrypted)\n }\n\n private writeUnlocked(storage: AccountStorageV4): void {\n const encrypted = encryptStorage(ensureAccountIds(storage).storage, this.codec)\n mkdirSync(dirname(this.file), { recursive: true })\n const tmp = `${this.file}.tmp`\n writeFileSync(tmp, JSON.stringify(encrypted, null, 2) + '\\n', { mode: 0o600 })\n renameSync(tmp, this.file)\n }\n\n /**\n * Materialize and persist generated/repaired UUIDs under the file lock:\n * re-reads the fresh on-disk state under the lock so concurrent mutations\n * are never clobbered by a stale pre-lock snapshot.\n */\n private async materializeIdsWithLock(): Promise<AccountStorageV4> {\n this.ensureFile()\n return this.lock.withLock(this.file, async () => {\n const { storage, mutated } = this.readAndMigrateUnlocked()\n if (mutated) {\n this.writeUnlocked(storage)\n }\n return storage\n })\n }\n\n async load(): Promise<AccountStorageV4> {\n const { storage, mutated } = this.readAndMigrateUnlocked()\n if (mutated) {\n // Missing or duplicate IDs detected: atomically materialize and persist under the lock.\n return this.materializeIdsWithLock()\n }\n return storage\n }\n\n async save(storage: AccountStorageV4): Promise<void> {\n this.ensureFile()\n await this.lock.withLock(this.file, async () => {\n this.writeUnlocked(storage)\n })\n }\n\n async mutate<T>(fn: (storage: AccountStorageV4) => T | Promise<T>): Promise<T> {\n this.ensureFile()\n return this.lock.withLock(this.file, async () => {\n const { storage } = this.readAndMigrateUnlocked()\n const result = await fn(storage)\n this.writeUnlocked(storage)\n return result\n })\n }\n}\n\n// ──── In-memory fake (the second adapter that justifies the seam) ───────────\n\nexport class InMemoryAccountStore implements AccountStore {\n private storage: AccountStorageV4\n private mutateChain: Promise<unknown> = Promise.resolve()\n\n constructor(initial: AccountStorageV4 = { version: CURRENT_STORAGE_VERSION, accounts: [], activeIndex: 0 }) {\n this.storage = ensureAccountIds(initial).storage\n }\n\n async load(): Promise<AccountStorageV4> {\n const { storage, mutated } = ensureAccountIds(this.storage)\n if (mutated) this.storage = storage\n return structuredClone(this.storage)\n }\n\n async save(storage: AccountStorageV4): Promise<void> {\n this.storage = structuredClone(ensureAccountIds(storage).storage)\n }\n\n async mutate<T>(fn: (storage: AccountStorageV4) => T | Promise<T>): Promise<T> {\n const run = async () => {\n const storage = structuredClone(ensureAccountIds(this.storage).storage)\n const result = await fn(storage)\n this.storage = ensureAccountIds(storage).storage\n return result\n }\n const next = this.mutateChain.then(run, run)\n this.mutateChain = next\n return next\n }\n}\n\n// ──── Pool helpers ──────────────────────────────────────────────────────────\n\n/** Keep one account per email (case-insensitive), first occurrence wins. */\nexport function deduplicateAccountsByEmail(accounts: ManagedAccount[]): ManagedAccount[] {\n const seen = new Set<string>()\n const result: ManagedAccount[] = []\n for (const account of accounts) {\n const key = account.email?.toLowerCase()\n if (key && seen.has(key)) continue\n if (key) seen.add(key)\n result.push(account)\n }\n return result\n}\n\n/** Pick the active account, falling back to the first enabled one. */\nexport function resolveActiveAccount(\n storage: AccountStorageV4,\n): { account: ManagedAccount; index: number } | undefined {\n const accounts = storage.accounts\n if (accounts.length === 0) return undefined\n const active = accounts[storage.activeIndex]\n if (active && active.enabled !== false) return { account: active, index: storage.activeIndex }\n const fallbackIndex = accounts.findIndex((a) => a.enabled !== false)\n if (fallbackIndex === -1) return undefined\n return { account: accounts[fallbackIndex]!, index: fallbackIndex }\n}\n"],"mappings":"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"}