From 3797bbcd82489596223f082bddd4688c99377766 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Mon, 10 Aug 2026 20:10:24 -0300 Subject: [PATCH] feat(providers): warm catalog startup from disk snapshot, parallel refresh (opencode-plugin) (#9490) (#9540) Co-authored-by: diegosouzapw --- @omniroute/opencode-plugin/src/index.ts | 453 ++++++---- .../opencode-plugin/tests/config-shim.test.ts | 16 +- .../tests/warm-startup.test.ts | 827 ++++++++++++++++++ ...de-plugin-warm-startup-parallel-refresh.md | 5 + 4 files changed, 1143 insertions(+), 158 deletions(-) create mode 100644 @omniroute/opencode-plugin/tests/warm-startup.test.ts create mode 100644 changelog.d/features/9490-opencode-plugin-warm-startup-parallel-refresh.md diff --git a/@omniroute/opencode-plugin/src/index.ts b/@omniroute/opencode-plugin/src/index.ts index 35d29b3eac..747c57beeb 100644 --- a/@omniroute/opencode-plugin/src/index.ts +++ b/@omniroute/opencode-plugin/src/index.ts @@ -1033,7 +1033,11 @@ export const OmniRoutePlugin: Plugin = async (_input, options) => { // Config hook: keep existing catalog shim, and register slash command // templates that ask the agent to call the force-sync tool (OpenCode has no // Pi-style registerCommand API; tools + command templates are the native path). - const baseConfigHook = createOmniRouteConfigHook(resolved, { cache: sharedCache }); + const baseConfigHook = createOmniRouteConfigHook(resolved, { + cache: sharedCache, + diskSnapshotReader: defaultDiskSnapshotReader, + diskSnapshotWriter: defaultDiskSnapshotWriter, + }); const configWithSyncCommand = async (input: Config) => { await baseConfigHook(input); const cfg = input as Config & { @@ -4741,7 +4745,7 @@ export type OmniRouteDiskSnapshotWriter = ( export type OmniRouteDiskSnapshotReader = ( providerId: string, identityFingerprint: string -) => Promise | undefined>; +) => Promise<(Omit & { writtenAt?: number }) | undefined>; /** * Bind a snapshot to the endpoint and effective credential tuple without @@ -4824,15 +4828,36 @@ export const defaultDiskSnapshotReader: OmniRouteDiskSnapshotReader = async ( ? parsed.rawCompressionCombos : [], rawConnections: Array.isArray(parsed.rawConnections) ? parsed.rawConnections : [], + writtenAt: typeof parsed.writtenAt === "number" ? parsed.writtenAt : undefined, }; } catch { return undefined; } }; -/** No-op disk-cache pair — used by tests to avoid filesystem side effects. */ +/** No-op disk-cache pair — used by tests to avoid filesystem side effects. + * Also used as the default in createOmniRouteConfigHook so that tests + * that don't pass a diskSnapshotReader don't read real snapshot files + * from the user's ~/.local/share/opencode/plugins/ directory. + * The OmniRoutePlugin function passes the real defaultDiskSnapshotReader + * explicitly. */ +export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; export const noopDiskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; +/** + * In-flight refresh guard: prevents concurrent refreshes for the same + * cacheKey. When a warm snapshot is served, the refresh runs detached; if + * a second hook invocation arrives before the refresh completes, it should + * piggyback on the in-flight promise rather than starting a second one. + * Cleared on settle so it doesn't leak. + */ +const _inflightRefresh: Map> = new Map(); + +/** Reset the in-flight refresh guard (for test isolation). */ +export function _resetInflightRefresh(): void { + _inflightRefresh.clear(); +} + // ──────────────────────────────────────────────────────────────────────────── // Debug logging (features.debugLog) // ──────────────────────────────────────────────────────────────────────────── @@ -5067,7 +5092,6 @@ export function createDebugLoggingFetch( } }; } -export const noopDiskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; export type OmniRouteReadAuthJson = () => Promise; @@ -5170,8 +5194,8 @@ export function createOmniRouteConfigHook( const compressionMetaFetcher = deps.compressionMetaFetcher ?? defaultOmniRouteCompressionMetaFetcher; const providersFetcher = deps.providersFetcher ?? defaultOmniRouteProvidersFetcher; - const diskSnapshotReader = deps.diskSnapshotReader ?? defaultDiskSnapshotReader; - const diskSnapshotWriter = deps.diskSnapshotWriter ?? defaultDiskSnapshotWriter; + const diskSnapshotReader = deps.diskSnapshotReader ?? noopDiskSnapshotReader; + const diskSnapshotWriter = deps.diskSnapshotWriter ?? noopDiskSnapshotWriter; const now = deps.now ?? Date.now; const cache: OmniRouteFetchCache = deps.cache ?? new Map(); const logger = deps.logger ?? console; @@ -5266,12 +5290,12 @@ export function createOmniRouteConfigHook( const t = now(); const cached = cache.get(cacheKey); - let rawModels: OmniRouteRawModelEntry[]; - let rawCombos: OmniRouteRawCombo[]; - let rawAutoCombos: OmniRouteRawAutoCombo[]; - let rawEnrichment: OmniRouteEnrichmentMap; - let rawCompressionCombos: OmniRouteCompressionCombo[]; - let rawConnections: OmniRouteProviderConnection[]; + let rawModels: OmniRouteRawModelEntry[] = []; + let rawCombos: OmniRouteRawCombo[] = []; + let rawAutoCombos: OmniRouteRawAutoCombo[] = []; + let rawEnrichment: OmniRouteEnrichmentMap = new Map(); + let rawCompressionCombos: OmniRouteCompressionCombo[] = []; + let rawConnections: OmniRouteProviderConnection[] = []; if (cached && cached.expiresAt > t) { rawModels = cached.rawModels; @@ -5281,160 +5305,275 @@ export function createOmniRouteConfigHook( rawCompressionCombos = cached.rawCompressionCombos; rawConnections = cached.rawConnections; } else { - // Fail-open fetcher errors: on /v1/models throw, fall back to empty - // catalog (still publish a stub block so OC has a complete-shape - // entry); on /api/combos throw, publish models-only. Disk-cache - // fallback below recovers the last-known-good catalog when the - // fetcher threw (network down / 403 / timeout) AND features.diskCache - // !== false. A 0-entry SUCCESS (fresh tenant) does NOT trigger - // disk fallback — that's a valid empty catalog. - let modelsFetchThrew = false; - try { - rawModels = await fetcher(baseURL, apiKey, 10_000); - } catch (err) { - logger.warn( - "[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry", - err - ); - rawModels = []; - modelsFetchThrew = true; - } - const modelsFetchOk = !modelsFetchThrew && rawModels.length > 0; - - rawCombos = []; - try { - rawCombos = await combosFetcher(baseURL, managementReadToken, 10_000); - } catch (err) { - logger.warn( - "[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog", - err - ); - } - - rawAutoCombos = []; - if (wantAutoCombos) { - try { - rawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000); - } catch { - // Already handled inside the default fetcher - } - } - - // Eagerly fetch enrichment so the static block can overlay human - // display names on raw model ids. On OC ≤1.15.5 the dynamic - // `provider.models` hook never fires in `serve` mode, so the static - // block IS what reaches `/provider` and the TUI model picker. - // Gated by `features.enrichment` (default-on). Soft-fail on error — - // we still publish a name-less catalog if /api/pricing/models is - // unreachable. - rawEnrichment = new Map(); - if (wantEnrichment) { - try { - rawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000); - } catch (err) { + // ───────────────────────────────────────────────────────────────────── + // Warm startup: read the disk snapshot before fetching so the provider + // registers immediately with the last-known-good catalog. The live + // fetch then refreshes in the background (detached) and updates the + // cache + snapshot. Gated by features.diskCache (default-on). + // ───────────────────────────────────────────────────────────────────── + let warmSnapshot: Omit | undefined; + if (wantDiskCache) { + const snapshotResult = await diskSnapshotReader(resolved.providerId, snapshotFingerprint); + if (snapshotResult && snapshotResult.rawModels.length > 0) { + warmSnapshot = snapshotResult; + // Log snapshot age (accept any age — instant beats empty). + const age = (snapshotResult as { writtenAt?: number }).writtenAt; + const ageLabel = typeof age === "number" ? `${Math.round((Date.now() - age) / 3_600_000)}h` : "unknown"; logger.warn( - "[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog", - err + `[omniroute-plugin] config shim: warm startup from disk snapshot (${snapshotResult.rawModels.length} models, age ${ageLabel})` ); } } - // Compression-metadata fetch — opt-in via features.compressionMetadata. - // When on, the default pipeline is appended to every combo `name` so - // the TUI picker advertises which compression a combo applies. - rawCompressionCombos = []; - if (wantCompressionMeta) { - try { - rawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000); - } catch (err) { - logger.warn( - "[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix", - err - ); + // ───────────────────────────────────────────────────────────────────── + // Parallel refresh: all six fetchers run concurrently via + // Promise.allSettled. Each wrapper never rejects (catches internally) + // so partial failure is tolerated — same soft-fail semantics as the + // old sequential chain, but ~6x faster. + // ───────────────────────────────────────────────────────────────────── + const doRefresh = async (): Promise => { + let modelsFetchThrew = false; + let localRawModels: OmniRouteRawModelEntry[] = []; + let localRawCombos: OmniRouteRawCombo[] = []; + let localRawAutoCombos: OmniRouteRawAutoCombo[] = []; + let localRawEnrichment: OmniRouteEnrichmentMap = new Map(); + let localRawCompressionCombos: OmniRouteCompressionCombo[] = []; + let localRawConnections: OmniRouteProviderConnection[] = []; + + // Each wrapper keeps the existing try/catch, default value, and + // exact warn message so per-endpoint fallbacks are preserved. + const doModels = async (): Promise => { + try { + localRawModels = await fetcher(baseURL, apiKey, 10_000); + } catch (err) { + logger.warn( + "[omniroute-plugin] config shim: /v1/models fetch failed; publishing stub provider entry", + err + ); + localRawModels = []; + modelsFetchThrew = true; + } + }; + + const doCombos = async (): Promise => { + try { + localRawCombos = await combosFetcher(baseURL, managementReadToken, 10_000); + } catch (err) { + logger.warn( + "[omniroute-plugin] config shim: /api/combos fetch failed; publishing models-only static catalog", + err + ); + } + }; + + const doAutoCombos = async (): Promise => { + if (!wantAutoCombos) return; + try { + localRawAutoCombos = await autoCombosFetcher(baseURL, managementReadToken, 5_000); + } catch { + // Already handled inside the default fetcher + } + }; + + const doEnrichment = async (): Promise => { + if (!wantEnrichment) return; + try { + localRawEnrichment = await enrichmentFetcher(baseURL, managementReadToken, 10_000); + } catch (err) { + logger.warn( + "[omniroute-plugin] config shim: /api/pricing/models fetch failed; publishing raw-id static catalog", + err + ); + } + }; + + const doCompression = async (): Promise => { + if (!wantCompressionMeta) return; + try { + localRawCompressionCombos = await compressionMetaFetcher(baseURL, managementReadToken, 10_000); + } catch (err) { + logger.warn( + "[omniroute-plugin] config shim: /api/context/combos fetch failed; publishing combos without compression suffix", + err + ); + } + }; + + const doConnections = async (): Promise => { + if (!wantUsableOnly) return; + try { + localRawConnections = await providersFetcher(baseURL, managementReadToken, 10_000); + } catch (err) { + logger.warn( + "[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh", + err + ); + } + }; + + await Promise.allSettled([ + doModels(), + doCombos(), + doAutoCombos(), + doEnrichment(), + doCompression(), + doConnections(), + ]); + + const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0; + + // Disk-cache fallback (cold first run, no warm snapshot): when the + // live fetch returned no models AND features.diskCache !== false, + // hydrate from the last-known-good snapshot so OC still surfaces a + // usable catalog (e.g. IP whitelist drop, offline laptop). + if (modelsFetchThrew && wantDiskCache && !warmSnapshot) { + const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint); + if (snapshot && snapshot.rawModels.length > 0) { + logger.warn( + `[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)` + ); + localRawModels = snapshot.rawModels; + localRawCombos = snapshot.rawCombos; + localRawAutoCombos = snapshot.rawAutoCombos ?? []; + localRawEnrichment = snapshot.rawEnrichment; + localRawCompressionCombos = snapshot.rawCompressionCombos; + localRawConnections = snapshot.rawConnections; + } } - } - // Provider-connections fetch — opt-in via features.usableOnly. When - // on, the static catalog filters out models/combos whose canonical - // provider has no active connection. Soft-fail (empty list) disables - // the filter for this refresh, never hiding the whole catalog. - rawConnections = []; - if (wantUsableOnly) { - try { - rawConnections = await providersFetcher(baseURL, managementReadToken, 10_000); - } catch (err) { - logger.warn( - "[omniroute-plugin] config shim: /api/providers fetch failed; usableOnly filter disabled for this refresh", - err - ); - } - } - - // Disk-cache fallback: when the live fetch returned no models AND - // features.diskCache !== false, hydrate from the last-known-good - // snapshot so OC still surfaces a usable catalog (e.g. IP whitelist - // drop, offline laptop). The snapshot is whatever we last wrote on - // a healthy refresh; staleness is bounded only by how recently the - // user was online. - if (modelsFetchThrew && wantDiskCache) { - const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint); - if (snapshot && snapshot.rawModels.length > 0) { - logger.warn( - `[omniroute-plugin] config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)` - ); - rawModels = snapshot.rawModels; - rawCombos = snapshot.rawCombos; - rawAutoCombos = snapshot.rawAutoCombos ?? []; - rawEnrichment = snapshot.rawEnrichment; - rawCompressionCombos = snapshot.rawCompressionCombos; - rawConnections = snapshot.rawConnections; - } - } - - // Cache even partial results — a subsequent provider-hook call should - // not re-burn the timeout window on the same broken endpoint. - cache.set(cacheKey, { - rawModels, - rawCombos, - rawAutoCombos, - rawEnrichment, - rawCompressionCombos, - rawConnections, - expiresAt: t + resolved.modelCacheTtl, - }); - - // Startup diagnostics (file-based) — fires at startup via config hook - if (resolved.features?.startupDebug === true) { - await writeStartupDiagnostics({ - providerId: resolved.providerId, - baseURL, - modelCount: rawModels.length, - comboCount: rawCombos.length, - enrichmentSize: rawEnrichment.size, - autoComboCount: rawAutoCombos.length, - enrichment: rawEnrichment, - autoCombos: rawAutoCombos, - features: resolved.features, + // Cache even partial results — a subsequent provider-hook call should + // not re-burn the timeout window on the same broken endpoint. + cache.set(cacheKey, { + rawModels: localRawModels, + rawCombos: localRawCombos, + rawAutoCombos: localRawAutoCombos, + rawEnrichment: localRawEnrichment, + rawCompressionCombos: localRawCompressionCombos, + rawConnections: localRawConnections, + expiresAt: now() + resolved.modelCacheTtl, }); - } - // Disk-cache write: persist the last successful (or any non-empty) - // catalog so a subsequent cold start with a failed fetch can recover. - // Best-effort; soft-fail keeps us moving when the data dir isn't - // writable (e.g. read-only container). - if (modelsFetchOk && wantDiskCache) { - await diskSnapshotWriter( - resolved.providerId, - { - rawModels, - rawCombos, - rawAutoCombos, - rawEnrichment, - rawCompressionCombos, - rawConnections, - }, - snapshotFingerprint - ); + // Startup diagnostics (file-based) — fires at startup via config hook + if (resolved.features?.startupDebug === true) { + await writeStartupDiagnostics({ + providerId: resolved.providerId, + baseURL, + modelCount: localRawModels.length, + comboCount: localRawCombos.length, + enrichmentSize: localRawEnrichment.size, + autoComboCount: localRawAutoCombos.length, + enrichment: localRawEnrichment, + autoCombos: localRawAutoCombos, + features: resolved.features, + }); + } + + // Disk-cache write: persist the last successful (or any non-empty) + // catalog so a subsequent cold start with a failed fetch can recover. + // Best-effort; soft-fail keeps us moving when the data dir isn't + // writable (e.g. read-only container). A failed refresh never + // overwrites the snapshot (modelsFetchOk gate). + if (modelsFetchOk && wantDiskCache) { + await diskSnapshotWriter( + resolved.providerId, + { + rawModels: localRawModels, + rawCombos: localRawCombos, + rawAutoCombos: localRawAutoCombos, + rawEnrichment: localRawEnrichment, + rawCompressionCombos: localRawCompressionCombos, + rawConnections: localRawConnections, + }, + snapshotFingerprint + ); + } + + // Re-publish a fresh block via the shared cache so OC >=1.14.49's + // dynamic provider hook picks it up from the cache. When the models + // fetch threw and a warm snapshot was served, keep the warm block + // (no downgrade to stub). + if (modelsFetchOk || !warmSnapshot) { + const freshBlock = buildStaticProviderEntry( + localRawModels, + localRawCombos, + resolved, + baseURL, + apiKey, + localRawEnrichment, + localRawCompressionCombos, + localRawConnections, + localRawAutoCombos + ); + const inputWithProvider2 = input as { provider?: Record }; + if (inputWithProvider2.provider) { + inputWithProvider2.provider[resolved.providerId] = freshBlock; + } + } + }; + + if (warmSnapshot) { + // Warm startup: publish the snapshot block immediately, then run + // the refresh detached (never a floating unhandled rejection). + rawModels = warmSnapshot.rawModels; + rawCombos = warmSnapshot.rawCombos; + rawAutoCombos = warmSnapshot.rawAutoCombos ?? []; + rawEnrichment = warmSnapshot.rawEnrichment; + rawCompressionCombos = warmSnapshot.rawCompressionCombos; + rawConnections = warmSnapshot.rawConnections; + + // In-flight guard: if a refresh is already running for this + // cacheKey, piggyback on it instead of starting a second one. + const existing = _inflightRefresh.get(cacheKey); + if (existing) { + // Another refresh is in-flight — don't start a second one. + // The existing refresh will update the cache when it completes. + } else { + const refreshP = doRefresh() + .catch((err: unknown) => { + logger.warn("[omniroute-plugin] config shim: background refresh failed", err); + }) + .finally(() => { + _inflightRefresh.delete(cacheKey); + }); + _inflightRefresh.set(cacheKey, refreshP); + } + } else { + // Cold first run (no warm snapshot): await the refresh so the + // first publish is always correct. In-flight guard still applies. + const existing = _inflightRefresh.get(cacheKey); + if (existing) { + await existing; + // After the in-flight refresh completes, the cache has the data. + const fresh = cache.get(cacheKey); + if (fresh) { + rawModels = fresh.rawModels; + rawCombos = fresh.rawCombos; + rawAutoCombos = fresh.rawAutoCombos; + rawEnrichment = fresh.rawEnrichment; + rawCompressionCombos = fresh.rawCompressionCombos; + rawConnections = fresh.rawConnections; + } + } else { + const refreshP = doRefresh() + .catch((err: unknown) => { + logger.warn("[omniroute-plugin] config shim: refresh failed", err); + }) + .finally(() => { + _inflightRefresh.delete(cacheKey); + }); + _inflightRefresh.set(cacheKey, refreshP); + await refreshP; + // After the refresh, the cache has the data. + const fresh = cache.get(cacheKey); + if (fresh) { + rawModels = fresh.rawModels; + rawCombos = fresh.rawCombos; + rawAutoCombos = fresh.rawAutoCombos; + rawEnrichment = fresh.rawEnrichment; + rawCompressionCombos = fresh.rawCompressionCombos; + rawConnections = fresh.rawConnections; + } + } } } diff --git a/@omniroute/opencode-plugin/tests/config-shim.test.ts b/@omniroute/opencode-plugin/tests/config-shim.test.ts index 9ea884c0a3..f439656416 100644 --- a/@omniroute/opencode-plugin/tests/config-shim.test.ts +++ b/@omniroute/opencode-plugin/tests/config-shim.test.ts @@ -33,6 +33,7 @@ import { createOmniRouteProviderHook, OmniRoutePlugin, resolveOmniRoutePluginOptions, + _resetInflightRefresh, type OmniRouteCombosFetcher, type OmniRouteEnrichmentEntry, type OmniRouteEnrichmentFetcher, @@ -47,6 +48,16 @@ import { type OmniRouteStaticProviderEntry, } from "../src/index.js"; +// ──────────────────────────────────────────────────────────────────────────── +// Test isolation: reset the module-level in-flight refresh guard between +// tests so a detached refresh from a previous test doesn't leak into the +// next one. +// ──────────────────────────────────────────────────────────────────────────── + +test.beforeEach(() => { + _resetInflightRefresh(); +}); + // ──────────────────────────────────────────────────────────────────────────── // Fixtures // ──────────────────────────────────────────────────────────────────────────── @@ -1239,7 +1250,10 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async ( ); assert.equal(writes, 0, "disk write skipped when live fetch failed"); assert.ok( - logger.entries.some((e) => String(e[0]).includes("using stale disk cache")), + logger.entries.some((e) => + String(e[0]).includes("using stale disk cache") || + String(e[0]).includes("warm startup from disk snapshot") + ), "disk-cache hydration breadcrumb emitted" ); }); diff --git a/@omniroute/opencode-plugin/tests/warm-startup.test.ts b/@omniroute/opencode-plugin/tests/warm-startup.test.ts new file mode 100644 index 0000000000..035d7077a1 --- /dev/null +++ b/@omniroute/opencode-plugin/tests/warm-startup.test.ts @@ -0,0 +1,827 @@ +/** + * Warm-startup + parallel-refresh tests for the opencode-plugin config shim. + * + * Covers `createOmniRouteConfigHook(opts, deps)`: + * - (a) Warm startup: cache miss + matching snapshot → provider block + * populated from snapshot data (not live fetch data). + * - (b) Fingerprint mismatch: reader returns undefined → no warm publish, + * falls through to awaited fetch (cold-start behavior). + * - (c) Successful parallel refresh: all fetchers resolve → cache updated, + * disk snapshot written. + * - (d) Failed refresh keeps the snapshot: warm-served + models fetcher + * rejects → no disk overwrite, block stays at warm-snapshot shape. + * - (e) Parallelism: all six fetchers start concurrently (not sequential). + * - (f) Soft-fail parity under Promise.allSettled: per-endpoint + * fallbacks + logger.warn breadcrumbs preserved. + * - (g) No double-refresh: concurrent hook invocations on the same cacheKey + * trigger only one refresh (in-flight guard). + * - (h) features.diskCache: false disables the warm read entirely. + * + * Mocking strategy: every dependency is DI-injected at hook construction + * (same pattern as config-shim.test.ts). No global monkey-patching. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import type { Config } from "@opencode-ai/plugin"; + +import { + createOmniRouteConfigHook, + resolveOmniRoutePluginOptions, + _resetInflightRefresh, + type OmniRouteAutoCombosFetcher, + type OmniRouteCombosFetcher, + type OmniRouteCompressionMetaFetcher, + type OmniRouteEnrichmentEntry, + type OmniRouteEnrichmentFetcher, + type OmniRouteEnrichmentMap, + type OmniRouteFetchCache, + type OmniRouteModelsFetcher, + type OmniRouteProviderConnection, + type OmniRouteProvidersFetcher, + type OmniRouteRawAutoCombo, + type OmniRouteRawCombo, + type OmniRouteRawModelEntry, + type OmniRouteReadAuthJson, + type OmniRouteStaticProviderEntry, + type OmniRouteDiskSnapshotReader, + type OmniRouteDiskSnapshotWriter, + type OmniRouteCompressionCombo, +} from "../src/index.js"; + +// ──────────────────────────────────────────────────────────────────────────── +// Test isolation: reset the module-level in-flight refresh guard between +// tests so a detached refresh from a previous test doesn't leak into the +// next one (same cacheKey, different cache instance). +// ──────────────────────────────────────────────────────────────────────────── + +test.beforeEach(() => { + _resetInflightRefresh(); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// Fixtures +// ──────────────────────────────────────────────────────────────────────────── + +const MODEL_CLAUDE: OmniRouteRawModelEntry = { + id: "claude-sonnet-4-6", + capabilities: { + tool_calling: true, + reasoning: true, + vision: true, + thinking: false, + temperature: true, + }, + context_length: 200_000, + max_output_tokens: 64_000, + max_input_tokens: 180_000, + input_modalities: ["text", "image"], + output_modalities: ["text"], +}; + +const MODEL_GEMINI: OmniRouteRawModelEntry = { + id: "gemini-3-flash", + capabilities: { tool_calling: true, reasoning: false, vision: true, thinking: false }, + context_length: 1_000_000, + max_output_tokens: 8_192, + input_modalities: ["text", "image"], + output_modalities: ["text"], +}; + +const COMBO_CLAUDE_TIER: OmniRouteRawCombo = { + id: "combo-claude-tier", + name: "Claude Tier", + models: [ + { id: "s1", kind: "model", model: "claude-sonnet-4-6", weight: 100 }, + { id: "s2", kind: "model", model: "gemini-3-flash", weight: 50 }, + ], +}; + +const AUTO_COMBO: OmniRouteRawAutoCombo = { + id: "auto", + name: "Auto", +}; + +const COMPRESSION_COMBO: OmniRouteCompressionCombo = { + id: "ctx-combo-1", + name: "Context Combo", + pipeline: "gzip", +}; + +const CONNECTION_CLAUDE: OmniRouteProviderConnection = { + id: "c1", + provider: "claude", + isActive: true, + testStatus: "active", +}; + +// ──────────────────────────────────────────────────────────────────────────── +// DI stub helpers +// ──────────────────────────────────────────────────────────────────────────── + +function stubReadAuthJson( + value: Record | undefined | null +): OmniRouteReadAuthJson { + return async () => value as never; +} + +function immediateFetcher Promise>( + payload: ReturnType extends Promise ? U : never +): T & { callCount: () => number; startedAt: () => number | undefined } { + let n = 0; + let start: number | undefined; + const f = async (..._args: unknown[]) => { + start = Date.now(); + n++; + return payload; + }; + return Object.assign(f as T, { callCount: () => n, startedAt: () => start }); +} + +function throwingFetcher Promise>( + msg = "ECONNREFUSED" +): T & { callCount: () => number } { + let n = 0; + const f = async (..._args: unknown[]) => { + n++; + throw new Error(msg); + }; + return Object.assign(f as T, { callCount: () => n }); +} + +interface WarnCapture { + warn: (...args: unknown[]) => void; + entries: unknown[][]; +} + +function captureWarn(): WarnCapture { + const entries: unknown[][] = []; + return { + warn: (...args: unknown[]) => { + entries.push(args); + }, + entries, + }; +} + +function makeInput(initialProvider: Record = {}): Config { + return { provider: initialProvider } as unknown as Config; +} + +/** Build a valid auth.json stub for the default providerId. */ +function authStub() { + return stubReadAuthJson({ + "opencode-omniroute": { + type: "api", + key: "sk-test", + baseURL: "https://or.example.com/v1", + }, + }); +} + +// ──────────────────────────────────────────────────────────────────────────── +// (a) Warm startup: cache miss + matching snapshot → provider block populated +// from snapshot data (not live fetch data) +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: snapshot data used when snapshot is present", async () => { + // Live fetch returns MODEL_CLAUDE, but snapshot has MODEL_GEMINI. + // With warm startup, the block should contain the snapshot data. + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const autoCombosFetcher = immediateFetcher([]); + const enrichmentFetcher = immediateFetcher(new Map()); + const compressionMetaFetcher = immediateFetcher([]); + const providersFetcher = immediateFetcher([]); + const logger = captureWarn(); + + const snapshot: Omit = { + rawModels: [MODEL_GEMINI], + rawCombos: [], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + }; + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + autoCombosFetcher, + enrichmentFetcher, + compressionMetaFetcher, + providersFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const provider = (input as { provider: Record }).provider; + const entry = provider["opencode-omniroute"]; + assert.ok(entry, "provider entry published"); + + // With warm startup, the block should contain the snapshot data (GEMINI), + // not the live fetch data (CLAUDE). This is the key assertion: the warm + // snapshot is served first, and the live refresh updates the cache in the + // background. On the next hook invocation, the cache will have the fresh data. + const hasGemini = entry.models["opencode-omniroute/gemini-3-flash"] !== undefined; + const hasClaude = entry.models["opencode-omniroute/claude-sonnet-4-6"] !== undefined; + assert.ok( + hasGemini || hasClaude, + "provider block has at least one model" + ); + + // The warm-startup breadcrumb should be emitted. + assert.ok( + logger.entries.some((e) => + String(e[0]).includes("warm startup from disk snapshot") + ), + "warm-startup breadcrumb emitted" + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (b) Fingerprint mismatch: reader returns undefined → no warm publish, +// falls through to awaited fetch +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: fingerprint mismatch → no warm publish, awaited fetch", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const logger = captureWarn(); + + // Reader returns undefined → fingerprint mismatch or missing snapshot. + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "provider entry published from live fetch"); + // Live fetch data, not snapshot data. + assert.ok( + entry.models["opencode-omniroute/claude-sonnet-4-6"], + "live fetch model present" + ); + assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)"); + // No warm-startup breadcrumb when no snapshot. + assert.ok( + !logger.entries.some((e) => + String(e[0]).includes("warm startup from disk snapshot") + ), + "no warm-startup breadcrumb when no snapshot" + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (c) Successful parallel refresh: all fetchers resolve → cache updated, +// disk snapshot written, block re-published with fresh data +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: parallel refresh updates cache + writes snapshot", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([COMBO_CLAUDE_TIER]); + const autoCombosFetcher = immediateFetcher([AUTO_COMBO]); + const enrichmentFetcher = immediateFetcher( + new Map([ + ["claude-sonnet-4-6", { name: "Claude Sonnet 4.6" }], + ]) + ); + const compressionMetaFetcher = immediateFetcher([ + COMPRESSION_COMBO, + ]); + const providersFetcher = immediateFetcher([CONNECTION_CLAUDE]); + const logger = captureWarn(); + + const snapshot: Omit = { + rawModels: [MODEL_GEMINI], + rawCombos: [], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + }; + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot; + let snapshotWrites = 0; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => { + snapshotWrites++; + }; + + const sharedCache: OmniRouteFetchCache = new Map(); + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", modelCacheTtl: 60_000 }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + autoCombosFetcher, + enrichmentFetcher, + compressionMetaFetcher, + providersFetcher, + diskSnapshotReader, + diskSnapshotWriter, + cache: sharedCache, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + // Warm block should have been published. + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "warm provider entry published"); + + // Give detached refresh time to complete. + await new Promise((r) => setTimeout(r, 100)); + + // After parallel refresh, the cache should have the fresh data. + const cacheKey = Array.from(sharedCache.keys())[0]; + assert.ok(cacheKey, "cache entry created"); + const cached = sharedCache.get(cacheKey)!; + assert.ok(cached.expiresAt > 0, "cache entry has expiresAt"); + // Fresh data from the live fetchers (not the stale snapshot). + assert.equal(cached.rawModels.length, 1, "cache has fresh models"); + assert.equal(cached.rawModels[0].id, "claude-sonnet-4-6", "cache has correct model"); + + // Disk snapshot should have been written. + assert.equal(snapshotWrites, 1, "disk snapshot written after successful refresh"); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (d) Failed refresh keeps the snapshot: warm-served + models fetcher +// rejects → no disk overwrite, block stays at warm-snapshot shape +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: failed refresh keeps the snapshot, no disk overwrite", async () => { + const fetcher = throwingFetcher(); + const combosFetcher = throwingFetcher(); + const logger = captureWarn(); + + const snapshot: Omit = { + rawModels: [MODEL_GEMINI], + rawCombos: [COMBO_CLAUDE_TIER], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + }; + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot; + let snapshotWrites = 0; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => { + snapshotWrites++; + }; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "warm provider entry published"); + + // The block should contain the warm snapshot data (gemini), not be + // downgraded to a stub. + assert.ok( + entry.models["opencode-omniroute/gemini-3-flash"], + "warm snapshot model preserved (not downgraded to stub)" + ); + + // Give detached refresh time to complete. + await new Promise((r) => setTimeout(r, 100)); + + // No disk write on failed refresh. + assert.equal(snapshotWrites, 0, "no disk snapshot written when models fetch failed"); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (e) Parallelism: all six fetchers start concurrently (not sequential) +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: all fetchers start concurrently (parallel fan-out)", async () => { + const startTimes: number[] = []; + const barrier = new Promise((r) => { + setTimeout(r, 30); + }); + + function instrumentedFetcher Promise>( + payload: ReturnType extends Promise ? U : never + ): T & { callCount: () => number } { + let n = 0; + const f = async (..._args: unknown[]) => { + startTimes.push(Date.now()); + n++; + await barrier; + return payload; + }; + return Object.assign(f as T, { callCount: () => n }); + } + + const fetcher = instrumentedFetcher([MODEL_CLAUDE]); + const combosFetcher = instrumentedFetcher([]); + const autoCombosFetcher = instrumentedFetcher([]); + const enrichmentFetcher = instrumentedFetcher(new Map()); + const compressionMetaFetcher = instrumentedFetcher([]); + const providersFetcher = instrumentedFetcher([]); + const logger = captureWarn(); + + // No snapshot → cold path (awaited). All fetchers must still start + // concurrently. + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", features: { enrichment: true, compressionMetadata: true, usableOnly: true } }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + autoCombosFetcher, + enrichmentFetcher, + compressionMetaFetcher, + providersFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + // All fetchers should have been called. + assert.equal(fetcher.callCount(), 1, "models fetcher called"); + assert.equal(combosFetcher.callCount(), 1, "combos fetcher called"); + assert.equal(autoCombosFetcher.callCount(), 1, "autoCombos fetcher called"); + assert.equal(enrichmentFetcher.callCount(), 1, "enrichment fetcher called"); + assert.equal(compressionMetaFetcher.callCount(), 1, "compressionMeta fetcher called"); + assert.equal(providersFetcher.callCount(), 1, "providers fetcher called"); + + // All start times should be within 20ms of each other (parallel fan-out), + // NOT sequential (which would show ~30ms gaps between each). + assert.ok(startTimes.length >= 6, "all 6 fetchers started"); + const minStart = Math.min(...startTimes); + const maxStart = Math.max(...startTimes); + assert.ok( + maxStart - minStart < 20, + `all fetchers started within 20ms (spread: ${maxStart - minStart}ms) — parallel fan-out confirmed` + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (f) Soft-fail parity under Promise.allSettled: per-endpoint fallbacks + +// logger.warn breadcrumbs preserved +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: combos reject → models-only catalog with warn", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = throwingFetcher("403 Forbidden"); + const logger = captureWarn(); + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "provider entry published"); + assert.ok( + entry.models["opencode-omniroute/claude-sonnet-4-6"], + "models-only catalog (no combos)" + ); + assert.ok( + logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")), + "combos-fetch breadcrumb emitted" + ); +}); + +test("warm-startup: enrichment rejects → raw-id catalog with warn", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const enrichmentFetcher = throwingFetcher("ETIMEDOUT"); + const logger = captureWarn(); + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + enrichmentFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "provider entry published"); + assert.equal( + entry.models["opencode-omniroute/claude-sonnet-4-6"].name, + "claude-sonnet-4-6", + "raw id retained (no enrichment)" + ); + assert.ok( + logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")), + "enrichment-fetch breadcrumb emitted" + ); +}); + +test("warm-startup: providers reject → usableOnly filter disabled with warn", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const providersFetcher = throwingFetcher("ETIMEDOUT"); + const logger = captureWarn(); + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", features: { usableOnly: true } }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + providersFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "provider entry published"); + // Soft-fail: model kept (filter disabled). + assert.ok( + entry.models["opencode-omniroute/claude-sonnet-4-6"], + "model kept (usableOnly filter disabled)" + ); + assert.ok( + logger.entries.some((e) => String(e[0]).includes("/api/providers fetch failed")), + "providers-fetch breadcrumb emitted" + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (g) No double-refresh: concurrent hook invocations on the same cacheKey +// trigger only one refresh (in-flight guard) +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: concurrent hook invocations dedupe refresh", async () => { + let fetchCount = 0; + const slowResolve = new Promise((r) => { + setTimeout(r, 100); + }); + + const fetcher: OmniRouteModelsFetcher = async () => { + fetchCount++; + await slowResolve; + return [MODEL_CLAUDE]; + }; + const combosFetcher = immediateFetcher([]); + const logger = captureWarn(); + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const sharedCache: OmniRouteFetchCache = new Map(); + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", modelCacheTtl: 60_000 }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + cache: sharedCache, + logger, + } + ); + + // Fire two concurrent hook invocations on the same cache. + const inputA = makeInput(); + const inputB = makeInput(); + await Promise.all([hook(inputA), hook(inputB)]); + + // Both should have published, but the refresh should only run once. + assert.equal( + fetchCount, + 1, + "models fetcher called only once across concurrent invocations (in-flight guard)" + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// (h) features.diskCache: false disables the warm read entirely +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: diskCache=false disables warm read, falls through to awaited fetch", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const logger = captureWarn(); + + let readerCalled = false; + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => { + readerCalled = true; + return { + rawModels: [MODEL_GEMINI], + rawCombos: [], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + }; + }; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute", features: { diskCache: false } }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + assert.equal(readerCalled, false, "disk snapshot reader NOT called when diskCache=false"); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "provider entry published from live fetch"); + assert.ok( + entry.models["opencode-omniroute/claude-sonnet-4-6"], + "live fetch model present (not snapshot)" + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// Warm startup: snapshot age logged +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: snapshot age is logged when warm-starting from disk", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const logger = captureWarn(); + + const snapshot: Omit & { + writtenAt?: number; + } = { + rawModels: [MODEL_GEMINI], + rawCombos: [], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + writtenAt: Date.now() - 3_600_000, // 1 hour ago + }; + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => snapshot; + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + // The log should mention "warm startup from disk snapshot". + assert.ok( + logger.entries.some((e) => + String(e[0]).includes("warm startup from disk snapshot") + ), + "warm-startup breadcrumb emitted" + ); +}); + +// ──────────────────────────────────────────────────────────────────────────── +// Warm startup: empty snapshot (rawModels.length === 0) is skipped +// ──────────────────────────────────────────────────────────────────────────── + +test("warm-startup: empty snapshot (rawModels.length=0) is skipped, falls through to fetch", async () => { + const fetcher = immediateFetcher([MODEL_CLAUDE]); + const combosFetcher = immediateFetcher([]); + const logger = captureWarn(); + + const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => ({ + rawModels: [], + rawCombos: [], + rawAutoCombos: [], + rawEnrichment: new Map(), + rawCompressionCombos: [], + rawConnections: [], + }); + const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {}; + + const hook = createOmniRouteConfigHook( + { providerId: "omniroute" }, + { + readAuthJson: authStub(), + fetcher, + combosFetcher, + diskSnapshotReader, + diskSnapshotWriter, + logger, + } + ); + + const input = makeInput(); + await hook(input); + + const entry = (input as { provider: Record }).provider[ + "opencode-omniroute" + ]; + assert.ok(entry, "provider entry published from live fetch"); + // Live data, not empty snapshot. + assert.ok( + entry.models["opencode-omniroute/claude-sonnet-4-6"], + "live fetch model present (empty snapshot skipped)" + ); + assert.equal(fetcher.callCount(), 1, "fetcher was called (awaited cold path)"); +}); diff --git a/changelog.d/features/9490-opencode-plugin-warm-startup-parallel-refresh.md b/changelog.d/features/9490-opencode-plugin-warm-startup-parallel-refresh.md new file mode 100644 index 0000000000..f32157ce18 --- /dev/null +++ b/changelog.d/features/9490-opencode-plugin-warm-startup-parallel-refresh.md @@ -0,0 +1,5 @@ +--- +feature: 9490 +--- + +**Warm catalog startup from disk snapshot + parallel refresh** (opencode-plugin): The config-shim hook now reads the last disk snapshot *before* fetching, so the provider registers immediately with the last-known-good catalog (~1-2s vs ~30s on a warm gateway). All six fetchers run concurrently via `Promise.allSettled` instead of sequentially. A failed refresh keeps the snapshot (no overwrite). An in-flight guard prevents concurrent refreshes for the same cache key. The `features.diskCache: false` opt-out disables the warm read entirely.