@gmickel/gno 2.8.0 → 2.8.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 (45) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +5 -2
  3. package/assets/skill/recipes/memory-scoped-recall.md +10 -5
  4. package/assets/spa-production.json.gz +0 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v2.8.0.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/spec/cli.md +41 -6
  10. package/spec/mcp.md +8 -4
  11. package/spec/output-schemas/memory-recall.schema.json +1 -1
  12. package/spec/output-schemas/status.schema.json +3 -3
  13. package/src/cli/commands/graph.ts +3 -1
  14. package/src/cli/commands/links.ts +27 -49
  15. package/src/cli/commands/status.ts +1 -0
  16. package/src/core/audit-links.ts +56 -4
  17. package/src/core/audit-outside-index.ts +215 -0
  18. package/src/core/audit-workspace.ts +13 -7
  19. package/src/core/audit.ts +9 -1
  20. package/src/core/link-inventory-markdown.ts +2 -3
  21. package/src/core/links.ts +40 -17
  22. package/src/core/memory-recall.ts +254 -15
  23. package/src/core/memory-types.ts +12 -0
  24. package/src/core/memory.ts +2 -0
  25. package/src/ingestion/sync.ts +5 -3
  26. package/src/mcp/tools/links.ts +71 -93
  27. package/src/mcp/tools/status.ts +1 -0
  28. package/src/pipeline/search.ts +2 -0
  29. package/src/pipeline/types.ts +4 -0
  30. package/src/sdk/client.ts +1 -0
  31. package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
  32. package/src/serve/routes/graph.ts +3 -1
  33. package/src/serve/routes/links.ts +32 -50
  34. package/src/serve/server.ts +2 -1
  35. package/src/serve/status.ts +1 -0
  36. package/src/store/sqlite/adapter.ts +87 -106
  37. package/src/store/sqlite/graph-link-resolver.ts +7 -0
  38. package/src/store/sqlite/graph-similarity.ts +96 -0
  39. package/src/store/sqlite/workspace-link-resolver.ts +119 -31
  40. package/src/store/types.ts +15 -2
  41. package/src/store/vector/status.ts +27 -0
  42. package/src/store/vector/stored-vectors.ts +158 -0
  43. package/src/store/vector/types.ts +6 -0
  44. package/src/store/vector/variant-search.ts +30 -14
  45. package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +0 -1
@@ -33,11 +33,23 @@ export const MEMORY_RECALL_MAX_TOKENS = 512;
33
33
  /** Retrieval depth per leg before fusion and budgeting. */
34
34
  export const MEMORY_RECALL_RETRIEVAL_LIMIT = 32;
35
35
  export const MEMORY_RRF_K = 60;
36
+ /**
37
+ * Any-term fallback floor: a fact must score at least this fraction of the
38
+ * best fact's raw BM25, so a term shared by most facts cannot pull them in.
39
+ */
40
+ export const MEMORY_RECALL_ANY_TERM_MIN_RELATIVE_SCORE = 0.1;
36
41
  export const MEMORY_DEFAULT_LOCK_WAIT_MS = 120_000;
37
42
  export const MEMORY_TOKEN_BYTES_ESTIMATE = 4;
38
43
 
44
+ /** Recall hint when the scope holds no current fact at all. */
39
45
  export const MEMORY_EMPTY_RECALL_HINT =
40
46
  'No memories in scope yet. Store one with: gno remember "<fact>" --scope <scope> --decision add';
47
+ /** Recall hint when the scope holds facts but none matched the query. */
48
+ export const MEMORY_NO_MATCH_RECALL_HINT =
49
+ 'No memories in scope matched this query. Rephrase with words the fact uses, or store one with: gno remember "<fact>" --scope <scope> --decision add';
50
+ /** Recall hint when facts matched but none fit the token budget. */
51
+ export const MEMORY_OVER_BUDGET_RECALL_HINT =
52
+ "Matching memories did not fit the token budget. Raise --max-tokens (maxTokens) to return them.";
41
53
 
42
54
  // ─────────────────────────────────────────────────────────────────────────────
43
55
  // Errors
