@oh-my-pi/pi-catalog 18.4.0 → 18.4.2

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.
@@ -270,6 +270,8 @@ export interface CompiledCursorParameter {
270
270
  /** One provider quota-scope table. */
271
271
  export interface CompiledQuotaRule {
272
272
  provider: string;
273
+ /** Tier used when no exact or fallback membership matches. */
274
+ defaultTier?: string;
273
275
  tiers: { label: string; models: string[] }[];
274
276
  fallbacks: { label: string; substring: string }[];
275
277
  }
@@ -94,7 +94,15 @@ export interface CacheEntry<TApi extends Api = Api> {
94
94
  let sharedDb: Database | null = null;
95
95
  let sharedDbPath: string | null = null;
96
96
 
97
- const readRowCache = new Map<string, { dataVersion: number; entry: CacheEntry<Api> | null }>();
97
+ interface ReadRowCacheEntry {
98
+ /** `PRAGMA data_version` of the shared connection when validated; null for per-call connections. */
99
+ dataVersion: number | null;
100
+ /** Raw row the entry was parsed from; null when the row was absent or rejected. */
101
+ row: CacheRow | null;
102
+ entry: CacheEntry<Api> | null;
103
+ }
104
+
105
+ const readRowCache = new Map<string, ReadRowCacheEntry>();
98
106
  const READ_ROW_CACHE_MAX = 64;
99
107
 
100
108
  function readCacheKey(resolvedPath: string, providerId: string): string {
@@ -329,67 +337,98 @@ export function readModelCache<TApi extends Api>(
329
337
  // Monotonic change signal: same-shaped WAL overwrites after checkpoint
330
338
  // can leave every size:mtime pair identical, so file metadata alone
331
339
  // cannot invalidate. PRAGMA data_version increments on each committed
332
- // write transaction visible to a new reader.
333
- const entry = withModelCacheDb(dbPath, db => {
334
- const dataVersion = dbDataVersion(db);
335
- if (dataVersion !== null) {
336
- const cached = readRowCache.get(key);
337
- if (cached !== undefined && cached.dataVersion === dataVersion) {
338
- // Freshness is time-relative: recompute per call from the
339
- // cached row's updatedAt so a long-lived process goes stale.
340
- return { hit: true as const, entry: withFreshness(cached.entry as CacheEntry<TApi> | null, ttlMs, now) };
341
- }
340
+ // write transaction visible to a new reader. It is only comparable
341
+ // across calls on the same connection, so it gates the no-query fast
342
+ // path for the shared handle only.
343
+ const shared = dbPath === undefined;
344
+ return runModelCacheDb(resolvedPath, shared, db => {
345
+ const dataVersion = shared ? dbDataVersion(db) : null;
346
+ const cached = readRowCache.get(key);
347
+ if (dataVersion !== null && cached?.dataVersion === dataVersion) {
348
+ // Freshness is time-relative: recompute per call from the
349
+ // cached row's updatedAt so a long-lived process goes stale.
350
+ return withFreshness(cached.entry as CacheEntry<TApi> | null, ttlMs, now);
342
351
  }
343
- const fresh = readRowUncached<TApi>(db, providerId, ttlMs, now);
344
- if (dataVersion !== null) {
345
- if (readRowCache.size >= READ_ROW_CACHE_MAX) readRowCache.clear();
346
- readRowCache.set(key, { dataVersion, entry: fresh as CacheEntry<Api> | null });
352
+ // Any commit (to any provider's row) bumps data_version. Re-read this
353
+ // row, but reuse the parsed entry when its bytes are unchanged.
354
+ const stmt = db.query<CacheRow, [string]>("SELECT * FROM model_cache WHERE provider_id = ?");
355
+ let row: CacheRow | null;
356
+ try {
357
+ row = stmt.get(providerId);
358
+ } finally {
359
+ stmt.finalize();
360
+ }
361
+ let entry: CacheEntry<TApi> | null;
362
+ if (
363
+ cached?.entry &&
364
+ cached.row &&
365
+ row &&
366
+ row.materialization_policy === materializationPolicy() &&
367
+ cacheRowsEqual(cached.row, row)
368
+ ) {
369
+ entry = withFreshness(cached.entry as CacheEntry<TApi>, ttlMs, now);
370
+ } else {
371
+ entry = parseCacheRow<TApi>(db, providerId, row, ttlMs, now);
347
372
  }
348
- return { hit: false as const, entry: fresh };
373
+ if (readRowCache.size >= READ_ROW_CACHE_MAX) readRowCache.clear();
374
+ readRowCache.set(key, {
375
+ dataVersion,
376
+ row: entry === null ? null : row,
377
+ entry: entry as CacheEntry<Api> | null,
378
+ });
379
+ return entry;
349
380
  });
350
- return entry.entry;
351
381
  } catch {
352
382
  return null;
353
383
  }
354
384
  }
355
385
 
356
- function readRowUncached<TApi extends Api>(
386
+ function cacheRowsEqual(left: CacheRow, right: CacheRow): boolean {
387
+ return (
388
+ left.version === right.version &&
389
+ left.materialization_policy === right.materialization_policy &&
390
+ left.updated_at === right.updated_at &&
391
+ left.authoritative === right.authoritative &&
392
+ left.static_fingerprint === right.static_fingerprint &&
393
+ left.header_omitted_model_ids === right.header_omitted_model_ids &&
394
+ left.unrestorable_header_model_ids === right.unrestorable_header_model_ids &&
395
+ left.header_restore_version === right.header_restore_version &&
396
+ left.models === right.models
397
+ );
398
+ }
399
+
400
+ function parseCacheRow<TApi extends Api>(
357
401
  db: Database,
358
402
  providerId: string,
403
+ row: CacheRow | null,
359
404
  ttlMs: number,
360
405
  now: () => number,
361
406
  ): CacheEntry<TApi> | null {
362
- const stmt = db.query<CacheRow, [string]>("SELECT * FROM model_cache WHERE provider_id = ?");
363
- try {
364
- const row = stmt.get(providerId);
365
- if (!row || row.version !== CACHE_SCHEMA_VERSION || row.materialization_policy !== materializationPolicy()) {
366
- return null;
367
- }
368
- const models = parseMaterializedModels<TApi>(row.models);
369
- const headerOmittedModelIds = parseModelIds(row.header_omitted_model_ids);
370
- const unrestorableHeaderModelIds = parseModelIds(row.unrestorable_header_model_ids);
371
- if (models === null || headerOmittedModelIds === null || unrestorableHeaderModelIds === null) {
372
- // Fail closed on corrupt header provenance: treating malformed
373
- // markers as empty could return a model with required credentials
374
- // silently absent. secure_delete scrubs the rejected payload.
375
- db.run("DELETE FROM model_cache WHERE provider_id = ?", [providerId]);
376
- return null;
377
- }
378
- const ageMs = now() - row.updated_at;
379
- const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
380
- return {
381
- models,
382
- fresh,
383
- authoritative: row.authoritative === 1,
384
- updatedAt: row.updated_at,
385
- headerOmittedModelIds,
386
- unrestorableHeaderModelIds,
387
- legacyHeaderRestoreMarkers: row.header_restore_version < HEADER_RESTORE_VERSION,
388
- staticFingerprint: row.static_fingerprint ?? "",
389
- };
390
- } finally {
391
- stmt.finalize();
407
+ if (!row || row.version !== CACHE_SCHEMA_VERSION || row.materialization_policy !== materializationPolicy()) {
408
+ return null;
392
409
  }
410
+ const models = parseMaterializedModels<TApi>(row.models);
411
+ const headerOmittedModelIds = parseModelIds(row.header_omitted_model_ids);
412
+ const unrestorableHeaderModelIds = parseModelIds(row.unrestorable_header_model_ids);
413
+ if (models === null || headerOmittedModelIds === null || unrestorableHeaderModelIds === null) {
414
+ // Fail closed on corrupt header provenance: treating malformed
415
+ // markers as empty could return a model with required credentials
416
+ // silently absent. secure_delete scrubs the rejected payload.
417
+ db.run("DELETE FROM model_cache WHERE provider_id = ?", [providerId]);
418
+ return null;
419
+ }
420
+ const ageMs = now() - row.updated_at;
421
+ const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
422
+ return {
423
+ models,
424
+ fresh,
425
+ authoritative: row.authoritative === 1,
426
+ updatedAt: row.updated_at,
427
+ headerOmittedModelIds,
428
+ unrestorableHeaderModelIds,
429
+ legacyHeaderRestoreMarkers: row.header_restore_version < HEADER_RESTORE_VERSION,
430
+ staticFingerprint: row.static_fingerprint ?? "",
431
+ };
393
432
  }
394
433
 
395
434
  /** Whether a live model carries at least one request header. */