@gmickel/gno 2.6.0 → 2.7.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.
Files changed (106) hide show
  1. package/README.md +25 -30
  2. package/assets/skill/SKILL.md +5 -3
  3. package/assets/skill/cli-reference.md +9 -2
  4. package/assets/skill/mcp-reference.md +2 -1
  5. package/assets/spa-production.json.gz +0 -0
  6. package/browser-extension/artifacts/{gno-browser-clipper-v2.6.0.zip → gno-browser-clipper-v2.7.1.zip} +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +1 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/package.json +2 -1
  10. package/spec/cli.md +109 -18
  11. package/spec/mcp.md +36 -2
  12. package/spec/output-schemas/ask.schema.json +1 -1
  13. package/spec/output-schemas/capture-receipt.schema.json +1 -1
  14. package/spec/output-schemas/collection-list.schema.json +2 -2
  15. package/spec/output-schemas/doctor.schema.json +88 -0
  16. package/spec/output-schemas/error.schema.json +11 -2
  17. package/spec/output-schemas/get.schema.json +1 -1
  18. package/spec/output-schemas/mcp-capture-result.schema.json +1 -2
  19. package/spec/output-schemas/memory-remember.schema.json +2 -2
  20. package/spec/output-schemas/multi-get.schema.json +4 -1
  21. package/spec/output-schemas/peek.schema.json +2 -9
  22. package/spec/output-schemas/resident-status.schema.json +22 -0
  23. package/spec/output-schemas/search-result.schema.json +1 -1
  24. package/spec/output-schemas/search-results.schema.json +1 -1
  25. package/spec/output-schemas/status.schema.json +110 -16
  26. package/src/cli/commands/ask.ts +31 -12
  27. package/src/cli/commands/doctor.ts +54 -20
  28. package/src/cli/commands/embed.ts +41 -3
  29. package/src/cli/commands/ls.ts +6 -1
  30. package/src/cli/commands/query.ts +5 -0
  31. package/src/cli/commands/status.ts +63 -5
  32. package/src/cli/commands/vec.ts +54 -0
  33. package/src/cli/detach.ts +29 -1
  34. package/src/cli/errors.ts +13 -9
  35. package/src/cli/program.ts +53 -1
  36. package/src/core/capture-sync.ts +9 -2
  37. package/src/core/host-paths.ts +49 -0
  38. package/src/core/memory-remember.ts +4 -3
  39. package/src/core/request-receipts.ts +63 -9
  40. package/src/core/shutdown-budget.ts +6 -0
  41. package/src/core/vector-partition-status.ts +52 -0
  42. package/src/core/windows-private-path.ts +136 -1
  43. package/src/embed/backlog.ts +145 -27
  44. package/src/embed/fingerprint.ts +6 -3
  45. package/src/embed/retry.ts +66 -27
  46. package/src/embed/variant-backlog.ts +48 -18
  47. package/src/embed/variant-retry.ts +31 -22
  48. package/src/index.ts +21 -2
  49. package/src/llm/inference-scope.ts +18 -0
  50. package/src/llm/native-worker/dispatcher.ts +2 -0
  51. package/src/llm/native-worker/embedding-identity.ts +42 -0
  52. package/src/llm/native-worker/protocol.ts +1 -0
  53. package/src/llm/types.ts +3 -0
  54. package/src/mcp/context.ts +9 -0
  55. package/src/mcp/resources/index.ts +6 -5
  56. package/src/mcp/tool-descriptions-core.ts +1 -1
  57. package/src/mcp/tools/capture.ts +1 -3
  58. package/src/mcp/tools/index.ts +11 -4
  59. package/src/mcp/tools/memory-remember.ts +1 -1
  60. package/src/mcp/tools/status.ts +23 -6
  61. package/src/pipeline/hybrid.ts +37 -7
  62. package/src/pipeline/vsearch.ts +14 -2
  63. package/src/serve/embed-scheduler.ts +133 -19
  64. package/src/serve/host-path-redaction.ts +116 -0
  65. package/src/serve/public/components/BootstrapStatus.tsx +5 -3
  66. package/src/serve/public/components/CaptureModal.tsx +1 -1
  67. package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
  68. package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
  69. package/src/serve/public/components/FirstRunWizard.tsx +4 -2
  70. package/src/serve/public/components/sessions/SessionSearch.tsx +2 -2
  71. package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
  72. package/src/serve/public/globals.built.css +1 -1
  73. package/src/serve/public/hooks/use-api.ts +17 -2
  74. package/src/serve/public/lib/request-intent.ts +8 -0
  75. package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
  76. package/src/serve/public/pages/Collections.tsx +16 -13
  77. package/src/serve/public/pages/Connectors.tsx +7 -4
  78. package/src/serve/public/pages/Dashboard.tsx +16 -11
  79. package/src/serve/public/pages/DocView.tsx +10 -5
  80. package/src/serve/public/pages/DocumentEditor.tsx +91 -14
  81. package/src/serve/public/pages/Search.tsx +1 -41
  82. package/src/serve/resident-runtime.ts +33 -1
  83. package/src/serve/resident-status.ts +13 -1
  84. package/src/serve/routes/sessions.ts +61 -5
  85. package/src/serve/server.ts +15 -12
  86. package/src/serve/status-model.ts +26 -6
  87. package/src/serve/status.ts +4 -7
  88. package/src/serve/watch-reconciliation-shared.ts +3 -0
  89. package/src/serve/watch-service-events.ts +3 -2
  90. package/src/serve/watch-service-run-flush.ts +35 -2
  91. package/src/serve/watch-service.ts +5 -0
  92. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  93. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  94. package/src/store/migrations/index.ts +4 -0
  95. package/src/store/sqlite/adapter.ts +24 -3
  96. package/src/store/sqlite/change-journal-store.ts +1 -1
  97. package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
  98. package/src/store/types.ts +11 -1
  99. package/src/store/vector/lazy.ts +46 -43
  100. package/src/store/vector/runtime-compat.ts +651 -0
  101. package/src/store/vector/sqlite-vec.ts +20 -2
  102. package/src/store/vector/status.ts +276 -35
  103. package/src/store/vector/types.ts +2 -0
  104. package/src/store/vector/variant-search.ts +71 -23
  105. package/src/store/vector/variants.ts +49 -14
  106. package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +0 -1
