@gmickel/gno 2.5.1 → 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 +60 -5
- package/assets/skill/README.md +5 -1
- package/assets/skill/SKILL.md +80 -4
- package/assets/skill/cli-reference.md +132 -2
- package/assets/skill/examples.md +30 -0
- package/assets/skill/mcp-reference.md +54 -1
- package/assets/skill/recipes/capture-and-file.md +6 -0
- package/assets/skill/recipes/memory-file-decision.md +6 -0
- package/assets/skill/recipes/memory-supersede-fact.md +5 -0
- package/assets/skill/recipes/session-evidence-lookup.md +98 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.5.1.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 +2 -1
- package/spec/cli.md +449 -35
- package/spec/mcp.md +234 -4
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +4 -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 +4 -2
- package/spec/output-schemas/memory-remember.schema.json +10 -4
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/request-status.schema.json +113 -0
- 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/sessions-automation-run.schema.json +46 -0
- package/spec/output-schemas/sessions-discovery.schema.json +38 -0
- package/spec/output-schemas/sessions-import-receipt.schema.json +156 -0
- package/spec/output-schemas/sessions-status.schema.json +432 -0
- package/spec/output-schemas/status.schema.json +98 -0
- package/src/cli/commands/ask.ts +14 -2
- package/src/cli/commands/capture.ts +55 -96
- package/src/cli/commands/daemon.ts +41 -0
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +3 -0
- package/src/cli/commands/memory.ts +12 -3
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/request-status.ts +59 -0
- package/src/cli/commands/reset.ts +39 -5
- package/src/cli/commands/sessions.ts +713 -0
- package/src/cli/commands/shared.ts +14 -1
- 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 +441 -2
- package/src/cli/session-binding.ts +49 -0
- package/src/config/types.ts +8 -0
- package/src/core/capture-publish.ts +239 -0
- package/src/core/capture-sync.ts +12 -2
- package/src/core/host-paths.ts +31 -0
- package/src/core/memory-remember.ts +234 -122
- package/src/core/memory-types.ts +11 -0
- package/src/core/network-boundary-inventory.ts +8 -0
- package/src/core/request-receipts.ts +671 -0
- 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 +30 -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 +17 -0
- package/src/mcp/http-egress.ts +4 -0
- package/src/mcp/http-transport.ts +2 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +87 -85
- package/src/mcp/tools/index.ts +77 -4
- package/src/mcp/tools/memory-remember.ts +8 -1
- package/src/mcp/tools/memory-shared.ts +7 -1
- package/src/mcp/tools/request-status.ts +73 -0
- package/src/mcp/tools/sessions.ts +208 -0
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/sdk/client.ts +180 -84
- package/src/sdk/index.ts +6 -0
- package/src/sdk/types.ts +54 -2
- package/src/serve/capture-service.ts +98 -32
- package/src/serve/config-sync.ts +3 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +79 -0
- package/src/serve/public/app.tsx +4 -1
- package/src/serve/public/components/CaptureModal.tsx +26 -8
- package/src/serve/public/components/sessions/AutomationPanel.tsx +800 -0
- package/src/serve/public/components/sessions/ImportReceipt.tsx +238 -0
- package/src/serve/public/components/sessions/SessionSearch.tsx +286 -0
- package/src/serve/public/components/sessions/SourcesPanel.tsx +541 -0
- package/src/serve/public/components/sessions/api.ts +40 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +26 -3
- package/src/serve/public/lib/request-intent.ts +77 -0
- package/src/serve/public/lib/snippet.tsx +52 -0
- package/src/serve/public/lib/workspace-actions.ts +12 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Dashboard.tsx +22 -9
- package/src/serve/public/pages/DocView.tsx +25 -6
- package/src/serve/public/pages/DocumentEditor.tsx +224 -104
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/public/pages/Sessions.tsx +350 -0
- package/src/serve/resident-runtime.ts +69 -4
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/routes/api.ts +476 -147
- package/src/serve/routes/sessions.ts +766 -0
- package/src/serve/security.ts +9 -0
- package/src/serve/server.ts +215 -10
- package/src/serve/session-automation.ts +146 -0
- 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/sessions/archive.ts +348 -0
- package/src/sessions/automation-state.ts +444 -0
- package/src/sessions/automation-status.ts +239 -0
- package/src/sessions/automation.ts +1169 -0
- package/src/sessions/binding.ts +105 -0
- package/src/sessions/claude-hook.ts +240 -0
- package/src/sessions/config.ts +176 -0
- package/src/sessions/format.ts +191 -0
- package/src/sessions/import-child-env.ts +8 -0
- package/src/sessions/import-child.ts +152 -0
- package/src/sessions/parsers/claude-code.ts +259 -0
- package/src/sessions/parsers/codex.ts +303 -0
- package/src/sessions/parsers/hermes.ts +248 -0
- package/src/sessions/parsers/openclaw.ts +496 -0
- package/src/sessions/parsers/shared.ts +184 -0
- package/src/sessions/sanitize.ts +222 -0
- package/src/sessions/service.ts +1533 -0
- package/src/sessions/setup.ts +477 -0
- package/src/sessions/sources.ts +518 -0
- package/src/sessions/state.ts +118 -0
- package/src/sessions/types.ts +457 -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 +76 -16
- package/src/store/sqlite/scoped-index.ts +9 -0
- 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.5.1.zip.sha256 +0 -1
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measured runtime compatibility for vector partitions (fn-184).
|
|
3
|
+
*
|
|
4
|
+
* Partition identity is model weights + formatter + dimensions + context and
|
|
5
|
+
* truncation policy. A runtime (Bun, native binding, backend, threads) may use
|
|
6
|
+
* a partition only after re-embedding a small sample of its stored chunks
|
|
7
|
+
* reproduces the stored vectors. The verdict is cached per (partition, runtime).
|
|
8
|
+
*
|
|
9
|
+
* @module src/store/vector/runtime-compat
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Database } from "bun:sqlite";
|
|
13
|
+
|
|
14
|
+
import type { EmbeddingPort } from "../../llm/types";
|
|
15
|
+
import type { VectorVariantIdentity } from "./types";
|
|
16
|
+
|
|
17
|
+
import { getVariantModelFingerprint } from "../../embed/fingerprint";
|
|
18
|
+
import { runtimeCallerKey } from "../../llm/native-worker/embedding-identity";
|
|
19
|
+
import { formatDocForEmbedding } from "../../pipeline/contextual";
|
|
20
|
+
import { decodeEmbedding } from "./sqlite-vec";
|
|
21
|
+
import {
|
|
22
|
+
embeddingInputHash,
|
|
23
|
+
loadSqliteVec,
|
|
24
|
+
SELECTED_VECTOR_PARTITION_PREFIX,
|
|
25
|
+
vectorPartitionId,
|
|
26
|
+
vectorVariantFingerprint,
|
|
27
|
+
} from "./variants";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Every sampled chunk must reach this cosine. Measured (spec fn-184): Bun
|
|
31
|
+
* version and CPU threads were bit-identical, GPU vs CPU min 0.99937, two
|
|
32
|
+
* different chunks of one corpus reached 0.951.
|
|
33
|
+
*/
|
|
34
|
+
export const RUNTIME_MIN_COSINE = 0.99;
|
|
35
|
+
export const RUNTIME_SAMPLE_SIZE = 8;
|
|
36
|
+
|
|
37
|
+
export type RuntimeVerdict = "compatible" | "incompatible" | "unverified";
|
|
38
|
+
|
|
39
|
+
interface VerdictRow {
|
|
40
|
+
verdict: "compatible" | "incompatible";
|
|
41
|
+
min_cosine: number;
|
|
42
|
+
samples: number;
|
|
43
|
+
sample_ms: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Runtime {
|
|
47
|
+
fingerprint: string;
|
|
48
|
+
label: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RuntimePartition {
|
|
52
|
+
/** Partition this runtime reads and writes. */
|
|
53
|
+
identity: VectorVariantIdentity;
|
|
54
|
+
verdict: RuntimeVerdict;
|
|
55
|
+
/**
|
|
56
|
+
* Set when this runtime must not use `identity`: building `separate` is a
|
|
57
|
+
* new partition and needs explicit confirmation.
|
|
58
|
+
*/
|
|
59
|
+
blocked?: { reason: string; separate: VectorVariantIdentity };
|
|
60
|
+
/** Measured embedding cost from the compatibility sample, when one ran. */
|
|
61
|
+
msPerChunk?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Effective partition identity of an initialized verified port. */
|
|
65
|
+
export function embeddingPartitionIdentity(
|
|
66
|
+
port: EmbeddingPort
|
|
67
|
+
): VectorVariantIdentity | undefined {
|
|
68
|
+
const identity = port.getIdentity?.();
|
|
69
|
+
if (!identity) return undefined;
|
|
70
|
+
const dimensions = port.dimensions();
|
|
71
|
+
return {
|
|
72
|
+
model: port.modelUri,
|
|
73
|
+
modelFingerprint: getVariantModelFingerprint(
|
|
74
|
+
{ modelUri: port.modelUri, dimensions },
|
|
75
|
+
identity
|
|
76
|
+
),
|
|
77
|
+
contextSize: identity.contextSize,
|
|
78
|
+
truncationPolicy: identity.truncationPolicy,
|
|
79
|
+
dimensions,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function identityPartitionId(identity: VectorVariantIdentity): string {
|
|
84
|
+
return vectorPartitionId(
|
|
85
|
+
identity.model,
|
|
86
|
+
vectorVariantFingerprint(identity),
|
|
87
|
+
identity.dimensions
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function partitionExists(db: Database, partitionId: string): boolean {
|
|
92
|
+
return !!db
|
|
93
|
+
.query("SELECT 1 FROM vector_partitions WHERE partition_id = ?")
|
|
94
|
+
.get(partitionId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Current owners bound to a partition; the completeness measure. */
|
|
98
|
+
export function currentOwnerCount(db: Database, partitionId: string): number {
|
|
99
|
+
return db
|
|
100
|
+
.query<{ count: number }, [string]>(`
|
|
101
|
+
SELECT count(*) AS count FROM vector_owners o
|
|
102
|
+
JOIN documents d ON d.id = o.document_id AND d.active = 1
|
|
103
|
+
AND d.mirror_hash = o.mirror_hash
|
|
104
|
+
WHERE o.partition_id = ?
|
|
105
|
+
`)
|
|
106
|
+
.get(partitionId)!.count;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** First stored variants (by id) whose current owner still yields the stored input. */
|
|
110
|
+
function sampleStored(
|
|
111
|
+
db: Database,
|
|
112
|
+
partitionId: string,
|
|
113
|
+
model: string
|
|
114
|
+
): { input: string; embedding: Float32Array }[] {
|
|
115
|
+
const statement = db.prepare<
|
|
116
|
+
{
|
|
117
|
+
text: string;
|
|
118
|
+
title: string | null;
|
|
119
|
+
input_hash: string;
|
|
120
|
+
embedding: Uint8Array;
|
|
121
|
+
},
|
|
122
|
+
[string]
|
|
123
|
+
>(`
|
|
124
|
+
SELECT c.text, d.title, v.input_hash, v.embedding FROM vector_variants v
|
|
125
|
+
JOIN vector_owners o ON o.partition_id = v.partition_id AND o.variant_id = v.variant_id
|
|
126
|
+
JOIN documents d ON d.id = o.document_id AND d.active = 1 AND d.mirror_hash = o.mirror_hash
|
|
127
|
+
JOIN content_chunks c ON c.mirror_hash = o.mirror_hash AND c.seq = o.seq
|
|
128
|
+
WHERE v.partition_id = ? ORDER BY v.variant_id
|
|
129
|
+
`);
|
|
130
|
+
const sample = new Map<string, { input: string; embedding: Float32Array }>();
|
|
131
|
+
try {
|
|
132
|
+
for (const row of statement.iterate(partitionId)) {
|
|
133
|
+
const input = formatDocForEmbedding(
|
|
134
|
+
row.text,
|
|
135
|
+
row.title ?? undefined,
|
|
136
|
+
model
|
|
137
|
+
);
|
|
138
|
+
if (embeddingInputHash(input) !== row.input_hash) continue;
|
|
139
|
+
sample.set(row.input_hash, {
|
|
140
|
+
input,
|
|
141
|
+
embedding: decodeEmbedding(row.embedding),
|
|
142
|
+
});
|
|
143
|
+
if (sample.size === RUNTIME_SAMPLE_SIZE) break;
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
statement.finalize();
|
|
147
|
+
}
|
|
148
|
+
return [...sample.values()];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cosine(a: ArrayLike<number>, b: ArrayLike<number>): number {
|
|
152
|
+
let dot = 0;
|
|
153
|
+
let aa = 0;
|
|
154
|
+
let bb = 0;
|
|
155
|
+
for (let i = 0; i < a.length; i++) {
|
|
156
|
+
dot += a[i]! * b[i]!;
|
|
157
|
+
aa += a[i]! * a[i]!;
|
|
158
|
+
bb += b[i]! * b[i]!;
|
|
159
|
+
}
|
|
160
|
+
return aa && bb ? dot / Math.sqrt(aa * bb) : 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface Verdict {
|
|
164
|
+
verdict: RuntimeVerdict;
|
|
165
|
+
msPerChunk?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function cachedVerdict(
|
|
169
|
+
db: Database,
|
|
170
|
+
partitionId: string,
|
|
171
|
+
runtime: string
|
|
172
|
+
): Verdict | undefined {
|
|
173
|
+
const cached = db
|
|
174
|
+
.query<VerdictRow, [string, string]>(
|
|
175
|
+
"SELECT verdict, min_cosine, samples, sample_ms FROM vector_runtime_verdicts WHERE partition_id = ? AND runtime = ?"
|
|
176
|
+
)
|
|
177
|
+
.get(partitionId, runtime);
|
|
178
|
+
return cached
|
|
179
|
+
? {
|
|
180
|
+
verdict: cached.verdict,
|
|
181
|
+
msPerChunk: cached.samples
|
|
182
|
+
? cached.sample_ms / cached.samples
|
|
183
|
+
: undefined,
|
|
184
|
+
}
|
|
185
|
+
: undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Re-embed the sample and cache the verdict. Throws only on embedding failure. */
|
|
189
|
+
async function measureVerdict(
|
|
190
|
+
db: Database,
|
|
191
|
+
port: EmbeddingPort,
|
|
192
|
+
partitionId: string,
|
|
193
|
+
model: string,
|
|
194
|
+
runtime: Runtime
|
|
195
|
+
): Promise<Verdict> {
|
|
196
|
+
const sample = sampleStored(db, partitionId, model);
|
|
197
|
+
if (!sample.length) return { verdict: "unverified" };
|
|
198
|
+
const startedAt = performance.now();
|
|
199
|
+
const result = await port.embedBatch(sample.map((entry) => entry.input));
|
|
200
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
201
|
+
const sampleMs = performance.now() - startedAt;
|
|
202
|
+
let minCosine = 1;
|
|
203
|
+
for (const [index, entry] of sample.entries()) {
|
|
204
|
+
const fresh = result.value[index];
|
|
205
|
+
minCosine = Math.min(
|
|
206
|
+
minCosine,
|
|
207
|
+
fresh && fresh.length === entry.embedding.length
|
|
208
|
+
? cosine(fresh, entry.embedding)
|
|
209
|
+
: 0
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const verdict =
|
|
213
|
+
minCosine >= RUNTIME_MIN_COSINE ? "compatible" : "incompatible";
|
|
214
|
+
try {
|
|
215
|
+
db.run(
|
|
216
|
+
`INSERT OR REPLACE INTO vector_runtime_verdicts
|
|
217
|
+
(partition_id, runtime, label, verdict, min_cosine, samples, sample_ms)
|
|
218
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
219
|
+
[
|
|
220
|
+
partitionId,
|
|
221
|
+
runtime.fingerprint,
|
|
222
|
+
runtime.label,
|
|
223
|
+
verdict,
|
|
224
|
+
minCosine,
|
|
225
|
+
sample.length,
|
|
226
|
+
sampleMs,
|
|
227
|
+
]
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
// The measured verdict still applies to this run; the next run re-measures.
|
|
231
|
+
}
|
|
232
|
+
return { verdict, msPerChunk: sampleMs / sample.length };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function verdictFor(
|
|
236
|
+
db: Database,
|
|
237
|
+
port: EmbeddingPort,
|
|
238
|
+
partitionId: string,
|
|
239
|
+
model: string,
|
|
240
|
+
runtime: Runtime
|
|
241
|
+
): Promise<Verdict> {
|
|
242
|
+
return (
|
|
243
|
+
cachedVerdict(db, partitionId, runtime.fingerprint) ??
|
|
244
|
+
measureVerdict(db, port, partitionId, model, runtime)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface LegacyPartition {
|
|
249
|
+
partition_id: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type LegacyOutcome =
|
|
253
|
+
| { kind: "none" }
|
|
254
|
+
| { kind: "rekeyed"; msPerChunk?: number }
|
|
255
|
+
| { kind: "blocked"; reason: string; msPerChunk?: number };
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* One-time migration: re-key the most complete compatible pre-fn-184 partition
|
|
259
|
+
* under the runtime-independent key and keep the others as shadow. Idempotent
|
|
260
|
+
* (re-keyed rows are no longer legacy) and atomic (one immediate transaction).
|
|
261
|
+
*/
|
|
262
|
+
async function rekeyLegacy(
|
|
263
|
+
db: Database,
|
|
264
|
+
port: EmbeddingPort,
|
|
265
|
+
primary: VectorVariantIdentity,
|
|
266
|
+
runtime: Runtime
|
|
267
|
+
): Promise<LegacyOutcome> {
|
|
268
|
+
const legacy = db
|
|
269
|
+
.query<LegacyPartition, [string, number]>(`
|
|
270
|
+
SELECT partition_id
|
|
271
|
+
FROM vector_partitions WHERE legacy = 1 AND model = ? AND dimensions = ?
|
|
272
|
+
ORDER BY partition_id
|
|
273
|
+
`)
|
|
274
|
+
.all(primary.model, primary.dimensions);
|
|
275
|
+
if (!legacy.length) return { kind: "none" };
|
|
276
|
+
const compatible: (LegacyPartition & { owners: number })[] = [];
|
|
277
|
+
let msPerChunk: number | undefined;
|
|
278
|
+
for (const partition of legacy) {
|
|
279
|
+
const measured = await verdictFor(
|
|
280
|
+
db,
|
|
281
|
+
port,
|
|
282
|
+
partition.partition_id,
|
|
283
|
+
primary.model,
|
|
284
|
+
runtime
|
|
285
|
+
);
|
|
286
|
+
msPerChunk ??= measured.msPerChunk;
|
|
287
|
+
if (measured.verdict === "compatible")
|
|
288
|
+
compatible.push({
|
|
289
|
+
...partition,
|
|
290
|
+
owners: currentOwnerCount(db, partition.partition_id),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
compatible.sort((a, b) => b.owners - a.owners);
|
|
294
|
+
const [winner, runnerUp] = compatible;
|
|
295
|
+
if (!winner)
|
|
296
|
+
return {
|
|
297
|
+
kind: "blocked",
|
|
298
|
+
reason: `none of the ${legacy.length} existing partition(s) reproduces its stored vectors under ${runtime.label}`,
|
|
299
|
+
msPerChunk,
|
|
300
|
+
};
|
|
301
|
+
if (runnerUp?.owners === winner.owners)
|
|
302
|
+
return {
|
|
303
|
+
kind: "blocked",
|
|
304
|
+
reason: `ambiguous migration: partitions ${compatible
|
|
305
|
+
.filter((p) => p.owners === winner.owners)
|
|
306
|
+
.map((p) => p.partition_id.slice(0, 12))
|
|
307
|
+
.join(
|
|
308
|
+
", "
|
|
309
|
+
)} are equally complete; all were kept. Build the runtime-independent partition with \`gno embed --new-partition\`; once it activates they become droppable shadows`,
|
|
310
|
+
msPerChunk,
|
|
311
|
+
};
|
|
312
|
+
if (!(await loadSqliteVec(db)))
|
|
313
|
+
return {
|
|
314
|
+
kind: "blocked",
|
|
315
|
+
reason:
|
|
316
|
+
"sqlite-vec is unavailable; existing partitions cannot be re-keyed",
|
|
317
|
+
msPerChunk,
|
|
318
|
+
};
|
|
319
|
+
const newId = identityPartitionId(primary);
|
|
320
|
+
const oldId = winner.partition_id;
|
|
321
|
+
db.transaction(() => {
|
|
322
|
+
// A concurrent process may have finished the migration first.
|
|
323
|
+
if (
|
|
324
|
+
partitionExists(db, newId) ||
|
|
325
|
+
!db
|
|
326
|
+
.query(
|
|
327
|
+
"SELECT 1 FROM vector_partitions WHERE partition_id = ? AND legacy = 1"
|
|
328
|
+
)
|
|
329
|
+
.get(oldId)
|
|
330
|
+
)
|
|
331
|
+
return;
|
|
332
|
+
db.run("PRAGMA defer_foreign_keys = ON");
|
|
333
|
+
db.run(
|
|
334
|
+
`INSERT INTO vector_partitions
|
|
335
|
+
(partition_id, version, model, fingerprint, dimensions, state, activated_epoch, provenance, legacy, base_fingerprint)
|
|
336
|
+
SELECT ?, version, model, ?, dimensions, state, activated_epoch, provenance, 0, ?
|
|
337
|
+
FROM vector_partitions WHERE partition_id = ?`,
|
|
338
|
+
[
|
|
339
|
+
newId,
|
|
340
|
+
vectorVariantFingerprint(primary),
|
|
341
|
+
vectorVariantFingerprint(primary),
|
|
342
|
+
oldId,
|
|
343
|
+
]
|
|
344
|
+
);
|
|
345
|
+
for (const table of [
|
|
346
|
+
"vector_variants",
|
|
347
|
+
"vector_owners",
|
|
348
|
+
"vector_runtime_verdicts",
|
|
349
|
+
])
|
|
350
|
+
db.run(`UPDATE ${table} SET partition_id = ? WHERE partition_id = ?`, [
|
|
351
|
+
newId,
|
|
352
|
+
oldId,
|
|
353
|
+
]);
|
|
354
|
+
db.run("UPDATE schema_meta SET value = ? WHERE key GLOB ? AND value = ?", [
|
|
355
|
+
newId,
|
|
356
|
+
`${SELECTED_VECTOR_PARTITION_PREFIX}*`,
|
|
357
|
+
oldId,
|
|
358
|
+
]);
|
|
359
|
+
db.run("DELETE FROM vector_partitions WHERE partition_id = ?", [oldId]);
|
|
360
|
+
db.run(
|
|
361
|
+
`UPDATE vector_partitions SET state = 'shadow', activated_epoch = NULL
|
|
362
|
+
WHERE legacy = 1 AND model = ? AND dimensions = ?`,
|
|
363
|
+
[primary.model, primary.dimensions]
|
|
364
|
+
);
|
|
365
|
+
// vec0 cannot be renamed; rebuild the materialized index from stored blobs.
|
|
366
|
+
db.exec(`CREATE VIRTUAL TABLE vec_v1_${newId} USING vec0(
|
|
367
|
+
variant_id INTEGER PRIMARY KEY,
|
|
368
|
+
embedding FLOAT[${primary.dimensions}] distance_metric=cosine
|
|
369
|
+
)`);
|
|
370
|
+
db.run(
|
|
371
|
+
`INSERT INTO vec_v1_${newId}(variant_id, embedding)
|
|
372
|
+
SELECT variant_id, embedding FROM vector_variants WHERE partition_id = ?`,
|
|
373
|
+
[newId]
|
|
374
|
+
);
|
|
375
|
+
db.exec(`DROP TABLE IF EXISTS vec_v1_${oldId}`);
|
|
376
|
+
}).immediate();
|
|
377
|
+
return { kind: "rekeyed", msPerChunk };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** What one runtime may do with the partitions stored for its vector space. */
|
|
381
|
+
export type RuntimeSelection =
|
|
382
|
+
| { kind: "use"; identity: VectorVariantIdentity; verdict: RuntimeVerdict }
|
|
383
|
+
| { kind: "blocked"; reason: string; separate: VectorVariantIdentity }
|
|
384
|
+
/** A verdict is missing; only a runtime with a loaded model can measure it. */
|
|
385
|
+
| { kind: "measure"; partitionId: string }
|
|
386
|
+
/** Pre-fn-184 partitions await the one-time measured re-key. */
|
|
387
|
+
| { kind: "legacy" };
|
|
388
|
+
|
|
389
|
+
const IDENTITY_CHANGED =
|
|
390
|
+
"the embedding identity changed (weights, formatter, dimensions, context size or truncation policy)";
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The single partition-selection rule, shared by retrieval, embedding and
|
|
394
|
+
* status: the primary first, then confirmed forks (most complete first); the
|
|
395
|
+
* first compatible partition wins. Reads cached verdicts plus `measured`.
|
|
396
|
+
*/
|
|
397
|
+
export function selectRuntimePartition(
|
|
398
|
+
db: Database,
|
|
399
|
+
primary: VectorVariantIdentity,
|
|
400
|
+
runtime: Runtime,
|
|
401
|
+
measured: ReadonlyMap<string, Verdict> = new Map()
|
|
402
|
+
): RuntimeSelection {
|
|
403
|
+
if (
|
|
404
|
+
!partitionExists(db, identityPartitionId(primary)) &&
|
|
405
|
+
db
|
|
406
|
+
.query(
|
|
407
|
+
"SELECT 1 FROM vector_partitions WHERE legacy = 1 AND model = ? AND dimensions = ? LIMIT 1"
|
|
408
|
+
)
|
|
409
|
+
.get(primary.model, primary.dimensions)
|
|
410
|
+
)
|
|
411
|
+
return { kind: "legacy" };
|
|
412
|
+
const candidates = db
|
|
413
|
+
.query<
|
|
414
|
+
{ partition_id: string; fork: string | null },
|
|
415
|
+
[string, string, number]
|
|
416
|
+
>(`
|
|
417
|
+
SELECT p.partition_id, p.fork FROM vector_partitions p
|
|
418
|
+
WHERE p.legacy = 0 AND p.base_fingerprint = ? AND p.model = ? AND p.dimensions = ?
|
|
419
|
+
ORDER BY p.fork IS NOT NULL,
|
|
420
|
+
(SELECT count(*) FROM vector_owners o WHERE o.partition_id = p.partition_id) DESC,
|
|
421
|
+
p.partition_id
|
|
422
|
+
`)
|
|
423
|
+
.all(vectorVariantFingerprint(primary), primary.model, primary.dimensions);
|
|
424
|
+
let unverified: VectorVariantIdentity | undefined;
|
|
425
|
+
for (const candidate of candidates) {
|
|
426
|
+
const identity = candidate.fork
|
|
427
|
+
? { ...primary, fork: candidate.fork }
|
|
428
|
+
: primary;
|
|
429
|
+
const known =
|
|
430
|
+
measured.get(candidate.partition_id) ??
|
|
431
|
+
cachedVerdict(db, candidate.partition_id, runtime.fingerprint) ??
|
|
432
|
+
(sampleStored(db, candidate.partition_id, primary.model).length
|
|
433
|
+
? undefined
|
|
434
|
+
: { verdict: "unverified" as const });
|
|
435
|
+
if (!known) return { kind: "measure", partitionId: candidate.partition_id };
|
|
436
|
+
if (known.verdict === "compatible")
|
|
437
|
+
return { kind: "use", identity, verdict: "compatible" };
|
|
438
|
+
if (known.verdict === "unverified") unverified ??= identity;
|
|
439
|
+
}
|
|
440
|
+
if (unverified)
|
|
441
|
+
return { kind: "use", identity: unverified, verdict: "unverified" };
|
|
442
|
+
if (candidates.length)
|
|
443
|
+
return {
|
|
444
|
+
kind: "blocked",
|
|
445
|
+
reason: `${runtime.label} does not reproduce the stored vectors (cosine below ${RUNTIME_MIN_COSINE})`,
|
|
446
|
+
separate: { ...primary, fork: runtime.fingerprint },
|
|
447
|
+
};
|
|
448
|
+
// First partition of this vector space: only an index without vectors for
|
|
449
|
+
// the model may start it silently (a changed context size or weights may not).
|
|
450
|
+
return db
|
|
451
|
+
.query("SELECT 1 FROM vector_partitions WHERE model = ? LIMIT 1")
|
|
452
|
+
.get(primary.model)
|
|
453
|
+
? { kind: "blocked", reason: IDENTITY_CHANGED, separate: primary }
|
|
454
|
+
: { kind: "use", identity: primary, verdict: "unverified" };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Resolve the partition an initialized verified runtime may use: run the
|
|
459
|
+
* one-time legacy re-key and any missing measurement, then apply
|
|
460
|
+
* `selectRuntimePartition`. Records the caller so status can apply the same
|
|
461
|
+
* rule without loading a model.
|
|
462
|
+
*/
|
|
463
|
+
export async function resolveRuntimePartition(
|
|
464
|
+
db: Database,
|
|
465
|
+
port: EmbeddingPort,
|
|
466
|
+
primary: VectorVariantIdentity
|
|
467
|
+
): Promise<RuntimePartition> {
|
|
468
|
+
const identity = port.getIdentity?.();
|
|
469
|
+
if (!identity) throw new Error("Verified embedding identity required");
|
|
470
|
+
const runtime: Runtime = {
|
|
471
|
+
fingerprint: identity.runtimeFingerprint,
|
|
472
|
+
label: identity.runtimeLabel ?? "unknown runtime",
|
|
473
|
+
};
|
|
474
|
+
recordRuntimeCaller(db, primary, runtime);
|
|
475
|
+
const measured = new Map<string, Verdict>();
|
|
476
|
+
let msPerChunk: number | undefined;
|
|
477
|
+
let rekeyed = false;
|
|
478
|
+
for (;;) {
|
|
479
|
+
const selection = selectRuntimePartition(db, primary, runtime, measured);
|
|
480
|
+
if (selection.kind === "use")
|
|
481
|
+
return { identity: selection.identity, verdict: selection.verdict };
|
|
482
|
+
if (selection.kind === "blocked")
|
|
483
|
+
return {
|
|
484
|
+
identity: primary,
|
|
485
|
+
verdict: "incompatible",
|
|
486
|
+
blocked: { reason: selection.reason, separate: selection.separate },
|
|
487
|
+
msPerChunk,
|
|
488
|
+
};
|
|
489
|
+
if (selection.kind === "measure") {
|
|
490
|
+
const verdict = await measureVerdict(
|
|
491
|
+
db,
|
|
492
|
+
port,
|
|
493
|
+
selection.partitionId,
|
|
494
|
+
primary.model,
|
|
495
|
+
runtime
|
|
496
|
+
);
|
|
497
|
+
msPerChunk ??= verdict.msPerChunk;
|
|
498
|
+
measured.set(selection.partitionId, verdict);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const legacy = rekeyed
|
|
502
|
+
? {
|
|
503
|
+
kind: "blocked" as const,
|
|
504
|
+
reason: "the one-time partition migration did not complete; retry",
|
|
505
|
+
msPerChunk,
|
|
506
|
+
}
|
|
507
|
+
: await rekeyLegacy(db, port, primary, runtime);
|
|
508
|
+
rekeyed = true;
|
|
509
|
+
if (legacy.kind === "blocked")
|
|
510
|
+
return {
|
|
511
|
+
identity: primary,
|
|
512
|
+
verdict: "incompatible",
|
|
513
|
+
blocked: { reason: legacy.reason, separate: primary },
|
|
514
|
+
msPerChunk: legacy.msPerChunk,
|
|
515
|
+
};
|
|
516
|
+
// Re-keyed (or raced by another process): select again.
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Status cannot load a model, so every resolving caller records its effective
|
|
522
|
+
* identity under a key status can compute from the process alone. Best effort.
|
|
523
|
+
*/
|
|
524
|
+
function recordRuntimeCaller(
|
|
525
|
+
db: Database,
|
|
526
|
+
primary: VectorVariantIdentity,
|
|
527
|
+
runtime: Runtime
|
|
528
|
+
): void {
|
|
529
|
+
const caller = runtimeCallerKey(primary.model);
|
|
530
|
+
const identity = JSON.stringify(primary);
|
|
531
|
+
try {
|
|
532
|
+
if (
|
|
533
|
+
db
|
|
534
|
+
.query(
|
|
535
|
+
"SELECT 1 FROM vector_runtime_callers WHERE caller = ? AND runtime = ? AND identity = ?"
|
|
536
|
+
)
|
|
537
|
+
.get(caller, runtime.fingerprint, identity)
|
|
538
|
+
)
|
|
539
|
+
return;
|
|
540
|
+
db.run(
|
|
541
|
+
`INSERT OR REPLACE INTO vector_runtime_callers (caller, runtime, label, identity)
|
|
542
|
+
VALUES (?, ?, ?, ?)`,
|
|
543
|
+
[caller, runtime.fingerprint, runtime.label, identity]
|
|
544
|
+
);
|
|
545
|
+
} catch {
|
|
546
|
+
// Status then reports this runtime as unresolved.
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The identity this process last resolved for `model`, if any. */
|
|
551
|
+
export function recordedRuntimeCaller(
|
|
552
|
+
db: Database,
|
|
553
|
+
model: string
|
|
554
|
+
):
|
|
555
|
+
| { runtime: string; label: string; identity: VectorVariantIdentity }
|
|
556
|
+
| undefined {
|
|
557
|
+
const row = db
|
|
558
|
+
.query<{ runtime: string; label: string; identity: string }, [string]>(
|
|
559
|
+
"SELECT runtime, label, identity FROM vector_runtime_callers WHERE caller = ?"
|
|
560
|
+
)
|
|
561
|
+
.get(runtimeCallerKey(model));
|
|
562
|
+
return row
|
|
563
|
+
? {
|
|
564
|
+
runtime: row.runtime,
|
|
565
|
+
label: row.label,
|
|
566
|
+
identity: JSON.parse(row.identity) as VectorVariantIdentity,
|
|
567
|
+
}
|
|
568
|
+
: undefined;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* A runtime that writes into a partition with no samplable vectors defines its
|
|
573
|
+
* reference: verdicts measured against vectors that no longer exist are void.
|
|
574
|
+
* Best effort, like any verdict write (R1).
|
|
575
|
+
*/
|
|
576
|
+
export function recordReferenceRuntime(
|
|
577
|
+
db: Database,
|
|
578
|
+
partitionId: string,
|
|
579
|
+
identity: { runtimeFingerprint: string; runtimeLabel?: string }
|
|
580
|
+
): void {
|
|
581
|
+
try {
|
|
582
|
+
db.transaction(() => {
|
|
583
|
+
db.run("DELETE FROM vector_runtime_verdicts WHERE partition_id = ?", [
|
|
584
|
+
partitionId,
|
|
585
|
+
]);
|
|
586
|
+
db.run(
|
|
587
|
+
`INSERT INTO vector_runtime_verdicts
|
|
588
|
+
(partition_id, runtime, label, verdict, min_cosine, samples, sample_ms)
|
|
589
|
+
VALUES (?, ?, ?, 'compatible', 1, 0, 0)`,
|
|
590
|
+
[
|
|
591
|
+
partitionId,
|
|
592
|
+
identity.runtimeFingerprint,
|
|
593
|
+
identity.runtimeLabel ?? "unknown runtime",
|
|
594
|
+
]
|
|
595
|
+
);
|
|
596
|
+
}).immediate();
|
|
597
|
+
} catch {
|
|
598
|
+
// The next run measures instead.
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export type RetrievalUse =
|
|
603
|
+
/** Search this partition (an unactivated one only before any activation). */
|
|
604
|
+
| { kind: "vectors"; identity: VectorVariantIdentity; activated: boolean }
|
|
605
|
+
/** Vector spaces are never mixed: this runtime has no usable partition. */
|
|
606
|
+
| { kind: "unavailable"; reason: string; building: boolean }
|
|
607
|
+
/** Status only: a verdict or the legacy re-key is still pending. */
|
|
608
|
+
| { kind: "unresolved" };
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* How retrieval uses a selection; shared by search and status so the
|
|
612
|
+
* partition status names is the one this caller's queries read.
|
|
613
|
+
*/
|
|
614
|
+
export function retrievalUse(
|
|
615
|
+
db: Database,
|
|
616
|
+
selection: RuntimeSelection
|
|
617
|
+
): RetrievalUse {
|
|
618
|
+
if (selection.kind === "blocked")
|
|
619
|
+
return { kind: "unavailable", reason: selection.reason, building: false };
|
|
620
|
+
if (selection.kind !== "use") return { kind: "unresolved" };
|
|
621
|
+
const activated = isPartitionActivated(
|
|
622
|
+
db,
|
|
623
|
+
identityPartitionId(selection.identity)
|
|
624
|
+
);
|
|
625
|
+
if (
|
|
626
|
+
!activated &&
|
|
627
|
+
db
|
|
628
|
+
.query(
|
|
629
|
+
"SELECT 1 FROM vector_partitions WHERE model = ? AND state = 'active' AND activated_epoch IS NOT NULL LIMIT 1"
|
|
630
|
+
)
|
|
631
|
+
.get(selection.identity.model)
|
|
632
|
+
)
|
|
633
|
+
return {
|
|
634
|
+
kind: "unavailable",
|
|
635
|
+
reason: "its vector partition is still being built",
|
|
636
|
+
building: true,
|
|
637
|
+
};
|
|
638
|
+
return { kind: "vectors", identity: selection.identity, activated };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Partition authority is durable once activated (see VectorVariantStore.hasActivated). */
|
|
642
|
+
export function isPartitionActivated(
|
|
643
|
+
db: Database,
|
|
644
|
+
partitionId: string
|
|
645
|
+
): boolean {
|
|
646
|
+
return !!db
|
|
647
|
+
.query(
|
|
648
|
+
"SELECT 1 FROM vector_partitions WHERE partition_id = ? AND state = 'active' AND activated_epoch IS NOT NULL"
|
|
649
|
+
)
|
|
650
|
+
.get(partitionId);
|
|
651
|
+
}
|
|
@@ -67,6 +67,23 @@ export function modelTableName(modelUri: string): string {
|
|
|
67
67
|
// Factory
|
|
68
68
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
69
|
|
|
70
|
+
const indexDatabases = new WeakMap<VectorIndexPort, Database>();
|
|
71
|
+
|
|
72
|
+
/** Partition resolution needs the database behind a vector index port. */
|
|
73
|
+
export function getVectorIndexDatabase(
|
|
74
|
+
port: VectorIndexPort
|
|
75
|
+
): Database | undefined {
|
|
76
|
+
return indexDatabases.get(port);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function bindVectorIndexDatabase<T extends VectorIndexPort>(
|
|
80
|
+
port: T,
|
|
81
|
+
db: Database
|
|
82
|
+
): T {
|
|
83
|
+
indexDatabases.set(port, db);
|
|
84
|
+
return port;
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
export interface VectorIndexOptions {
|
|
71
88
|
model: string;
|
|
72
89
|
dimensions: number;
|
|
@@ -224,7 +241,7 @@ export async function createVectorIndexPort(
|
|
|
224
241
|
return Promise.resolve(ok(rows.length));
|
|
225
242
|
}
|
|
226
243
|
|
|
227
|
-
|
|
244
|
+
const port: VectorIndexPort = {
|
|
228
245
|
searchAvailable,
|
|
229
246
|
model,
|
|
230
247
|
dimensions,
|
|
@@ -531,5 +548,6 @@ export async function createVectorIndexPort(
|
|
|
531
548
|
);
|
|
532
549
|
}
|
|
533
550
|
},
|
|
534
|
-
}
|
|
551
|
+
};
|
|
552
|
+
return ok(bindVectorIndexDatabase(port, db));
|
|
535
553
|
}
|