@gmickel/gno 2.6.0 → 2.7.0
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/README.md +1 -1
- package/assets/skill/SKILL.md +5 -3
- package/assets/skill/cli-reference.md +9 -2
- package/assets/skill/mcp-reference.md +2 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.6.0.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +109 -18
- package/spec/mcp.md +36 -2
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +1 -1
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +1 -2
- package/spec/output-schemas/memory-remember.schema.json +2 -2
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +98 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +53 -1
- package/src/core/capture-sync.ts +9 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +4 -3
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/embed/backlog.ts +124 -18
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +15 -10
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +21 -2
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +9 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +1 -3
- package/src/mcp/tools/index.ts +11 -4
- package/src/mcp/tools/memory-remember.ts +1 -1
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +2 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +17 -2
- package/src/serve/public/lib/request-intent.ts +8 -0
- package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
- package/src/serve/public/pages/Dashboard.tsx +12 -9
- package/src/serve/public/pages/DocView.tsx +10 -5
- package/src/serve/public/pages/DocumentEditor.tsx +91 -14
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/resident-runtime.ts +26 -1
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/server.ts +10 -9
- package/src/serve/status-model.ts +16 -0
- package/src/serve/status.ts +2 -0
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +22 -1
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- 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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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 {
|
|
14
|
-
|
|
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
|
-
/**
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 =
|
|
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 =
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|