@@ -3,8 +3,16 @@ import type { Database } from "bun:sqlite";
3
3
 
4
4
  import { getEmbeddingFingerprint } from "../../embed/fingerprint";
5
5
  import { formatDocForEmbedding } from "../../pipeline/contextual";
6
+ import {
7
+ currentOwnerCount,
8
+ identityPartitionId,
9
+ recordedRuntimeCaller,
10
+ retrievalUse,
11
+ selectRuntimePartition,
12
+ } from "./runtime-compat";
6
13
  import {
7
14
  embeddingInputHash,
15
+ loadSqliteVec,
8
16
  SELECTED_VECTOR_PARTITION_PREFIX,
9
17
  } from "./variants";
10
18
 
@@ -16,6 +24,204 @@ interface Partition {
16
24
  dimensions: number;
17
25
  state: string;
18
26
  activated_epoch: number | null;
27
+ legacy: number;
28
+ provenance: string | null;
29
+ fork: string | null;
30
+ }
31
+
32
+ export interface VectorPartitionStatus {
33
+ id: string;
34
+ model: string;
35
+ dimensions: number;
36
+ state: "active" | "shadow";
37
+ /** Pre-fn-184 runtime-keyed partition awaiting measured re-keying. */
38
+ legacy: boolean;
39
+ /**
40
+ * This caller's retrieval reads this partition, selected by the same rule
41
+ * queries use (see `vectorRuntime`); status counts use it.
42
+ */
43
+ retrieval: boolean;
44
+ /** `gno vec drop` accepts it: a shadow partition this runtime does not read. */
45
+ droppable: boolean;
46
+ /** Current document chunks bound to this partition. */
47
+ owners: number;
48
+ /** Runtime that built the partition, e.g. "CUDA, Bun 1.4.2". */
49
+ provenance: string;
50
+ /** Runtimes measured (or recorded as the builder) to read this partition. */
51
+ compatibleRuntimes: string[];
52
+ /** Runtimes measured incompatible; they need another partition or lexical. */
53
+ incompatibleRuntimes: string[];
54
+ }
55
+
56
+ /** The calling process's runtime, resolved exactly as its queries resolve it. */
57
+ export interface VectorRuntimeStatus {
58
+ /** Runtime label recorded by this caller's last query or embed. */
59
+ label: string | null;
60
+ /**
61
+ * vectors: queries read `partition`; unavailable: queries use lexical
62
+ * retrieval only; unresolved: no query or embed has resolved this caller
63
+ * yet (or a verdict is pending).
64
+ */
65
+ state: "vectors" | "unavailable" | "unresolved";
66
+ partition: string | null;
67
+ reason?: string;
68
+ }
69
+
70
+ const activated = (p: Partition): boolean =>
71
+ p.state === "active" && p.activated_epoch !== null;
72
+
73
+ /** Apply retrieval's selection to the caller recorded for this process. */
74
+ export function vectorRuntimeStatus(
75
+ db: Database,
76
+ model: string
77
+ ): VectorRuntimeStatus {
78
+ const caller = recordedRuntimeCaller(db, model);
79
+ if (!caller) return { label: null, state: "unresolved", partition: null };
80
+ const use = retrievalUse(
81
+ db,
82
+ selectRuntimePartition(db, caller.identity, {
83
+ fingerprint: caller.runtime,
84
+ label: caller.label,
85
+ })
86
+ );
87
+ if (use.kind === "vectors")
88
+ return {
89
+ label: caller.label,
90
+ state: "vectors",
91
+ partition: identityPartitionId(use.identity),
92
+ };
93
+ return use.kind === "unavailable"
94
+ ? {
95
+ label: caller.label,
96
+ state: "unavailable",
97
+ partition: null,
98
+ reason: use.reason,
99
+ }
100
+ : { label: caller.label, state: "unresolved", partition: null };
101
+ }
102
+
103
+ /** The partition status counts: the caller's retrieval partition, else the fallback. */
104
+ function countedPartition(
105
+ db: Database,
106
+ model: string,
107
+ candidates: Partition[],
108
+ selection: string | undefined
109
+ ): Partition | undefined {
110
+ const { partition } = vectorRuntimeStatus(db, model);
111
+ return (
112
+ candidates.find((p) => p.partition_id === partition) ??
113
+ retrievalPartition(db, candidates, selection)
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Fallback for a caller that has not resolved yet: the activated primary first,
119
+ * then an activated confirmed fork, then an activated legacy one, so an
120
+ * incomplete shadow never reads as lost embeddings. The last embedding
121
+ * selection, then coverage, breaks ties within a tier.
122
+ */
123
+ function retrievalPartition(
124
+ db: Database,
125
+ candidates: Partition[],
126
+ selection: string | undefined
127
+ ): Partition | undefined {
128
+ for (const tier of [
129
+ candidates.filter((p) => activated(p) && !p.legacy && !p.fork),
130
+ candidates.filter((p) => activated(p) && !p.legacy),
131
+ candidates.filter(activated),
132
+ ]) {
133
+ if (!tier.length) continue;
134
+ const selected = tier.find((p) => p.partition_id === selection);
135
+ if (selected) return selected;
136
+ let best = tier[0]!;
137
+ let bestOwners = currentOwnerCount(db, best.partition_id);
138
+ for (const partition of tier.slice(1)) {
139
+ const owners = currentOwnerCount(db, partition.partition_id);
140
+ if (owners > bestOwners) [best, bestOwners] = [partition, owners];
141
+ }
142
+ return best;
143
+ }
144
+ return selection === undefined
145
+ ? candidates.length === 1
146
+ ? candidates[0]
147
+ : undefined
148
+ : candidates.find((p) => p.partition_id === selection);
149
+ }
150
+
151
+ function readPartitions(db: Database, model: string | null): Partition[] {
152
+ return db
153
+ .query<Partition, [string | null, string | null]>(`
154
+ SELECT partition_id, version, model, fingerprint, dimensions, state,
155
+ activated_epoch, legacy, provenance, fork
156
+ FROM vector_partitions WHERE (? IS NULL OR model = ?)
157
+ ORDER BY model, partition_id
158
+ `)
159
+ .all(model, model);
160
+ }
161
+
162
+ function readSelections(db: Database): Map<string, string> {
163
+ return new Map(
164
+ db
165
+ .query<{ key: string; value: string }, [string]>(
166
+ "SELECT key, value FROM schema_meta WHERE key GLOB ?"
167
+ )
168
+ .all(`${SELECTED_VECTOR_PARTITION_PREFIX}*`)
169
+ .map((row) => [
170
+ row.key.slice(SELECTED_VECTOR_PARTITION_PREFIX.length),
171
+ row.value,
172
+ ])
173
+ );
174
+ }
175
+
176
+ function hasPartitionTable(db: Database): boolean {
177
+ return !!db
178
+ .query(
179
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'vector_partitions'"
180
+ )
181
+ .get();
182
+ }
183
+
184
+ /** Every partition with its state, owners and provenance (R4). */
185
+ export function listVectorPartitions(
186
+ db: Database,
187
+ model?: string
188
+ ): VectorPartitionStatus[] {
189
+ return db.transaction(() => {
190
+ if (!hasPartitionTable(db)) return [];
191
+ const partitions = readPartitions(db, model ?? null);
192
+ const retrieval = new Set<string>();
193
+ for (const name of new Set(partitions.map((p) => p.model))) {
194
+ const { partition } = vectorRuntimeStatus(db, name);
195
+ if (partition) retrieval.add(partition);
196
+ }
197
+ const runtimes = db.prepare<{ label: string }, [string, string]>(
198
+ "SELECT DISTINCT label FROM vector_runtime_verdicts WHERE partition_id = ? AND verdict = ? ORDER BY label"
199
+ );
200
+ return partitions.map(
201
+ (p): VectorPartitionStatus => ({
202
+ id: p.partition_id,
203
+ model: p.model,
204
+ dimensions: p.dimensions,
205
+ state: activated(p) ? "active" : "shadow",
206
+ legacy: p.legacy === 1,
207
+ retrieval: retrieval.has(p.partition_id),
208
+ // Shadows only (legacy shadows included). An activated partition may be
209
+ // another runtime's retrieval partition, or a legacy one awaiting its
210
+ // measured re-key: possibly the only copy of its vectors.
211
+ droppable: !retrieval.has(p.partition_id) && !activated(p),
212
+ owners: currentOwnerCount(db, p.partition_id),
213
+ provenance:
214
+ p.provenance ??
215
+ (p.legacy ? "unrecorded (pre-fn-184 key)" : "unrecorded"),
216
+ compatibleRuntimes: runtimes
217
+ .all(p.partition_id, "compatible")
218
+ .map((row) => row.label),
219
+ incompatibleRuntimes: runtimes
220
+ .all(p.partition_id, "incompatible")
221
+ .map((row) => row.label),
222
+ })
223
+ );
224
+ })();
19
225
  }
20
226
 
21
227
  interface OwnerCoverage {
@@ -37,31 +243,9 @@ export function getVariantStatus(
37
243
  options?: { embedModel?: string; embedFingerprint?: string }
38
244
  ): { backlog: number; embeddedByCollection: Map<string, number> } | null {
39
245
  return db.transaction(() => {
40
- if (
41
- !db
42
- .query(
43
- "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'vector_partitions'"
44
- )
45
- .get()
46
- )
47
- return null;
48
- const partitions = db
49
- .query<Partition, [string | null, string | null]>(`
50
- SELECT partition_id, version, model, fingerprint, dimensions, state, activated_epoch
51
- FROM vector_partitions WHERE (? IS NULL OR model = ?)
52
- `)
53
- .all(options?.embedModel ?? null, options?.embedModel ?? null);
54
- const selections = db
55
- .query<{ key: string; value: string }, [string]>(
56
- "SELECT key, value FROM schema_meta WHERE key GLOB ?"
57
- )
58
- .all(`${SELECTED_VECTOR_PARTITION_PREFIX}*`);
59
- const selected = new Map(
60
- selections.map((row) => [
61
- row.key.slice(SELECTED_VECTOR_PARTITION_PREFIX.length),
62
- row.value,
63
- ])
64
- );
246
+ if (!hasPartitionTable(db)) return null;
247
+ const partitions = readPartitions(db, options?.embedModel ?? null);
248
+ const selected = readSelections(db);
65
249
  const models = new Set(partitions.map((p) => p.model));
66
250
  if (options?.embedModel) models.add(options.embedModel);
67
251
  else for (const model of selected.keys()) models.add(model);
@@ -70,20 +254,12 @@ export function getVariantStatus(
70
254
  for (const model of models) {
71
255
  const candidates = partitions.filter((p) => p.model === model);
72
256
  const selection = selected.get(model);
73
- const activated = candidates.some(
74
- (p) => p.state === "active" && p.activated_epoch !== null
75
- );
76
- if (selection === undefined && !activated) continue;
257
+ if (selection === undefined && !candidates.some(activated)) continue;
77
258
  authoritativeModels.add(model);
78
259
  // Resolve one persisted identity per model, never combine alternative
79
260
  // partitions of the same model. Unscoped status may accept any model.
80
261
  // Stale epochs do not revoke owners whose current inputs still match.
81
- const partition =
82
- selection === undefined
83
- ? candidates.length === 1
84
- ? candidates[0]
85
- : undefined
86
- : candidates.find((p) => p.partition_id === selection);
262
+ const partition = countedPartition(db, model, candidates, selection);
87
263
  if (
88
264
  partition &&
89
265
  partition.version === 1 &&
@@ -194,3 +370,68 @@ export function getVariantStatus(
194
370
  return { backlog, embeddedByCollection };
195
371
  })();
196
372
  }
373
+
374
+ const MIN_PARTITION_PREFIX = 8;
375
+
376
+ type DropResult =
377
+ | { ok: true; partition: VectorPartitionStatus }
378
+ | { ok: false; error: string };
379
+
380
+ /**
381
+ * Remove an abandoned shadow partition (legacy included) this caller's retrieval does
382
+ * not use, with its vectors, owners and verdicts. Status prints its hint for
383
+ * exactly the same `droppable` partitions.
384
+ */
385
+ export async function dropVectorPartition(
386
+ db: Database,
387
+ idPrefix: string
388
+ ): Promise<DropResult> {
389
+ if (idPrefix.length < MIN_PARTITION_PREFIX)
390
+ return {
391
+ ok: false,
392
+ error: `Give at least ${MIN_PARTITION_PREFIX} characters of the partition id (see \`gno status\`)`,
393
+ };
394
+ const vecLoaded = await loadSqliteVec(db);
395
+ return db
396
+ .transaction((): DropResult => {
397
+ const matches = listVectorPartitions(db).filter((p) =>
398
+ p.id.startsWith(idPrefix)
399
+ );
400
+ const [partition] = matches;
401
+ if (!partition || matches.length > 1)
402
+ return {
403
+ ok: false,
404
+ error: matches.length
405
+ ? `Partition id prefix ${idPrefix} is ambiguous`
406
+ : `No vector partition ${idPrefix}`,
407
+ };
408
+ if (!partition.droppable)
409
+ return {
410
+ ok: false,
411
+ error: partition.retrieval
412
+ ? `Refusing to drop partition ${partition.id.slice(0, 12)}: this runtime's retrieval uses it`
413
+ : `Refusing to drop active partition ${partition.id.slice(0, 12)}: other runtimes may read it, or it awaits its one-time re-key`,
414
+ };
415
+ const table = `vec_v1_${partition.id}`;
416
+ if (
417
+ !vecLoaded &&
418
+ db.query("SELECT 1 FROM sqlite_master WHERE name = ?").get(table)
419
+ )
420
+ return {
421
+ ok: false,
422
+ error:
423
+ "sqlite-vec is unavailable; cannot drop the vector index table",
424
+ };
425
+ for (const statement of [
426
+ "DELETE FROM vector_owners WHERE partition_id = ?",
427
+ "DELETE FROM vector_variants WHERE partition_id = ?",
428
+ "DELETE FROM vector_runtime_verdicts WHERE partition_id = ?",
429
+ "DELETE FROM vector_partitions WHERE partition_id = ?",
430
+ `DELETE FROM schema_meta WHERE key GLOB '${SELECTED_VECTOR_PARTITION_PREFIX}*' AND value = ?`,
431
+ ])
432
+ db.run(statement, [partition.id]);
433
+ db.exec(`DROP TABLE IF EXISTS ${table}`);
434
+ return { ok: true, partition };
435
+ })
436
+ .immediate();
437
+ }
@@ -14,6 +14,8 @@ export interface VectorVariantIdentity {
14
14
  contextSize: number;
15
15
  truncationPolicy: string;
16
16
  dimensions: number;
17
+ /** Runtime fingerprint of an explicitly confirmed separate partition. */
18
+ fork?: string;
17
19
  }
18
20
 
19
21
  /** Snapshot carried through asynchronous embedding; write revalidates input. */
@@ -2,34 +2,84 @@ import type { Database } from "bun:sqlite";
2
2
 
3
3
  import type { EmbeddingPort } from "../../llm/types";
4
4
  import type {
5
+ VectorIndexPort,
5
6
  VectorSearchOptions,
6
7
  VectorSearchResult,
7
8
  VectorVariantIdentity,
8
9
  } from "./types";
9
10
 
10
- import { getVariantModelFingerprint } from "../../embed/fingerprint";
11
11
  import { formatDocForEmbedding } from "../../pipeline/contextual";
12
12
  import { buildEligibleDocumentQuery } from "../sqlite/eligibility";
13
- import { encodeEmbedding } from "./sqlite-vec";
14
- import { embeddingInputHash, vectorVariantFingerprint } from "./variants";
13
+ import {
14
+ embeddingPartitionIdentity,
15
+ identityPartitionId,
16
+ resolveRuntimePartition,
17
+ retrievalUse,
18
+ } from "./runtime-compat";
19
+ import { encodeEmbedding, getVectorIndexDatabase } from "./sqlite-vec";
20
+ import { embeddingInputHash } from "./variants";
15
21
 
16
- /** Call after query embedding initialized the port; never infer runtime policy. */
17
- export function resolveVectorSearchIdentity(
18
- port: EmbeddingPort
19
- ): VectorVariantIdentity | undefined {
20
- const identity = port.getIdentity?.();
21
- if (!identity) return undefined;
22
- const dimensions = port.dimensions();
23
- return {
24
- model: port.modelUri,
25
- modelFingerprint: getVariantModelFingerprint(
26
- { modelUri: port.modelUri, dimensions },
27
- identity
28
- ),
29
- contextSize: identity.contextSize,
30
- truncationPolicy: identity.truncationPolicy,
31
- dimensions,
32
- };
22
+ /** Fallback/warning code: this runtime may not read the activated vectors. */
23
+ export const VECTOR_RUNTIME_INCOMPATIBLE = "vector_runtime_incompatible";
24
+
25
+ export type SearchPartition =
26
+ | { identity?: VectorVariantIdentity; unavailable?: undefined }
27
+ | {
28
+ identity?: undefined;
29
+ /** Why this runtime cannot read vectors; callers phrase the fallback. */
30
+ unavailable: { reason: string; building: boolean };
31
+ };
32
+
33
+ /**
34
+ * Call after query embedding initialized the port. A runtime that cannot use
35
+ * an activated partition gets a reason instead of an identity; vector spaces
36
+ * are never mixed.
37
+ */
38
+ export async function resolveVectorSearchIdentity(
39
+ port: EmbeddingPort,
40
+ vectorIndex: VectorIndexPort
41
+ ): Promise<SearchPartition> {
42
+ const primary = embeddingPartitionIdentity(port);
43
+ const db = getVectorIndexDatabase(vectorIndex);
44
+ if (!primary || !db) return { identity: primary };
45
+ let resolved;
46
+ try {
47
+ resolved = await resolveRuntimePartition(db, port, primary);
48
+ } catch (cause) {
49
+ return {
50
+ unavailable: {
51
+ reason: `the runtime compatibility check failed (${cause instanceof Error ? cause.message : String(cause)})`,
52
+ building: false,
53
+ },
54
+ };
55
+ }
56
+ const use = retrievalUse(
57
+ db,
58
+ resolved.blocked
59
+ ? { kind: "blocked", ...resolved.blocked }
60
+ : { kind: "use", identity: resolved.identity, verdict: resolved.verdict }
61
+ );
62
+ if (use.kind === "unavailable")
63
+ return { unavailable: { reason: use.reason, building: use.building } };
64
+ return { identity: resolved.identity };
65
+ }
66
+
67
+ /** Result notice for hybrid retrieval, which falls back to lexical. */
68
+ export function lexicalFallbackNotice(
69
+ unavailable: NonNullable<SearchPartition["unavailable"]>
70
+ ): string {
71
+ return `Semantic search unavailable for this runtime: ${unavailable.reason}. Using lexical retrieval only; ${unavailable.building ? "finish it with `gno embed`" : "see `gno status`"}.`;
72
+ }
73
+
74
+ /** vsearch cannot fall back; it says what the caller can do instead. */
75
+ export function vectorSearchUnavailableMessage(
76
+ unavailable: NonNullable<SearchPartition["unavailable"]>
77
+ ): string {
78
+ return `Vector search unavailable for this runtime: ${unavailable.reason}. ${
79
+ unavailable.building
80
+ ? "Finish the partition with `gno embed`, or use `gno query` / `gno search`."
81
+ : "Use `gno query` or `gno search`, or build a separate partition with `gno embed --new-partition`."
82
+ }`;
33
83
  }
34
84
 
35
85
  /** Null means legacy authority. Once promoted, unavailable provenance fails closed. */
@@ -66,9 +116,7 @@ function searchVectorVariantsInSnapshot(
66
116
  embedding.length !== dimensions
67
117
  )
68
118
  throw new Error("Query embedding identity does not match vector index");
69
- const partitionId = embeddingInputHash(
70
- JSON.stringify([model, vectorVariantFingerprint(identity), dimensions])
71
- );
119
+ const partitionId = identityPartitionId(identity);
72
120
  const partition = db
73
121
  .query<{ state: string; activated_epoch: number | null }, [string]>(
74
122
  "SELECT state, activated_epoch FROM vector_partitions WHERE partition_id = ?"
@@ -22,10 +22,30 @@ export function vectorVariantFingerprint(
22
22
  identity.modelFingerprint,
23
23
  identity.contextSize,
24
24
  identity.truncationPolicy,
25
+ ...(identity.fork ? [identity.fork] : []),
25
26
  ])
26
27
  );
27
28
  }
28
29
 
30
+ export function vectorPartitionId(
31
+ model: string,
32
+ fingerprint: string,
33
+ dimensions: number
34
+ ): string {
35
+ return embeddingInputHash(JSON.stringify([model, fingerprint, dimensions]));
36
+ }
37
+
38
+ /** Storage stays usable without sqlite-vec; search and activation need it. */
39
+ export async function loadSqliteVec(db: Database): Promise<boolean> {
40
+ try {
41
+ const sqliteVec = await import("sqlite-vec");
42
+ sqliteVec.load(db);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
29
49
  interface OwnerRow {
30
50
  documentId: number;
31
51
  mirrorHash: string;
@@ -49,7 +69,9 @@ export class VectorVariantStore {
49
69
  constructor(
50
70
  private readonly db: Database,
51
71
  identity: VectorVariantIdentity,
52
- readonly searchAvailable: boolean
72
+ readonly searchAvailable: boolean,
73
+ /** Readable label of the runtime that creates the partition. */
74
+ provenance?: string
53
75
  ) {
54
76
  if (
55
77
  !identity.model ||
@@ -64,19 +86,26 @@ export class VectorVariantStore {
64
86
  }
65
87
  this.identity = Object.freeze({ ...identity });
66
88
  this.fingerprint = vectorVariantFingerprint(identity);
67
- this.partitionId = embeddingInputHash(
68
- JSON.stringify([identity.model, this.fingerprint, identity.dimensions])
89
+ this.partitionId = vectorPartitionId(
90
+ identity.model,
91
+ this.fingerprint,
92
+ identity.dimensions
69
93
  );
70
94
  this.tableName = `vec_v1_${this.partitionId}`;
71
95
  db.transaction(() => {
72
96
  db.run(
73
97
  `INSERT OR IGNORE INTO vector_partitions
74
- (partition_id, version, model, fingerprint, dimensions) VALUES (?, 1, ?, ?, ?)`,
98
+ (partition_id, version, model, fingerprint, dimensions, provenance,
99
+ base_fingerprint, fork)
100
+ VALUES (?, 1, ?, ?, ?, ?, ?, ?)`,
75
101
  [
76
102
  this.partitionId,
77
103
  identity.model,
78
104
  this.fingerprint,
79
105
  identity.dimensions,
106
+ provenance ?? null,
107
+ vectorVariantFingerprint({ ...identity, fork: undefined }),
108
+ identity.fork ?? null,
80
109
  ]
81
110
  );
82
111
  if (searchAvailable) {
@@ -349,6 +378,14 @@ export class VectorVariantStore {
349
378
  `UPDATE vector_partitions SET state = 'active', activated_epoch = ? WHERE partition_id = ?`,
350
379
  [expectedEpoch, this.partitionId]
351
380
  );
381
+ // A complete runtime-independent partition supersedes the pre-fn-184
382
+ // partitions of its vector space, as the one-time re-key does.
383
+ if (!this.identity.fork)
384
+ this.db.run(
385
+ `UPDATE vector_partitions SET state = 'shadow', activated_epoch = NULL
386
+ WHERE legacy = 1 AND model = ? AND dimensions = ?`,
387
+ [this.identity.model, this.identity.dimensions]
388
+ );
352
389
  })
353
390
  .immediate();
354
391
  }
@@ -448,15 +485,13 @@ export class VectorVariantStore {
448
485
 
449
486
  export async function createVectorVariantStore(
450
487
  db: Database,
451
- identity: VectorVariantIdentity
488
+ identity: VectorVariantIdentity,
489
+ provenance?: string
452
490
  ): Promise<VectorVariantStore> {
453
- let searchAvailable = false;
454
- try {
455
- const sqliteVec = await import("sqlite-vec");
456
- sqliteVec.load(db);
457
- searchAvailable = true;
458
- } catch {
459
- // Storage remains usable offline; activation waits for a materialized index.
460
- }
461
- return new VectorVariantStore(db, identity, searchAvailable);
491
+ return new VectorVariantStore(
492
+ db,
493
+ identity,
494
+ await loadSqliteVec(db),
495
+ provenance
496
+ );
462
497
  }
@@ -1 +0,0 @@
1
- 80268befd10f0f0d79796dacb845d840b3b3e691d265bdb17e54f3e86b5209ff gno-browser-clipper-v2.6.0.zip