@@ -34,6 +34,8 @@ export {
34
34
  MEMORY_CANDIDATE_POOL,
35
35
  MEMORY_EMPTY_RECALL_HINT,
36
36
  MEMORY_LEXICAL_LIKELY_THRESHOLD,
37
+ MEMORY_NO_MATCH_RECALL_HINT,
38
+ MEMORY_OVER_BUDGET_RECALL_HINT,
37
39
  MEMORY_RECALL_MAX_FACTS,
38
40
  MEMORY_RECALL_MAX_TOKENS,
39
41
  MEMORY_SEMANTIC_LIKELY_THRESHOLD,
@@ -57,7 +57,6 @@ import {
57
57
  } from "../core/links";
58
58
  import { extractMemoryScopes } from "../core/memory-record";
59
59
  import { normalizeTag, validateTag } from "../core/tags";
60
- import { TYPED_METADATA_INGEST_VERSION } from "../core/typed-metadata";
61
60
  import { defaultChunker } from "./chunker";
62
61
  import { persistChunkLayout, prepareChunking } from "./chunking";
63
62
  import {
@@ -102,9 +101,12 @@ const MAX_CONCURRENCY = 16;
102
101
  /**
103
102
  * Current ingest schema version.
104
103
  * Increment when ingestion adds new derived data (tags, metadata, etc.)
105
- * Documents with ingestVersion < INGEST_VERSION will be re-processed.
104
+ * or changes how it is parsed. Documents with ingestVersion < INGEST_VERSION
105
+ * will be re-processed. Must stay >= TYPED_METADATA_INGEST_VERSION (tested).
106
+ * 8: wiki links with a table-escaped alias (`[[Note\|Alias]]`) and Markdown
107
+ * link text with square brackets parse the way Obsidian renders them.
106
108
  */
107
- export const INGEST_VERSION = TYPED_METADATA_INGEST_VERSION;
109
+ export const INGEST_VERSION = 8;
108
110
  const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT =
109
111
  fingerprintContentTypeMetadataRules([]);
110
112
  const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
@@ -25,6 +25,12 @@ import { parseRef } from "../../core/ref-parser";
25
25
  import { normalizeCollectionName } from "../../core/validation";
26
26
  import { getActivePreset } from "../../llm/registry";
27
27
  import { createVectorIndexPort } from "../../store/vector";
28
+ import {
29
+ readStoredDocumentVectors,
30
+ resolveStoredVectorSource,
31
+ similarityHitDocuments,
32
+ storedVectorSearchOptions,
33
+ } from "../../store/vector/stored-vectors";
28
34
  import { runTool, type ToolResult } from "./index";
29
35
 
30
36
  // ─────────────────────────────────────────────────────────────────────────────
@@ -409,62 +415,41 @@ export function handleSimilar(
409
415
  const preset = getActivePreset(ctx.config);
410
416
  const modelUri = preset.embed;
411
417
 
412
- // Get stored embeddings from content_vectors (NO model loading required)
418
+ // Stored chunk vectors from the active partition (NO model loading required)
413
419
  const db = ctx.store.getRawDb();
420
+ const source = resolveStoredVectorSource(db, modelUri);
414
421
 
415
- interface VectorRow {
416
- embedding: Uint8Array;
422
+ let vectors: Float32Array[];
423
+ try {
424
+ vectors =
425
+ readStoredDocumentVectors(db, source, [
426
+ { id: doc.id, mirrorHash: doc.mirrorHash },
427
+ ]).get(doc.id) ?? [];
428
+ } catch (e) {
429
+ throw new Error(
430
+ `Invalid stored embedding data: ${e instanceof Error ? e.message : String(e)}`
431
+ );
417
432
  }
418
-
419
- const vectorRows = db
420
- .query<VectorRow, [string, string]>(
421
- `SELECT embedding FROM content_vectors
422
- WHERE mirror_hash = ? AND model = ?
423
- ORDER BY seq`
424
- )
425
- .all(doc.mirrorHash, modelUri);
426
-
427
- if (vectorRows.length === 0) {
433
+ const first = vectors[0];
434
+ if (!first) {
428
435
  throw new Error(
429
436
  `${MCP_ERRORS.NOT_FOUND.code}: Document has no embeddings. Run: gno embed`
430
437
  );
431
438
  }
432
439
 
433
440
  // Compute average embedding from all chunks
434
- const firstBlob = vectorRows[0]?.embedding;
435
- if (!firstBlob) {
436
- throw new Error("No embedding data available");
437
- }
438
-
439
- let dimensions: number;
440
- let avgEmbedding: Float32Array;
441
-
442
- try {
443
- dimensions = firstBlob.byteLength / 4;
444
- avgEmbedding = new Float32Array(dimensions);
445
-
446
- for (const row of vectorRows) {
447
- const blob = new Uint8Array(row.embedding);
448
- const embeddingDims = blob.byteLength / 4;
449
- if (embeddingDims !== dimensions) {
450
- throw new Error(
451
- `Inconsistent embedding dimensions: expected ${dimensions}, got ${embeddingDims}`
452
- );
453
- }
454
- const embedding = new Float32Array(
455
- blob.buffer,
456
- blob.byteOffset,
457
- embeddingDims
441
+ const dimensions = first.length;
442
+ const avgEmbedding = new Float32Array(dimensions);
443
+ for (const embedding of vectors) {
444
+ if (embedding.length !== dimensions) {
445
+ throw new Error(
446
+ `Invalid stored embedding data: Inconsistent embedding dimensions: expected ${dimensions}, got ${embedding.length}`
458
447
  );
459
- for (let i = 0; i < dimensions; i++) {
460
- const current = avgEmbedding[i] ?? 0;
461
- avgEmbedding[i] = current + (embedding[i] ?? 0) / vectorRows.length;
462
- }
463
448
  }
464
- } catch (e) {
465
- throw new Error(
466
- `Invalid stored embedding data: ${e instanceof Error ? e.message : String(e)}`
467
- );
449
+ for (let i = 0; i < dimensions; i++) {
450
+ const current = avgEmbedding[i] ?? 0;
451
+ avgEmbedding[i] = current + (embedding[i] ?? 0) / vectors.length;
452
+ }
468
453
  }
469
454
 
470
455
  // Normalize the average embedding for cosine similarity
@@ -513,73 +498,62 @@ export function handleSimilar(
513
498
  const searchResult = await vectorIndex.searchNearest(
514
499
  avgEmbedding,
515
500
  candidateLimit,
516
- {}
501
+ storedVectorSearchOptions(source)
517
502
  );
518
503
 
519
504
  if (!searchResult.ok) {
520
505
  throw new Error(searchResult.error.message);
521
506
  }
522
507
 
523
- // Get unique mirrorHashes, excluding self
524
- const mirrorHashes = [
525
- ...new Set(
526
- searchResult.value
527
- .filter((r) => r.mirrorHash !== doc.mirrorHash)
528
- .map((r) => r.mirrorHash)
529
- ),
530
- ];
508
+ // Legacy hits (no owners) exclude the source's content, as before
509
+ const hits = searchResult.value.filter(
510
+ (r) => r.documentIds !== undefined || r.mirrorHash !== doc.mirrorHash
511
+ );
531
512
 
532
- // Batch query documents by mirrorHash (avoid N+1)
513
+ // Batch query the hits' documents: exact owners for partition hits,
514
+ // documents sharing the content for legacy hits (avoid N+1)
533
515
  interface DocRow {
516
+ id: number;
534
517
  docid: string;
535
518
  uri: string;
536
519
  title: string | null;
537
520
  collection: string;
538
521
  rel_path: string;
539
- mirror_hash: string;
522
+ mirrorHash: string;
540
523
  }
541
524
 
542
- const placeholders = mirrorHashes.map(() => "?").join(",");
543
- const docRows =
544
- mirrorHashes.length > 0
545
- ? (db
546
- .query<DocRow, string[]>(
547
- `SELECT docid, uri, title, collection, rel_path, mirror_hash
548
- FROM documents WHERE mirror_hash IN (${placeholders}) AND active = 1`
549
- )
550
- .all(...mirrorHashes) as DocRow[])
551
- : [];
552
-
553
- // Build lookup map
554
- const docsByHash = new Map<string, DocRow>();
555
- for (const row of docRows) {
556
- if (!docsByHash.has(row.mirror_hash)) {
557
- docsByHash.set(row.mirror_hash, row);
558
- }
559
- }
560
-
561
- // Build best score per mirrorHash from search results
562
- const scoresByHash = new Map<string, number>();
563
- for (const r of searchResult.value) {
564
- if (r.mirrorHash === doc.mirrorHash) continue;
565
- // Compute similarity score from cosine distance
566
- // sqlite-vec with cosine metric returns distance where similarity = 1 - distance
567
- const score = Math.max(0, Math.min(1, 1 - r.distance));
568
- const existing = scoresByHash.get(r.mirrorHash) ?? 0;
569
- if (score > existing) {
570
- scoresByHash.set(r.mirrorHash, score);
571
- }
572
- }
525
+ const docRows = db
526
+ .query<DocRow, [string, string]>(
527
+ `SELECT id, docid, uri, title, collection, rel_path, mirror_hash AS mirrorHash
528
+ FROM documents WHERE active = 1
529
+ AND (id IN (SELECT value FROM json_each(?))
530
+ OR mirror_hash IN (SELECT value FROM json_each(?)))
531
+ ORDER BY id`
532
+ )
533
+ .all(
534
+ JSON.stringify([
535
+ ...new Set(hits.flatMap((r) => r.documentIds ?? [])),
536
+ ]),
537
+ JSON.stringify([
538
+ ...new Set(
539
+ hits.filter((r) => !r.documentIds).map((r) => r.mirrorHash)
540
+ ),
541
+ ])
542
+ );
573
543
 
574
- // Build similar docs list
544
+ // Build similar docs list; hits are nearest first, so a document's
545
+ // first hit carries its best score
575
546
  const similar: SimilarDocOutput[] = [];
547
+ const seenIds = new Set<number>();
576
548
  const docCollection = doc.collection.toLowerCase();
577
549
 
578
- for (const mirrorHash of mirrorHashes) {
550
+ for (const { document: docRow, distance } of similarityHitDocuments(
551
+ hits,
552
+ docRows
553
+ )) {
579
554
  if (similar.length >= limit) break;
580
-
581
- const docRow = docsByHash.get(mirrorHash);
582
- if (!docRow) continue;
555
+ if (docRow.id === doc.id || seenIds.has(docRow.id)) continue;
556
+ seenIds.add(docRow.id);
583
557
 
584
558
  // Filter by collection if not crossCollection (case-insensitive)
585
559
  if (
@@ -589,7 +563,9 @@ export function handleSimilar(
589
563
  continue;
590
564
  }
591
565
 
592
- const score = scoresByHash.get(mirrorHash) ?? 0;
566
+ // Compute similarity score from cosine distance
567
+ // sqlite-vec with cosine metric returns distance where similarity = 1 - distance
568
+ const score = Math.max(0, Math.min(1, 1 - distance));
593
569
  if (score < threshold) continue;
594
570
 
595
571
  // Get absPath (case-insensitive collection lookup)
@@ -799,6 +775,7 @@ export function handleGraph(
799
775
  threshold: args.threshold ?? 0.7,
800
776
  linkedOnly: args.linkedOnly ?? true,
801
777
  similarTopK: args.similarTopK ?? 5,
778
+ embedModel: getActivePreset(ctx.config).embed,
802
779
  });
803
780
 
804
781
  if (!result.ok) {
@@ -836,6 +813,7 @@ async function getValidatedGraph(
836
813
  threshold: args.threshold ?? 0.7,
837
814
  linkedOnly: args.linkedOnly ?? true,
838
815
  similarTopK: args.similarTopK ?? 5,
816
+ embedModel: getActivePreset(ctx.config).embed,
839
817
  });
840
818
 
841
819
  if (!result.ok) {
@@ -105,6 +105,7 @@ export function handleStatus(
105
105
  const result = await ctx.store.getStatus({
106
106
  embedModel: resolveModelUri(ctx.config, "embed"),
107
107
  chunking: ctx.config.chunking ?? {},
108
+ configuredCollections: ctx.config.collections.map(({ name }) => name),
108
109
  });
109
110
  if (!result.ok) {
110
111
  throw new Error(result.error.message);
@@ -223,6 +223,8 @@ export async function searchBm25(
223
223
  filter: options.filter,
224
224
  memoryScopesAny: options.memoryFilter?.scopes,
225
225
  excludeSuperseded: options.memoryFilter?.excludeSuperseded,
226
+ anyTerm: options.anyTerm,
227
+ minRelativeScore: options.minRelativeScore,
226
228
  });
227
229
 
228
230
  if (!ftsResult.ok) {
@@ -232,6 +232,10 @@ export interface SearchOptions extends InferenceOptions {
232
232
  scopes: string[];
233
233
  excludeSuperseded: boolean;
234
234
  };
235
+ /** Internal: match any positive lexical term (OR) instead of all (AND). */
236
+ anyTerm?: boolean;
237
+ /** Internal: drop hits below this fraction of the best raw BM25 score. */
238
+ minRelativeScore?: number;
235
239
  }
236
240
 
237
241
  /** Structured query mode identifier */
package/src/sdk/client.ts CHANGED
@@ -1509,6 +1509,7 @@ class GnoClientImpl implements GnoClient {
1509
1509
  await this.store.getStatus({
1510
1510
  embedModel: resolveModelUri(this.config, "embed"),
1511
1511
  chunking: this.config.chunking ?? {},
1512
+ configuredCollections: this.config.collections.map(({ name }) => name),
1512
1513
  })
1513
1514
  );
1514
1515
  return {
@@ -16,6 +16,7 @@ import remarkGfm from "remark-gfm";
16
16
  import {
17
17
  normalizeWikiName,
18
18
  parseTargetParts,
19
+ splitWikiLinkContent,
19
20
  stripWikiMdExt,
20
21
  } from "../../../../core/links";
21
22
  import { slugifySectionTitle } from "../../../../core/sections";
@@ -103,9 +104,10 @@ function renderMarkdownWithWikiLinks(
103
104
  }
104
105
 
105
106
  return content.replace(WIKI_LINK_REGEX, (match, rawContent: string) => {
106
- const [rawTarget, rawAlias] = rawContent.split("|");
107
- const displayText = rawAlias?.trim() || rawTarget?.trim() || match;
108
- const parsed = parseTargetParts(rawTarget ?? "");
107
+ const { target: rawTarget, alias: rawAlias } =
108
+ splitWikiLinkContent(rawContent);
109
+ const displayText = rawAlias?.trim() || rawTarget.trim() || match;
110
+ const parsed = parseTargetParts(rawTarget);
109
111
  const targetCollection = parsed.collection || collection || "";
110
112
  const targetRefKey = normalizeWikiName(stripWikiMdExt(parsed.ref));
111
113
  const targetAnchorKey = (parsed.anchor ?? "").trim().toLowerCase();
@@ -141,7 +141,8 @@ function parseBoolean(value: string | null, defaultValue: boolean): boolean {
141
141
  */
142
142
  export async function handleGraph(
143
143
  store: SqliteAdapter,
144
- url: URL
144
+ url: URL,
145
+ embedModel?: string
145
146
  ): Promise<Response> {
146
147
  // Parse query params
147
148
  const collection = url.searchParams.get("collection") || undefined;
@@ -197,6 +198,7 @@ export async function handleGraph(
197
198
  threshold: thresholdResult.value,
198
199
  linkedOnly,
199
200
  similarTopK: similarTopKResult.value,
201
+ embedModel,
200
202
  };
201
203
 
202
204
  const result = await store.getGraph(options);
@@ -7,7 +7,12 @@
7
7
  import type { SqliteAdapter } from "../../store/sqlite/adapter";
8
8
  import type { ServerContext } from "../context";
9
9
 
10
- import { decodeEmbedding } from "../../store/vector/sqlite-vec";
10
+ import {
11
+ readStoredDocumentVectors,
12
+ resolveStoredVectorSource,
13
+ similarityHitDocuments,
14
+ storedVectorSearchOptions,
15
+ } from "../../store/vector/stored-vectors";
11
16
 
12
17
  // ─────────────────────────────────────────────────────────────────────────────
13
18
  // Types
@@ -373,31 +378,27 @@ export async function handleDocSimilar(
373
378
  } satisfies SimilarDocResponse);
374
379
  }
375
380
 
376
- // Get embedding model from context
377
- const embedModel = ctx.vectorIndex.model;
378
-
379
- // Get document embedding from content_vectors (prefer seq=0)
381
+ // Stored vector of the document's first chunk, from the active partition
380
382
  const db = store.getRawDb();
381
-
382
- interface VectorRow {
383
- embedding: Uint8Array;
383
+ const source = resolveStoredVectorSource(db, ctx.vectorIndex.model);
384
+ let embedding: Float32Array | undefined;
385
+ try {
386
+ [embedding] =
387
+ readStoredDocumentVectors(
388
+ db,
389
+ source,
390
+ [{ id: doc.id, mirrorHash: doc.mirrorHash }],
391
+ { firstChunkOnly: true }
392
+ ).get(doc.id) ?? [];
393
+ } catch (e) {
394
+ return errorResponse(
395
+ "RUNTIME",
396
+ `Invalid stored embedding data: ${e instanceof Error ? e.message : String(e)}`,
397
+ 500
398
+ );
384
399
  }
385
400
 
386
- const vectorRow = db
387
- .query<VectorRow, [string, string]>(
388
- "SELECT embedding FROM content_vectors WHERE mirror_hash = ? AND model = ? AND seq = 0 LIMIT 1"
389
- )
390
- .get(doc.mirrorHash, embedModel);
391
-
392
- const fallbackRow =
393
- vectorRow ??
394
- db
395
- .query<VectorRow, [string, string]>(
396
- "SELECT embedding FROM content_vectors WHERE mirror_hash = ? AND model = ? ORDER BY seq LIMIT 1"
397
- )
398
- .get(doc.mirrorHash, embedModel);
399
-
400
- if (!fallbackRow) {
401
+ if (!embedding) {
401
402
  return jsonResponse({
402
403
  similar: [],
403
404
  meta: {
@@ -409,20 +410,7 @@ export async function handleDocSimilar(
409
410
  },
410
411
  } satisfies SimilarDocResponse);
411
412
  }
412
-
413
- let dimensions: number;
414
- let embedding: Float32Array;
415
-
416
- try {
417
- embedding = decodeEmbedding(fallbackRow.embedding);
418
- dimensions = embedding.length;
419
- } catch (e) {
420
- return errorResponse(
421
- "RUNTIME",
422
- `Invalid stored embedding data: ${e instanceof Error ? e.message : String(e)}`,
423
- 500
424
- );
425
- }
413
+ const dimensions = embedding.length;
426
414
 
427
415
  // Normalize embedding for cosine similarity
428
416
  let norm = 0;
@@ -442,7 +430,7 @@ export async function handleDocSimilar(
442
430
  const searchResult = await ctx.vectorIndex.searchNearest(
443
431
  embedding,
444
432
  candidateLimit,
445
- {}
433
+ storedVectorSearchOptions(source)
446
434
  );
447
435
 
448
436
  if (!searchResult.ok) {
@@ -457,22 +445,16 @@ export async function handleDocSimilar(
457
445
  return errorResponse("RUNTIME", docsResult.error.message, 500);
458
446
  }
459
447
 
460
- const docsByHash = new Map(
461
- docsResult.value
462
- .filter((d) => d.mirrorHash && d.active)
463
- .map((d) => [d.mirrorHash!, d])
464
- );
465
-
466
- // Build similar docs list, excluding self
448
+ // Build similar docs list from each hit's owning documents, excluding self
467
449
  const similar: SimilarDocResponse["similar"] = [];
468
450
  const seenDocids = new Set<string>();
469
451
 
470
- for (const vec of searchResult.value) {
452
+ for (const { document: similarDoc, distance } of similarityHitDocuments(
453
+ searchResult.value,
454
+ docsResult.value.filter((d) => d.mirrorHash && d.active)
455
+ )) {
471
456
  if (similar.length >= limit) break;
472
457
 
473
- const similarDoc = docsByHash.get(vec.mirrorHash);
474
- if (!similarDoc) continue;
475
-
476
458
  // Exclude self
477
459
  if (similarDoc.docid === doc.docid) continue;
478
460
 
@@ -481,7 +463,7 @@ export async function handleDocSimilar(
481
463
 
482
464
  // Compute similarity score from cosine distance
483
465
  // sqlite-vec with cosine metric returns distance where similarity = 1 - distance
484
- const score = Math.max(0, Math.min(1, 1 - vec.distance));
466
+ const score = Math.max(0, Math.min(1, 1 - distance));
485
467
  if (score < threshold) continue;
486
468
 
487
469
  similar.push({
@@ -3,6 +3,7 @@ import type { RequestPeerServer } from "./request-locality";
3
3
  import type { ResidentRuntime } from "./resident-runtime";
4
4
  import type { ContextHolder } from "./routes/api";
5
5
 
6
+ import { getActivePreset } from "../llm/registry";
6
7
  import {
7
8
  isHttpGatewayLoopbackBind,
8
9
  resolveHttpGatewayConfig,
@@ -1694,7 +1695,7 @@ export async function startServer(
1694
1695
  const url = new URL(req.url);
1695
1696
  return withSecurityHeaders(
1696
1697
  await handleResidentRead(runtime as ResidentRuntime, req, () =>
1697
- handleGraph(store, url)
1698
+ handleGraph(store, url, getActivePreset(ctxHolder.config).embed)
1698
1699
  ),
1699
1700
  isDev
1700
1701
  );
@@ -650,6 +650,7 @@ export async function buildAppStatus(
650
650
  const result = await ctx.store.getStatus({
651
651
  embedModel: resolveModelUri(ctx.config, "embed"),
652
652
  chunking: ctx.config.chunking ?? {},
653
+ configuredCollections: ctx.config.collections.map(({ name }) => name),
653
654
  });
654
655
  if (!result.ok) {
655
656
  throw result.error;