@gmickel/gno 2.7.1 → 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.
- package/README.md +3 -2
- package/assets/skill/SKILL.md +11 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- package/assets/skill/recipes/memory-scoped-recall.md +10 -5
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.1.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +86 -9
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +26 -7
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +13 -0
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/memory-recall.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +18 -3
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/graph.ts +3 -1
- package/src/cli/commands/links.ts +61 -180
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +6 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +197 -25
- package/src/core/audit-outside-index.ts +215 -0
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +30 -9
- package/src/core/audit.ts +76 -16
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-inventory-markdown.ts +2 -3
- package/src/core/link-workspace.ts +324 -0
- package/src/core/links.ts +40 -17
- package/src/core/memory-recall.ts +254 -15
- package/src/core/memory-types.ts +12 -0
- package/src/core/memory.ts +2 -0
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +27 -4
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +4 -3
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +74 -93
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +10 -3
- package/src/sdk/client.ts +1 -0
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/graph.ts +3 -1
- package/src/serve/routes/links.ts +45 -50
- package/src/serve/routes/sessions.ts +41 -53
- package/src/serve/server.ts +2 -1
- package/src/serve/status.ts +1 -0
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +477 -337
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +259 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/graph-similarity.ts +96 -0
- package/src/store/sqlite/workspace-link-resolver.ts +742 -0
- package/src/store/types.ts +64 -5
- package/src/store/vector/stats.ts +1 -1
- package/src/store/vector/status.ts +27 -0
- package/src/store/vector/stored-vectors.ts +158 -0
- package/src/store/vector/types.ts +6 -0
- package/src/store/vector/variant-search.ts +30 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +0 -1
|
@@ -169,9 +169,10 @@ export function withOwnedInferenceScope<T>(
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* Cancellation, or a deadline of the
|
|
172
|
+
* Best-effort inference (one page of a background pass, or query expansion):
|
|
173
|
+
* an inference deadline inside it fails only this operation (undefined),
|
|
174
|
+
* leaving the enclosing scope active. Cancellation, or a deadline of the
|
|
175
|
+
* enclosing scope itself, still throws.
|
|
175
176
|
*/
|
|
176
177
|
export async function withInferencePage<T>(
|
|
177
178
|
operation: () => Promise<T>
|
package/src/mcp/http-egress.ts
CHANGED
|
@@ -124,6 +124,21 @@ const collectionFromRef = (value: unknown): string | null => {
|
|
|
124
124
|
return value.slice(0, slash).trim().toLowerCase() || null;
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Tools whose results follow resolved graph edges. Links resolve across a
|
|
129
|
+
* link workspace, so a ref's own collection does not bound the result: the
|
|
130
|
+
* scope is the explicit collection argument (else every collection) plus the
|
|
131
|
+
* collection of every referenced document.
|
|
132
|
+
*/
|
|
133
|
+
const GRAPH_RESULT_TOOLS = new Set([
|
|
134
|
+
"gno_backlinks",
|
|
135
|
+
"gno_graph",
|
|
136
|
+
"gno_graph_neighbors",
|
|
137
|
+
"gno_graph_path",
|
|
138
|
+
"gno_graph_query",
|
|
139
|
+
"gno_impact",
|
|
140
|
+
]);
|
|
141
|
+
|
|
127
142
|
const requestedCollections = (
|
|
128
143
|
params: unknown,
|
|
129
144
|
collections: readonly Collection[]
|
|
@@ -134,17 +149,41 @@ const requestedCollections = (
|
|
|
134
149
|
|
|
135
150
|
const names = new Set<string>();
|
|
136
151
|
const direct = args.collection;
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
// Handlers treat a blank collection as omitted, so it must not count as a
|
|
153
|
+
// scope here either (a graph call with one is authorized as unscoped).
|
|
154
|
+
if (typeof direct === "string" && direct.trim())
|
|
155
|
+
names.add(direct.trim().toLowerCase());
|
|
156
|
+
if (
|
|
157
|
+
(record?.name === "gno_audit" || record?.name === "gno_impact") &&
|
|
158
|
+
Array.isArray(args.collections)
|
|
159
|
+
) {
|
|
139
160
|
for (const value of args.collections) {
|
|
140
161
|
if (typeof value !== "string") continue;
|
|
141
162
|
const normalized = value.trim().toLowerCase();
|
|
142
163
|
if (normalized) names.add(normalized);
|
|
143
164
|
}
|
|
144
165
|
}
|
|
166
|
+
const graphTool =
|
|
167
|
+
typeof record?.name === "string" && GRAPH_RESULT_TOOLS.has(record.name);
|
|
168
|
+
// A graph result spans every collection a link resolves into: without an
|
|
169
|
+
// explicit scope, authorize them all.
|
|
170
|
+
// `gno_similar` with crossCollection returns documents from every collection.
|
|
171
|
+
const crossCollectionSimilar =
|
|
172
|
+
record?.name === "gno_similar" && args.crossCollection === true;
|
|
173
|
+
if ((graphTool && names.size === 0) || crossCollectionSimilar) {
|
|
174
|
+
for (const { name } of collections) names.add(name);
|
|
175
|
+
}
|
|
176
|
+
// The referenced documents' own collections are always authorized too:
|
|
177
|
+
// graph tools serialize the target's metadata even when the result scope
|
|
178
|
+
// is narrower. A graph-tool ref whose collection cannot be read without
|
|
179
|
+
// the index (a docid) authorizes every collection (fail closed).
|
|
145
180
|
for (const key of ["ref", "target", "from", "to", "root", "uri"]) {
|
|
146
|
-
const
|
|
181
|
+
const value = args[key];
|
|
182
|
+
const collection = collectionFromRef(value);
|
|
147
183
|
if (collection) names.add(collection);
|
|
184
|
+
else if (graphTool && typeof value === "string" && value.trim()) {
|
|
185
|
+
for (const { name } of collections) names.add(name);
|
|
186
|
+
}
|
|
148
187
|
}
|
|
149
188
|
if (Array.isArray(args.refs)) {
|
|
150
189
|
for (const ref of args.refs) {
|
package/src/mcp/tools/audit.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
import type { AuditCategory, AuditReport } from "../../core/audit";
|
|
6
6
|
import type { ToolContext } from "../server";
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
AUDIT_CATEGORIES,
|
|
10
|
+
AUDIT_MAX_FINDINGS_ALL,
|
|
11
|
+
AUDIT_MAX_FINDINGS_LIMIT,
|
|
12
|
+
} from "../../core/audit";
|
|
9
13
|
import { runWorkspaceAudit } from "../../core/audit-workspace";
|
|
10
14
|
import { normalizeTag, validateTag } from "../../core/tags";
|
|
11
15
|
import { normalizeCollectionName } from "../../core/validation";
|
|
@@ -19,7 +23,12 @@ export const auditInputSchema = z
|
|
|
19
23
|
collections: z.array(z.string().min(1)).max(256).default([]),
|
|
20
24
|
paths: z.array(z.string().min(1)).max(256).default([]),
|
|
21
25
|
tags: z.array(z.string().min(1)).max(256).default([]),
|
|
22
|
-
maxFindings: z
|
|
26
|
+
maxFindings: z
|
|
27
|
+
.union([
|
|
28
|
+
z.number().int().min(1).max(AUDIT_MAX_FINDINGS_LIMIT),
|
|
29
|
+
z.literal(AUDIT_MAX_FINDINGS_ALL),
|
|
30
|
+
])
|
|
31
|
+
.default(100),
|
|
23
32
|
maxAgeDays: z.number().int().min(1).optional(),
|
|
24
33
|
orphanRoots: z.array(z.string().min(1)).max(256).default([]),
|
|
25
34
|
orphanIgnorePrefixes: z.array(z.string().min(1)).max(256).default([]),
|
package/src/mcp/tools/changes.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const diffInputSchema = z.object({
|
|
|
24
24
|
|
|
25
25
|
export const impactInputSchema = z.object({
|
|
26
26
|
ref: z.string().trim().min(1).max(4096),
|
|
27
|
+
collections: z.array(z.string().trim().min(1)).max(256).optional(),
|
|
27
28
|
maxDepth: z.number().int().min(1).max(6).default(3),
|
|
28
29
|
maxNodes: z.number().int().min(1).max(1000).default(100),
|
|
29
30
|
maxEdges: z.number().int().min(1).max(5000).default(250),
|
package/src/mcp/tools/links.ts
CHANGED
|
@@ -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
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -219,6 +225,8 @@ interface BacklinksInput {
|
|
|
219
225
|
interface BacklinkOutput {
|
|
220
226
|
sourceDocUri: string;
|
|
221
227
|
sourceDocTitle?: string;
|
|
228
|
+
/** Collection of the linking document. */
|
|
229
|
+
sourceCollection?: string;
|
|
222
230
|
linkText?: string;
|
|
223
231
|
position: { startLine: number; startCol: number };
|
|
224
232
|
}
|
|
@@ -308,6 +316,7 @@ export function handleBacklinks(
|
|
|
308
316
|
(b: BacklinkRow) => ({
|
|
309
317
|
sourceDocUri: b.sourceDocUri,
|
|
310
318
|
...(b.sourceDocTitle && { sourceDocTitle: b.sourceDocTitle }),
|
|
319
|
+
...(b.sourceCollection && { sourceCollection: b.sourceCollection }),
|
|
311
320
|
...(b.linkText && { linkText: b.linkText }),
|
|
312
321
|
position: { startLine: b.startLine, startCol: b.startCol },
|
|
313
322
|
})
|
|
@@ -406,62 +415,41 @@ export function handleSimilar(
|
|
|
406
415
|
const preset = getActivePreset(ctx.config);
|
|
407
416
|
const modelUri = preset.embed;
|
|
408
417
|
|
|
409
|
-
//
|
|
418
|
+
// Stored chunk vectors from the active partition (NO model loading required)
|
|
410
419
|
const db = ctx.store.getRawDb();
|
|
420
|
+
const source = resolveStoredVectorSource(db, modelUri);
|
|
411
421
|
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
);
|
|
414
432
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
.query<VectorRow, [string, string]>(
|
|
418
|
-
`SELECT embedding FROM content_vectors
|
|
419
|
-
WHERE mirror_hash = ? AND model = ?
|
|
420
|
-
ORDER BY seq`
|
|
421
|
-
)
|
|
422
|
-
.all(doc.mirrorHash, modelUri);
|
|
423
|
-
|
|
424
|
-
if (vectorRows.length === 0) {
|
|
433
|
+
const first = vectors[0];
|
|
434
|
+
if (!first) {
|
|
425
435
|
throw new Error(
|
|
426
436
|
`${MCP_ERRORS.NOT_FOUND.code}: Document has no embeddings. Run: gno embed`
|
|
427
437
|
);
|
|
428
438
|
}
|
|
429
439
|
|
|
430
440
|
// Compute average embedding from all chunks
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
let avgEmbedding: Float32Array;
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
dimensions = firstBlob.byteLength / 4;
|
|
441
|
-
avgEmbedding = new Float32Array(dimensions);
|
|
442
|
-
|
|
443
|
-
for (const row of vectorRows) {
|
|
444
|
-
const blob = new Uint8Array(row.embedding);
|
|
445
|
-
const embeddingDims = blob.byteLength / 4;
|
|
446
|
-
if (embeddingDims !== dimensions) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
`Inconsistent embedding dimensions: expected ${dimensions}, got ${embeddingDims}`
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
const embedding = new Float32Array(
|
|
452
|
-
blob.buffer,
|
|
453
|
-
blob.byteOffset,
|
|
454
|
-
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}`
|
|
455
447
|
);
|
|
456
|
-
for (let i = 0; i < dimensions; i++) {
|
|
457
|
-
const current = avgEmbedding[i] ?? 0;
|
|
458
|
-
avgEmbedding[i] = current + (embedding[i] ?? 0) / vectorRows.length;
|
|
459
|
-
}
|
|
460
448
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
449
|
+
for (let i = 0; i < dimensions; i++) {
|
|
450
|
+
const current = avgEmbedding[i] ?? 0;
|
|
451
|
+
avgEmbedding[i] = current + (embedding[i] ?? 0) / vectors.length;
|
|
452
|
+
}
|
|
465
453
|
}
|
|
466
454
|
|
|
467
455
|
// Normalize the average embedding for cosine similarity
|
|
@@ -510,73 +498,62 @@ export function handleSimilar(
|
|
|
510
498
|
const searchResult = await vectorIndex.searchNearest(
|
|
511
499
|
avgEmbedding,
|
|
512
500
|
candidateLimit,
|
|
513
|
-
|
|
501
|
+
storedVectorSearchOptions(source)
|
|
514
502
|
);
|
|
515
503
|
|
|
516
504
|
if (!searchResult.ok) {
|
|
517
505
|
throw new Error(searchResult.error.message);
|
|
518
506
|
}
|
|
519
507
|
|
|
520
|
-
//
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
.filter((r) => r.mirrorHash !== doc.mirrorHash)
|
|
525
|
-
.map((r) => r.mirrorHash)
|
|
526
|
-
),
|
|
527
|
-
];
|
|
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
|
+
);
|
|
528
512
|
|
|
529
|
-
// Batch query documents
|
|
513
|
+
// Batch query the hits' documents: exact owners for partition hits,
|
|
514
|
+
// documents sharing the content for legacy hits (avoid N+1)
|
|
530
515
|
interface DocRow {
|
|
516
|
+
id: number;
|
|
531
517
|
docid: string;
|
|
532
518
|
uri: string;
|
|
533
519
|
title: string | null;
|
|
534
520
|
collection: string;
|
|
535
521
|
rel_path: string;
|
|
536
|
-
|
|
522
|
+
mirrorHash: string;
|
|
537
523
|
}
|
|
538
524
|
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
// Build best score per mirrorHash from search results
|
|
559
|
-
const scoresByHash = new Map<string, number>();
|
|
560
|
-
for (const r of searchResult.value) {
|
|
561
|
-
if (r.mirrorHash === doc.mirrorHash) continue;
|
|
562
|
-
// Compute similarity score from cosine distance
|
|
563
|
-
// sqlite-vec with cosine metric returns distance where similarity = 1 - distance
|
|
564
|
-
const score = Math.max(0, Math.min(1, 1 - r.distance));
|
|
565
|
-
const existing = scoresByHash.get(r.mirrorHash) ?? 0;
|
|
566
|
-
if (score > existing) {
|
|
567
|
-
scoresByHash.set(r.mirrorHash, score);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
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
|
+
);
|
|
570
543
|
|
|
571
|
-
// Build similar docs list
|
|
544
|
+
// Build similar docs list; hits are nearest first, so a document's
|
|
545
|
+
// first hit carries its best score
|
|
572
546
|
const similar: SimilarDocOutput[] = [];
|
|
547
|
+
const seenIds = new Set<number>();
|
|
573
548
|
const docCollection = doc.collection.toLowerCase();
|
|
574
549
|
|
|
575
|
-
for (const
|
|
550
|
+
for (const { document: docRow, distance } of similarityHitDocuments(
|
|
551
|
+
hits,
|
|
552
|
+
docRows
|
|
553
|
+
)) {
|
|
576
554
|
if (similar.length >= limit) break;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (!docRow) continue;
|
|
555
|
+
if (docRow.id === doc.id || seenIds.has(docRow.id)) continue;
|
|
556
|
+
seenIds.add(docRow.id);
|
|
580
557
|
|
|
581
558
|
// Filter by collection if not crossCollection (case-insensitive)
|
|
582
559
|
if (
|
|
@@ -586,7 +563,9 @@ export function handleSimilar(
|
|
|
586
563
|
continue;
|
|
587
564
|
}
|
|
588
565
|
|
|
589
|
-
|
|
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));
|
|
590
569
|
if (score < threshold) continue;
|
|
591
570
|
|
|
592
571
|
// Get absPath (case-insensitive collection lookup)
|
|
@@ -796,6 +775,7 @@ export function handleGraph(
|
|
|
796
775
|
threshold: args.threshold ?? 0.7,
|
|
797
776
|
linkedOnly: args.linkedOnly ?? true,
|
|
798
777
|
similarTopK: args.similarTopK ?? 5,
|
|
778
|
+
embedModel: getActivePreset(ctx.config).embed,
|
|
799
779
|
});
|
|
800
780
|
|
|
801
781
|
if (!result.ok) {
|
|
@@ -833,6 +813,7 @@ async function getValidatedGraph(
|
|
|
833
813
|
threshold: args.threshold ?? 0.7,
|
|
834
814
|
linkedOnly: args.linkedOnly ?? true,
|
|
835
815
|
similarTopK: args.similarTopK ?? 5,
|
|
816
|
+
embedModel: getActivePreset(ctx.config).embed,
|
|
836
817
|
});
|
|
837
818
|
|
|
838
819
|
if (!result.ok) {
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
|
|
16
|
+
import type { Config } from "../../config/types";
|
|
16
17
|
import type { ToolContext } from "../server";
|
|
17
18
|
|
|
18
19
|
import { runAutomationProfile } from "../../sessions/automation";
|
|
20
|
+
import { refreshServedConfig } from "../../sessions/config-refresh";
|
|
19
21
|
import {
|
|
20
22
|
formatAutomationRunText,
|
|
21
23
|
formatImportReceiptText,
|
|
@@ -99,9 +101,36 @@ export const SESSIONS_IMPORT_MCP_ANNOTATIONS = {
|
|
|
99
101
|
openWorldHint: false,
|
|
100
102
|
} as const;
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The config file as it is now, adopted when the CLI changed it while this
|
|
106
|
+
* server runs (same binding checks and errors as the REST routes). Inside a
|
|
107
|
+
* request `ctx.config` stays the snapshot taken at its start, so callers use
|
|
108
|
+
* the returned config. An adoption moves the egress policy epoch; this
|
|
109
|
+
* request advances with it rather than voiding itself.
|
|
110
|
+
*/
|
|
111
|
+
function currentConfig(ctx: ToolContext): Promise<Config> {
|
|
112
|
+
return refreshServedConfig({
|
|
113
|
+
configPath: ctx.actualConfigPath,
|
|
114
|
+
indexName: ctx.indexName,
|
|
115
|
+
store: ctx.store,
|
|
104
116
|
config: ctx.config,
|
|
117
|
+
setConfig: (config) => {
|
|
118
|
+
ctx.config = config;
|
|
119
|
+
},
|
|
120
|
+
invalidateEgressPolicy: async () => {
|
|
121
|
+
const invalidation = await ctx.invalidateEgressPolicy?.();
|
|
122
|
+
if (invalidation) {
|
|
123
|
+
ctx.advanceRequestAuthorizationEpoch?.(invalidation.policyEpoch);
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
markContentMutation: () => ctx.markContentMutation?.(),
|
|
127
|
+
markIndexMutation: () => ctx.markIndexMutation?.(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function service(ctx: ToolContext): Promise<SessionsService> {
|
|
132
|
+
return new SessionsService({
|
|
133
|
+
config: await currentConfig(ctx),
|
|
105
134
|
configPath: ctx.actualConfigPath,
|
|
106
135
|
indexName: ctx.indexName,
|
|
107
136
|
store: ctx.store,
|
|
@@ -120,7 +149,7 @@ export function handleSessionsStatus(ctx: ToolContext): Promise<ToolResult> {
|
|
|
120
149
|
"gno_sessions_status",
|
|
121
150
|
async () => {
|
|
122
151
|
try {
|
|
123
|
-
return await service(ctx).status();
|
|
152
|
+
return await (await service(ctx)).status();
|
|
124
153
|
} catch (error) {
|
|
125
154
|
return rethrowSessionsError(error);
|
|
126
155
|
}
|
|
@@ -146,7 +175,7 @@ export function handleSessionsImport(
|
|
|
146
175
|
try {
|
|
147
176
|
// A child process keeps this server answering during a long import.
|
|
148
177
|
receipt = await importInChildProcess({
|
|
149
|
-
config: ctx
|
|
178
|
+
config: await currentConfig(ctx),
|
|
150
179
|
configPath: ctx.actualConfigPath,
|
|
151
180
|
indexName: ctx.indexName,
|
|
152
181
|
sourceId: args.sourceId,
|
package/src/mcp/tools/status.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { ToolContext } from "../server";
|
|
|
10
10
|
import { buildContentTypeBoostStatus } from "../../config/content-types";
|
|
11
11
|
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
12
12
|
import { OWNER_CONFIG_PATH_FIELDS, withoutFields } from "../../core/host-paths";
|
|
13
|
+
import { formatLinkWorkspace } from "../../core/link-workspace";
|
|
13
14
|
import { formatVectorPartitionLines } from "../../core/vector-partition-status";
|
|
14
15
|
import { resolveModelUri } from "../../llm/registry";
|
|
15
16
|
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
@@ -55,6 +56,8 @@ function formatStatus(status: StatusView): string {
|
|
|
55
56
|
` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
|
|
56
57
|
(c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : "")
|
|
57
58
|
);
|
|
59
|
+
const workspace = formatLinkWorkspace(c);
|
|
60
|
+
if (workspace) lines.push(` Link workspace: ${workspace}`);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -102,6 +105,7 @@ export function handleStatus(
|
|
|
102
105
|
const result = await ctx.store.getStatus({
|
|
103
106
|
embedModel: resolveModelUri(ctx.config, "embed"),
|
|
104
107
|
chunking: ctx.config.chunking ?? {},
|
|
108
|
+
configuredCollections: ctx.config.collections.map(({ name }) => name),
|
|
105
109
|
});
|
|
106
110
|
if (!result.ok) {
|
|
107
111
|
throw new Error(result.error.message);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
assertInferenceActive,
|
|
16
16
|
assertInferenceResult,
|
|
17
17
|
inferenceOptions,
|
|
18
|
+
withInferencePage,
|
|
18
19
|
} from "../llm/inference-scope";
|
|
19
20
|
import { ok } from "../store/types";
|
|
20
21
|
|
|
@@ -23,7 +24,6 @@ import { ok } from "../store/types";
|
|
|
23
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
25
|
|
|
25
26
|
const EXPANSION_PROMPT_VERSION = "v3";
|
|
26
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
27
27
|
// Non-greedy to avoid matching from first { to last } across multiple objects
|
|
28
28
|
const JSON_EXTRACT_PATTERN = /\{[\s\S]*?\}/;
|
|
29
29
|
const QUOTED_PHRASE_PATTERN = /"([^"]+)"/g;
|
|
@@ -457,8 +457,6 @@ export function parseExpansionOutput(
|
|
|
457
457
|
export interface ExpansionOptions extends InferenceOptions {
|
|
458
458
|
/** Language hint for prompt selection */
|
|
459
459
|
lang?: string;
|
|
460
|
-
/** Timeout in milliseconds */
|
|
461
|
-
timeout?: number;
|
|
462
460
|
/** Optional context that steers expansion for ambiguous queries */
|
|
463
461
|
intent?: string;
|
|
464
462
|
/** Optional bounded context size override for expansion generation */
|
|
@@ -467,59 +465,49 @@ export interface ExpansionOptions extends InferenceOptions {
|
|
|
467
465
|
|
|
468
466
|
/**
|
|
469
467
|
* Expand query using generation model.
|
|
470
|
-
* Returns null
|
|
468
|
+
* Returns null when generation fails or times out, or its output does not
|
|
469
|
+
* parse (graceful degradation).
|
|
471
470
|
*/
|
|
472
471
|
export async function expandQuery(
|
|
473
472
|
genPort: GenerationPort,
|
|
474
473
|
query: string,
|
|
475
474
|
options: ExpansionOptions = {}
|
|
476
475
|
): Promise<StoreResult<ExpansionResult | null>> {
|
|
477
|
-
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
478
|
-
|
|
479
476
|
// Build prompt
|
|
480
477
|
const prompt = buildExpansionPrompt(query, options);
|
|
481
478
|
|
|
482
479
|
assertInferenceActive(options);
|
|
483
480
|
const operational = inferenceOptions(options);
|
|
484
|
-
const budget = new AbortController();
|
|
485
|
-
const expiresAt = performance.now() + timeout;
|
|
486
|
-
const timer = setTimeout(() => budget.abort(), timeout);
|
|
487
481
|
try {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
482
|
+
// Expansion is best effort but has no budget of its own: slow hardware
|
|
483
|
+
// (CPU generation takes 15-20s) and a cold model load still expand. A
|
|
484
|
+
// generation that hits models.inferenceTimeout fails only expansion.
|
|
485
|
+
const result = await withInferencePage(() =>
|
|
486
|
+
genPort.generate(
|
|
487
|
+
prompt,
|
|
488
|
+
{
|
|
489
|
+
temperature: 0,
|
|
490
|
+
seed: 42,
|
|
491
|
+
maxTokens: 512,
|
|
492
|
+
contextSize: options.contextSize,
|
|
493
|
+
},
|
|
494
|
+
operational
|
|
495
|
+
)
|
|
502
496
|
);
|
|
503
|
-
//
|
|
504
|
-
// cancellation/deadline always wins and cannot become a lexical success.
|
|
497
|
+
// Caller cancellation/deadline always wins and cannot become a lexical success.
|
|
505
498
|
assertInferenceActive(options);
|
|
506
|
-
if (
|
|
507
|
-
return ok(null);
|
|
499
|
+
if (!result) return ok(null);
|
|
508
500
|
assertInferenceResult(result);
|
|
509
501
|
if (!result.ok) return ok(null);
|
|
510
502
|
return ok(parseExpansionOutput(result.value, query));
|
|
511
503
|
} catch (cause) {
|
|
512
504
|
assertInferenceActive(options);
|
|
513
|
-
if (budget.signal.aborted || performance.now() >= expiresAt)
|
|
514
|
-
return ok(null);
|
|
515
505
|
if (
|
|
516
506
|
cause instanceof Error &&
|
|
517
507
|
["AbortError", "TimeoutError"].includes(cause.name)
|
|
518
508
|
)
|
|
519
509
|
throw cause;
|
|
520
510
|
return ok(null);
|
|
521
|
-
} finally {
|
|
522
|
-
clearTimeout(timer);
|
|
523
511
|
}
|
|
524
512
|
}
|
|
525
513
|
|
|
@@ -196,6 +196,7 @@ const loadGraphLinks = async (
|
|
|
196
196
|
seedDocumentIds: number[],
|
|
197
197
|
options: {
|
|
198
198
|
collection?: string;
|
|
199
|
+
collections?: string[];
|
|
199
200
|
includeSimilar?: boolean;
|
|
200
201
|
}
|
|
201
202
|
): Promise<
|
|
@@ -205,6 +206,7 @@ const loadGraphLinks = async (
|
|
|
205
206
|
const neighborsResult = await store.getGraphNeighborsForSeeds({
|
|
206
207
|
seedDocumentIds,
|
|
207
208
|
collection: options.collection,
|
|
209
|
+
collections: options.collections,
|
|
208
210
|
limitEdges: GRAPH_EDGE_LIMIT,
|
|
209
211
|
});
|
|
210
212
|
if (!neighborsResult.ok) {
|
|
@@ -239,6 +241,11 @@ export async function expandGraphCandidates(
|
|
|
239
241
|
fusedCandidates: FusionCandidate[],
|
|
240
242
|
options: {
|
|
241
243
|
collection?: string;
|
|
244
|
+
/**
|
|
245
|
+
* Graph allowlist (plural request scope). Neighbours must belong to one of
|
|
246
|
+
* these collections; defaults to `[collection]` when unset.
|
|
247
|
+
*/
|
|
248
|
+
collections?: string[];
|
|
242
249
|
includeSimilar?: boolean;
|
|
243
250
|
eligibility?: DocumentEligibilityOptions;
|
|
244
251
|
limit?: number;
|
|
@@ -353,8 +360,14 @@ export async function expandGraphCandidates(
|
|
|
353
360
|
}
|
|
354
361
|
|
|
355
362
|
const seedDocumentIds = [...seedByDocid.values()].map(({ doc }) => doc.id);
|
|
363
|
+
const graphAllowlist = options.collections
|
|
364
|
+
? new Set(options.collections)
|
|
365
|
+
: options.collection
|
|
366
|
+
? new Set([options.collection])
|
|
367
|
+
: undefined;
|
|
356
368
|
const linksResult = await loadGraphLinks(store, seedDocumentIds, {
|
|
357
369
|
collection: options.collection,
|
|
370
|
+
collections: options.collections,
|
|
358
371
|
includeSimilar: options.includeSimilar,
|
|
359
372
|
});
|
|
360
373
|
if (!linksResult.ok) {
|
|
@@ -400,9 +413,15 @@ export async function expandGraphCandidates(
|
|
|
400
413
|
return { candidates: [], meta };
|
|
401
414
|
}
|
|
402
415
|
|
|
416
|
+
// A plural allowlist hydrates across its collections; scope is enforced on
|
|
417
|
+
// each neighbour's own collection below, never widened past the allowlist.
|
|
418
|
+
const plural = options.collections !== undefined;
|
|
403
419
|
const docsResult = await store.getDocumentsByDocids(rankedNeighborDocids, {
|
|
404
|
-
eligibility:
|
|
405
|
-
|
|
420
|
+
eligibility:
|
|
421
|
+
plural && options.eligibility
|
|
422
|
+
? { ...options.eligibility, collection: undefined }
|
|
423
|
+
: options.eligibility,
|
|
424
|
+
collection: plural ? undefined : options.collection,
|
|
406
425
|
activeOnly: true,
|
|
407
426
|
});
|
|
408
427
|
if (!docsResult.ok) {
|
|
@@ -413,6 +432,7 @@ export async function expandGraphCandidates(
|
|
|
413
432
|
const metadataFilteredDocs = docsResult.value.filter(
|
|
414
433
|
(doc) =>
|
|
415
434
|
doc.mirrorHash &&
|
|
435
|
+
(!graphAllowlist || graphAllowlist.has(doc.collection)) &&
|
|
416
436
|
(options.relPathPrefix === undefined ||
|
|
417
437
|
sourceRelPath(doc) === options.relPathPrefix ||
|
|
418
438
|
sourceRelPath(doc).startsWith(`${options.relPathPrefix}/`)) &&
|
package/src/pipeline/hybrid.ts
CHANGED
|
@@ -520,7 +520,6 @@ async function searchHybridWithHydration(
|
|
|
520
520
|
const expandResult = await expandQuery(expandPort, query, {
|
|
521
521
|
// Use queryLanguage for prompt selection, NOT options.lang (retrieval filter)
|
|
522
522
|
lang: queryLanguage,
|
|
523
|
-
timeout: pipelineConfig.expansionTimeout,
|
|
524
523
|
intent: options.intent,
|
|
525
524
|
contextSize: deps.config.models?.expandContextSize,
|
|
526
525
|
});
|
|
@@ -804,6 +803,7 @@ async function searchHybridWithHydration(
|
|
|
804
803
|
fusedCandidates,
|
|
805
804
|
{
|
|
806
805
|
collection: options.collection,
|
|
806
|
+
collections: options.graphCollections,
|
|
807
807
|
includeSimilar: vectorAvailable,
|
|
808
808
|
eligibility: vectorEligibility,
|
|
809
809
|
limit,
|
package/src/pipeline/search.ts
CHANGED
|
@@ -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) {
|