@oh-my-pi/pi-catalog 18.4.6 → 18.4.9
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.
- package/CHANGELOG.md +6 -0
- package/dist/types/model-cache.d.ts +25 -0
- package/package.json +4 -4
- package/src/model-cache.ts +228 -40
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.4.9] - 2026-10-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed model catalog caching so unchanged catalogs refresh without unnecessary rewrites, and offline snapshots for endpoint-less models (such as Azure models) are now handled correctly across startups.
|
|
10
|
+
|
|
5
11
|
## [18.4.6] - 2026-10-01
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { Api, Model } from "./types.js";
|
|
2
|
+
/** Cumulative model-cache write counters for this process. */
|
|
3
|
+
export interface ModelCacheWriteStats {
|
|
4
|
+
/** Payload rows (re)written because their stored content changed. */
|
|
5
|
+
payloadWrites: number;
|
|
6
|
+
/** Serialized model bytes written by `payloadWrites`. */
|
|
7
|
+
payloadBytes: number;
|
|
8
|
+
/** Unchanged payloads whose freshness/authority advanced via the side table. */
|
|
9
|
+
refreshWrites: number;
|
|
10
|
+
/** Writes that matched the stored row exactly and touched nothing. */
|
|
11
|
+
skippedWrites: number;
|
|
12
|
+
/** Provider ids of the most recent payload writes (bounded). */
|
|
13
|
+
recentPayloadProviders: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** Snapshot of this process's model-cache write counters. */
|
|
16
|
+
export declare function getModelCacheWriteStats(): ModelCacheWriteStats;
|
|
2
17
|
/** Materialized provider snapshot with freshness and header-restoration provenance. */
|
|
3
18
|
export interface CacheEntry<TApi extends Api = Api> {
|
|
4
19
|
/** Trusted, fully materialized rows; request headers remain omitted. */
|
|
@@ -21,4 +36,14 @@ export interface CacheEntry<TApi extends Api = Api> {
|
|
|
21
36
|
staticFingerprint: string;
|
|
22
37
|
}
|
|
23
38
|
export declare function readModelCache<TApi extends Api>(providerId: string, ttlMs: number, now: () => number, dbPath?: string): CacheEntry<TApi> | null;
|
|
39
|
+
/**
|
|
40
|
+
* Persist a provider snapshot.
|
|
41
|
+
*
|
|
42
|
+
* When the stored payload (models, header provenance, fingerprint, schema and
|
|
43
|
+
* materialization policy) is byte-identical, the multi-MB row is left alone and
|
|
44
|
+
* only `model_cache_refresh` records the new freshness and authority; a write
|
|
45
|
+
* that changes nothing at all is skipped. Every model persisted here is
|
|
46
|
+
* validated with the reader's own predicate, so a row this writes is never
|
|
47
|
+
* rejected (and deleted) by `readModelCache`.
|
|
48
|
+
*/
|
|
24
49
|
export declare function writeModelCache<TApi extends Api>(providerId: string, updatedAt: number, models: Model<TApi>[], authoritative: boolean, staticFingerprint: string, dbPath?: string, staticHeaderSources?: readonly Model<TApi>[], restorableHeaderFallback?: Record<string, string>): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-catalog",
|
|
3
|
-
"version": "18.4.
|
|
3
|
+
"version": "18.4.9",
|
|
4
4
|
"description": "Model catalog for omp: bundled model database, provider discovery descriptors, model identity, classification, and equivalence",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -93,12 +93,12 @@
|
|
|
93
93
|
"gen:proto": "bun scripts/generate-protocols.ts"
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
|
-
"@oh-my-pi/omptype": "18.4.
|
|
97
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
96
|
+
"@oh-my-pi/omptype": "18.4.9",
|
|
97
|
+
"@oh-my-pi/pi-utils": "18.4.9"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@bgotink/kdl": "0.4.0",
|
|
101
|
-
"@oh-my-pi/pi-ai": "18.4.
|
|
101
|
+
"@oh-my-pi/pi-ai": "18.4.9",
|
|
102
102
|
"@types/bun": "^1.3.14"
|
|
103
103
|
},
|
|
104
104
|
"engines": {
|
package/src/model-cache.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Replaces per-provider JSON files with a single cache.db.
|
|
4
4
|
*/
|
|
5
5
|
import type { Database } from "bun:sqlite";
|
|
6
|
-
import { getModelDbPath, isSqliteCorruptionError, openSqliteDatabaseSync, VERSION } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import { getModelDbPath, isSqliteCorruptionError, logger, openSqliteDatabaseSync, VERSION } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import RULES from "./compat/rules.json" with { type: "json" };
|
|
8
8
|
import type { Api, Model } from "./types";
|
|
9
9
|
|
|
@@ -25,6 +25,8 @@ import type { Api, Model } from "./types";
|
|
|
25
25
|
// effort-tier variant collapsing (raw `-low`/`-high`/`-thinking` member ids);
|
|
26
26
|
// v4 dropped the pre-efforts ThinkingConfig shape.
|
|
27
27
|
const CACHE_SCHEMA_VERSION = 13;
|
|
28
|
+
/** Oldest schema whose rows are guaranteed never to carry request headers. */
|
|
29
|
+
const FIRST_HEADERLESS_SCHEMA_VERSION = 11;
|
|
28
30
|
const HEADER_RESTORE_VERSION = 1;
|
|
29
31
|
/**
|
|
30
32
|
* Explicit compatibility gate for materialized rows. Bump whenever buildModel
|
|
@@ -55,8 +57,58 @@ interface CacheRow {
|
|
|
55
57
|
header_omitted_model_ids: string;
|
|
56
58
|
unrestorable_header_model_ids: string;
|
|
57
59
|
header_restore_version: number;
|
|
60
|
+
/** `model_cache_refresh` columns joined onto the payload row; null when absent. */
|
|
61
|
+
refresh_payload_updated_at: number | null;
|
|
62
|
+
refresh_updated_at: number | null;
|
|
63
|
+
refresh_authoritative: number | null;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Payload rows are multi-MB and SQLite rewrites a whole record (including its
|
|
68
|
+
* overflow pages) on any column update. Freshness that advances without a
|
|
69
|
+
* payload change therefore lives in `model_cache_refresh`, a small side row
|
|
70
|
+
* keyed by provider. It applies only while `payload_updated_at` still matches
|
|
71
|
+
* the payload row's `updated_at`, so a payload rewritten by another binary
|
|
72
|
+
* silently invalidates it.
|
|
73
|
+
*/
|
|
74
|
+
const SELECT_CACHE_ROW = `
|
|
75
|
+
SELECT m.*,
|
|
76
|
+
r.payload_updated_at AS refresh_payload_updated_at,
|
|
77
|
+
r.updated_at AS refresh_updated_at,
|
|
78
|
+
r.authoritative AS refresh_authoritative
|
|
79
|
+
FROM model_cache m
|
|
80
|
+
LEFT JOIN model_cache_refresh r ON r.provider_id = m.provider_id
|
|
81
|
+
WHERE m.provider_id = ?
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
/** Cumulative model-cache write counters for this process. */
|
|
85
|
+
export interface ModelCacheWriteStats {
|
|
86
|
+
/** Payload rows (re)written because their stored content changed. */
|
|
87
|
+
payloadWrites: number;
|
|
88
|
+
/** Serialized model bytes written by `payloadWrites`. */
|
|
89
|
+
payloadBytes: number;
|
|
90
|
+
/** Unchanged payloads whose freshness/authority advanced via the side table. */
|
|
91
|
+
refreshWrites: number;
|
|
92
|
+
/** Writes that matched the stored row exactly and touched nothing. */
|
|
93
|
+
skippedWrites: number;
|
|
94
|
+
/** Provider ids of the most recent payload writes (bounded). */
|
|
95
|
+
recentPayloadProviders: readonly string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const writeStats = { payloadWrites: 0, payloadBytes: 0, refreshWrites: 0, skippedWrites: 0 };
|
|
99
|
+
const recentPayloadProviders: string[] = [];
|
|
100
|
+
const RECENT_PAYLOAD_PROVIDERS_MAX = 64;
|
|
101
|
+
|
|
102
|
+
/** Snapshot of this process's model-cache write counters. */
|
|
103
|
+
export function getModelCacheWriteStats(): ModelCacheWriteStats {
|
|
104
|
+
return { ...writeStats, recentPayloadProviders: [...recentPayloadProviders] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type StoredRowState = Pick<
|
|
108
|
+
CacheRow,
|
|
109
|
+
"updated_at" | "authoritative" | "refresh_payload_updated_at" | "refresh_updated_at" | "refresh_authoritative"
|
|
110
|
+
>;
|
|
111
|
+
|
|
60
112
|
interface TableInfoRow {
|
|
61
113
|
name: string;
|
|
62
114
|
}
|
|
@@ -144,9 +196,11 @@ function invalidateReadPath(resolvedPath: string): void {
|
|
|
144
196
|
|
|
145
197
|
function initializeDb(db: Database): void {
|
|
146
198
|
// The shared opener installs the busy handler before any lock-taking
|
|
147
|
-
// statement.
|
|
148
|
-
//
|
|
149
|
-
|
|
199
|
+
// statement. Current rows never persist request headers, so routine
|
|
200
|
+
// replacements do not need freed overflow pages zero-filled (that doubled
|
|
201
|
+
// the I/O of every multi-MB refresh). The legacy credential purge (#5780)
|
|
202
|
+
// re-enables ON just around its DELETE.
|
|
203
|
+
db.run("PRAGMA secure_delete = FAST");
|
|
150
204
|
db.run("PRAGMA journal_mode = WAL");
|
|
151
205
|
db.run(`
|
|
152
206
|
CREATE TABLE IF NOT EXISTS model_cache (
|
|
@@ -162,6 +216,14 @@ function initializeDb(db: Database): void {
|
|
|
162
216
|
models TEXT NOT NULL
|
|
163
217
|
)
|
|
164
218
|
`);
|
|
219
|
+
db.run(`
|
|
220
|
+
CREATE TABLE IF NOT EXISTS model_cache_refresh (
|
|
221
|
+
provider_id TEXT PRIMARY KEY,
|
|
222
|
+
payload_updated_at INTEGER NOT NULL,
|
|
223
|
+
updated_at INTEGER NOT NULL,
|
|
224
|
+
authoritative INTEGER NOT NULL
|
|
225
|
+
)
|
|
226
|
+
`);
|
|
165
227
|
migrateCacheSchema(db);
|
|
166
228
|
}
|
|
167
229
|
|
|
@@ -237,15 +299,29 @@ function migrateCacheSchema(db: Database): void {
|
|
|
237
299
|
} finally {
|
|
238
300
|
stmt.finalize();
|
|
239
301
|
}
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
302
|
+
// Rows predating v11 may carry credential-bearing request headers (v10 could
|
|
303
|
+
// still persist derived computer-use headers), so purge them and scrub the
|
|
304
|
+
// freed cells (#5780). Every other non-current row (v11/v12, a newer schema,
|
|
305
|
+
// or another materialization policy from a different app version sharing
|
|
306
|
+
// this file) never persisted headers: readers gate on the exact version and
|
|
307
|
+
// policy, treat it as absent, and the next write for that provider replaces
|
|
308
|
+
// it lazily, so switching versions does not wipe and re-download every
|
|
309
|
+
// provider. Never promote old rows in place: the legacy `UPDATE ... WHERE
|
|
310
|
+
// version = 2` migration did, defeating every later invalidation (#4146).
|
|
311
|
+
const legacyStmt = db.prepare("SELECT 1 AS found FROM model_cache WHERE version < ? LIMIT 1");
|
|
312
|
+
let hasLegacyRows: boolean;
|
|
313
|
+
try {
|
|
314
|
+
hasLegacyRows = legacyStmt.get(FIRST_HEADERLESS_SCHEMA_VERSION) !== null;
|
|
315
|
+
} finally {
|
|
316
|
+
legacyStmt.finalize();
|
|
317
|
+
}
|
|
318
|
+
if (!hasLegacyRows) return;
|
|
319
|
+
db.run("PRAGMA secure_delete = ON");
|
|
320
|
+
try {
|
|
321
|
+
db.run("DELETE FROM model_cache WHERE version < ?", [FIRST_HEADERLESS_SCHEMA_VERSION]);
|
|
322
|
+
} finally {
|
|
323
|
+
db.run("PRAGMA secure_delete = FAST");
|
|
324
|
+
}
|
|
249
325
|
}
|
|
250
326
|
|
|
251
327
|
function isMaterializedModel(value: unknown): value is PersistedModel<Api> {
|
|
@@ -260,8 +336,9 @@ function isMaterializedModel(value: unknown): value is PersistedModel<Api> {
|
|
|
260
336
|
model.api.length === 0 ||
|
|
261
337
|
typeof model.provider !== "string" ||
|
|
262
338
|
model.provider.length === 0 ||
|
|
339
|
+
// Empty is legitimate: Azure-style providers resolve the endpoint from
|
|
340
|
+
// configuration at request time, and their bundled rows carry "".
|
|
263
341
|
typeof model.baseUrl !== "string" ||
|
|
264
|
-
model.baseUrl.length === 0 ||
|
|
265
342
|
typeof model.reasoning !== "boolean" ||
|
|
266
343
|
!Array.isArray(model.input) ||
|
|
267
344
|
model.input.length === 0 ||
|
|
@@ -351,7 +428,7 @@ export function readModelCache<TApi extends Api>(
|
|
|
351
428
|
}
|
|
352
429
|
// Any commit (to any provider's row) bumps data_version. Re-read this
|
|
353
430
|
// row, but reuse the parsed entry when its bytes are unchanged.
|
|
354
|
-
const stmt = db.query<CacheRow, [string]>(
|
|
431
|
+
const stmt = db.query<CacheRow, [string]>(SELECT_CACHE_ROW);
|
|
355
432
|
let row: CacheRow | null;
|
|
356
433
|
try {
|
|
357
434
|
row = stmt.get(providerId);
|
|
@@ -366,7 +443,8 @@ export function readModelCache<TApi extends Api>(
|
|
|
366
443
|
row.materialization_policy === materializationPolicy() &&
|
|
367
444
|
cacheRowsEqual(cached.row, row)
|
|
368
445
|
) {
|
|
369
|
-
|
|
446
|
+
// Payload bytes unchanged; only side-table freshness may have moved.
|
|
447
|
+
entry = withFreshness(withRowState(cached.entry as CacheEntry<TApi>, row), ttlMs, now);
|
|
370
448
|
} else {
|
|
371
449
|
entry = parseCacheRow<TApi>(db, providerId, row, ttlMs, now);
|
|
372
450
|
}
|
|
@@ -383,6 +461,7 @@ export function readModelCache<TApi extends Api>(
|
|
|
383
461
|
}
|
|
384
462
|
}
|
|
385
463
|
|
|
464
|
+
/** Whether two rows carry the same payload record (refresh side-row columns excluded). */
|
|
386
465
|
function cacheRowsEqual(left: CacheRow, right: CacheRow): boolean {
|
|
387
466
|
return (
|
|
388
467
|
left.version === right.version &&
|
|
@@ -397,6 +476,25 @@ function cacheRowsEqual(left: CacheRow, right: CacheRow): boolean {
|
|
|
397
476
|
);
|
|
398
477
|
}
|
|
399
478
|
|
|
479
|
+
/** Effective freshness/authority: the side row wins while it refers to this payload. */
|
|
480
|
+
function rowState(row: StoredRowState): { updatedAt: number; authoritative: boolean } {
|
|
481
|
+
if (
|
|
482
|
+
row.refresh_updated_at !== null &&
|
|
483
|
+
row.refresh_payload_updated_at === row.updated_at &&
|
|
484
|
+
row.refresh_authoritative !== null
|
|
485
|
+
) {
|
|
486
|
+
return { updatedAt: row.refresh_updated_at, authoritative: row.refresh_authoritative === 1 };
|
|
487
|
+
}
|
|
488
|
+
return { updatedAt: row.updated_at, authoritative: row.authoritative === 1 };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function withRowState<TApi extends Api>(entry: CacheEntry<TApi>, row: CacheRow): CacheEntry<TApi> {
|
|
492
|
+
const { updatedAt, authoritative } = rowState(row);
|
|
493
|
+
return entry.updatedAt === updatedAt && entry.authoritative === authoritative
|
|
494
|
+
? entry
|
|
495
|
+
: { ...entry, updatedAt, authoritative };
|
|
496
|
+
}
|
|
497
|
+
|
|
400
498
|
function parseCacheRow<TApi extends Api>(
|
|
401
499
|
db: Database,
|
|
402
500
|
providerId: string,
|
|
@@ -413,17 +511,19 @@ function parseCacheRow<TApi extends Api>(
|
|
|
413
511
|
if (models === null || headerOmittedModelIds === null || unrestorableHeaderModelIds === null) {
|
|
414
512
|
// Fail closed on corrupt header provenance: treating malformed
|
|
415
513
|
// markers as empty could return a model with required credentials
|
|
416
|
-
// silently absent.
|
|
514
|
+
// silently absent. Current-schema rows never persist headers, so the
|
|
515
|
+
// rejected payload needs no secure scrub.
|
|
417
516
|
db.run("DELETE FROM model_cache WHERE provider_id = ?", [providerId]);
|
|
418
517
|
return null;
|
|
419
518
|
}
|
|
420
|
-
const
|
|
519
|
+
const { updatedAt, authoritative } = rowState(row);
|
|
520
|
+
const ageMs = now() - updatedAt;
|
|
421
521
|
const fresh = Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= ttlMs;
|
|
422
522
|
return {
|
|
423
523
|
models,
|
|
424
524
|
fresh,
|
|
425
|
-
authoritative
|
|
426
|
-
updatedAt
|
|
525
|
+
authoritative,
|
|
526
|
+
updatedAt,
|
|
427
527
|
headerOmittedModelIds,
|
|
428
528
|
unrestorableHeaderModelIds,
|
|
429
529
|
legacyHeaderRestoreMarkers: row.header_restore_version < HEADER_RESTORE_VERSION,
|
|
@@ -468,6 +568,16 @@ function headersEqual(left: Record<string, string> | undefined, right: Record<st
|
|
|
468
568
|
return true;
|
|
469
569
|
}
|
|
470
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Persist a provider snapshot.
|
|
573
|
+
*
|
|
574
|
+
* When the stored payload (models, header provenance, fingerprint, schema and
|
|
575
|
+
* materialization policy) is byte-identical, the multi-MB row is left alone and
|
|
576
|
+
* only `model_cache_refresh` records the new freshness and authority; a write
|
|
577
|
+
* that changes nothing at all is skipped. Every model persisted here is
|
|
578
|
+
* validated with the reader's own predicate, so a row this writes is never
|
|
579
|
+
* rejected (and deleted) by `readModelCache`.
|
|
580
|
+
*/
|
|
471
581
|
export function writeModelCache<TApi extends Api>(
|
|
472
582
|
providerId: string,
|
|
473
583
|
updatedAt: number,
|
|
@@ -484,8 +594,16 @@ export function writeModelCache<TApi extends Api>(
|
|
|
484
594
|
const headerOmittedModelIds: string[] = [];
|
|
485
595
|
const unrestorableHeaderModelIds: string[] = [];
|
|
486
596
|
const cachedModels: PersistedModel<TApi>[] = [];
|
|
597
|
+
const rejectedModelIds: string[] = [];
|
|
487
598
|
const staticById = new Map(staticHeaderSources.map(model => [model.id, model]));
|
|
488
599
|
for (const model of models) {
|
|
600
|
+
const cachedModel = toCachedModel(model);
|
|
601
|
+
if (!isMaterializedModel(cachedModel)) {
|
|
602
|
+
// One unreadable model must not make the whole row fail
|
|
603
|
+
// validation on read (which deletes it and refetches forever).
|
|
604
|
+
rejectedModelIds.push(model.id);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
489
607
|
if (hasModelHeaders(model)) {
|
|
490
608
|
headerOmittedModelIds.push(model.id);
|
|
491
609
|
// Synthesized variants (e.g. Copilot `-1m`) have no same-id static
|
|
@@ -507,27 +625,97 @@ export function writeModelCache<TApi extends Api>(
|
|
|
507
625
|
unrestorableHeaderModelIds.push(model.id);
|
|
508
626
|
}
|
|
509
627
|
}
|
|
510
|
-
cachedModels.push(
|
|
628
|
+
cachedModels.push(cachedModel);
|
|
629
|
+
}
|
|
630
|
+
if (rejectedModelIds.length > 0) {
|
|
631
|
+
logger.debug("model cache skipped models that would fail cache validation", {
|
|
632
|
+
provider: providerId,
|
|
633
|
+
models: rejectedModelIds,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const policy = materializationPolicy();
|
|
637
|
+
const authoritativeFlag = authoritative ? 1 : 0;
|
|
638
|
+
const serializedHeaderOmitted = JSON.stringify(headerOmittedModelIds);
|
|
639
|
+
const serializedUnrestorable = JSON.stringify(unrestorableHeaderModelIds);
|
|
640
|
+
const serializedModels = JSON.stringify(cachedModels);
|
|
641
|
+
const outcome = db
|
|
642
|
+
.transaction((): "skipped" | "refreshed" | "written" => {
|
|
643
|
+
const matchStmt = db.prepare<
|
|
644
|
+
StoredRowState,
|
|
645
|
+
[string, number, string, string, string, string, number, string]
|
|
646
|
+
>(
|
|
647
|
+
`SELECT m.updated_at, m.authoritative,
|
|
648
|
+
r.payload_updated_at AS refresh_payload_updated_at,
|
|
649
|
+
r.updated_at AS refresh_updated_at,
|
|
650
|
+
r.authoritative AS refresh_authoritative
|
|
651
|
+
FROM model_cache m
|
|
652
|
+
LEFT JOIN model_cache_refresh r ON r.provider_id = m.provider_id
|
|
653
|
+
WHERE m.provider_id = ? AND m.version = ? AND m.materialization_policy = ?
|
|
654
|
+
AND m.static_fingerprint = ? AND m.header_omitted_model_ids = ?
|
|
655
|
+
AND m.unrestorable_header_model_ids = ? AND m.header_restore_version = ? AND m.models = ?`,
|
|
656
|
+
);
|
|
657
|
+
let stored: StoredRowState | null;
|
|
658
|
+
try {
|
|
659
|
+
stored = matchStmt.get(
|
|
660
|
+
providerId,
|
|
661
|
+
CACHE_SCHEMA_VERSION,
|
|
662
|
+
policy,
|
|
663
|
+
staticFingerprint,
|
|
664
|
+
serializedHeaderOmitted,
|
|
665
|
+
serializedUnrestorable,
|
|
666
|
+
HEADER_RESTORE_VERSION,
|
|
667
|
+
serializedModels,
|
|
668
|
+
);
|
|
669
|
+
} finally {
|
|
670
|
+
matchStmt.finalize();
|
|
671
|
+
}
|
|
672
|
+
if (stored) {
|
|
673
|
+
const state = rowState(stored);
|
|
674
|
+
if (state.updatedAt === updatedAt && state.authoritative === authoritative) return "skipped";
|
|
675
|
+
db.run(
|
|
676
|
+
`INSERT INTO model_cache_refresh (provider_id, payload_updated_at, updated_at, authoritative)
|
|
677
|
+
VALUES (?, ?, ?, ?)
|
|
678
|
+
ON CONFLICT(provider_id) DO UPDATE SET
|
|
679
|
+
payload_updated_at = excluded.payload_updated_at,
|
|
680
|
+
updated_at = excluded.updated_at,
|
|
681
|
+
authoritative = excluded.authoritative`,
|
|
682
|
+
[providerId, stored.updated_at, updatedAt, authoritativeFlag],
|
|
683
|
+
);
|
|
684
|
+
return "refreshed";
|
|
685
|
+
}
|
|
686
|
+
db.run(
|
|
687
|
+
`INSERT OR REPLACE INTO model_cache (
|
|
688
|
+
provider_id, version, materialization_policy, updated_at, authoritative, static_fingerprint,
|
|
689
|
+
header_omitted_model_ids, unrestorable_header_model_ids,
|
|
690
|
+
header_restore_version, models
|
|
691
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
692
|
+
[
|
|
693
|
+
providerId,
|
|
694
|
+
CACHE_SCHEMA_VERSION,
|
|
695
|
+
policy,
|
|
696
|
+
updatedAt,
|
|
697
|
+
authoritativeFlag,
|
|
698
|
+
staticFingerprint,
|
|
699
|
+
serializedHeaderOmitted,
|
|
700
|
+
serializedUnrestorable,
|
|
701
|
+
HEADER_RESTORE_VERSION,
|
|
702
|
+
serializedModels,
|
|
703
|
+
],
|
|
704
|
+
);
|
|
705
|
+
db.run("DELETE FROM model_cache_refresh WHERE provider_id = ?", [providerId]);
|
|
706
|
+
return "written";
|
|
707
|
+
})
|
|
708
|
+
.immediate();
|
|
709
|
+
if (outcome === "skipped") {
|
|
710
|
+
writeStats.skippedWrites++;
|
|
711
|
+
} else if (outcome === "refreshed") {
|
|
712
|
+
writeStats.refreshWrites++;
|
|
713
|
+
} else {
|
|
714
|
+
writeStats.payloadWrites++;
|
|
715
|
+
writeStats.payloadBytes += serializedModels.length;
|
|
716
|
+
if (recentPayloadProviders.length >= RECENT_PAYLOAD_PROVIDERS_MAX) recentPayloadProviders.shift();
|
|
717
|
+
recentPayloadProviders.push(providerId);
|
|
511
718
|
}
|
|
512
|
-
db.run(
|
|
513
|
-
`INSERT OR REPLACE INTO model_cache (
|
|
514
|
-
provider_id, version, materialization_policy, updated_at, authoritative, static_fingerprint,
|
|
515
|
-
header_omitted_model_ids, unrestorable_header_model_ids,
|
|
516
|
-
header_restore_version, models
|
|
517
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
518
|
-
[
|
|
519
|
-
providerId,
|
|
520
|
-
CACHE_SCHEMA_VERSION,
|
|
521
|
-
materializationPolicy(),
|
|
522
|
-
updatedAt,
|
|
523
|
-
authoritative ? 1 : 0,
|
|
524
|
-
staticFingerprint,
|
|
525
|
-
JSON.stringify(headerOmittedModelIds),
|
|
526
|
-
JSON.stringify(unrestorableHeaderModelIds),
|
|
527
|
-
HEADER_RESTORE_VERSION,
|
|
528
|
-
JSON.stringify(cachedModels),
|
|
529
|
-
],
|
|
530
|
-
);
|
|
531
719
|
});
|
|
532
720
|
} catch {
|
|
533
721
|
// Cache writes are best-effort; failures should not break model resolution.
|