@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
package/src/pipeline/types.ts
CHANGED
|
@@ -191,6 +191,12 @@ export interface SearchOptions extends InferenceOptions {
|
|
|
191
191
|
minScore?: number;
|
|
192
192
|
/** Filter by collection */
|
|
193
193
|
collection?: string;
|
|
194
|
+
/**
|
|
195
|
+
* Internal graph allowlist for a request partitioned into one retrieval per
|
|
196
|
+
* collection (Context Capsules, replay): graph neighbours may come from any
|
|
197
|
+
* of these collections, never from others. Defaults to `[collection]`.
|
|
198
|
+
*/
|
|
199
|
+
graphCollections?: string[];
|
|
194
200
|
/** Internal exact corpus scope used by deterministic retrieval replay. */
|
|
195
201
|
retrievalScope?: {
|
|
196
202
|
relPathPrefix?: string;
|
|
@@ -226,6 +232,10 @@ export interface SearchOptions extends InferenceOptions {
|
|
|
226
232
|
scopes: string[];
|
|
227
233
|
excludeSuperseded: boolean;
|
|
228
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;
|
|
229
239
|
}
|
|
230
240
|
|
|
231
241
|
/** Structured query mode identifier */
|
|
@@ -406,8 +416,6 @@ export type RerankedCandidate = FusionCandidate & {
|
|
|
406
416
|
|
|
407
417
|
/** Search pipeline configuration */
|
|
408
418
|
export interface PipelineConfig {
|
|
409
|
-
/** Expansion timeout in ms */
|
|
410
|
-
expansionTimeout: number;
|
|
411
419
|
/** Max candidates to rerank */
|
|
412
420
|
rerankCandidates: number;
|
|
413
421
|
/** RRF configuration */
|
|
@@ -418,7 +426,6 @@ export interface PipelineConfig {
|
|
|
418
426
|
|
|
419
427
|
/** Default pipeline configuration */
|
|
420
428
|
export const DEFAULT_PIPELINE_CONFIG: PipelineConfig = {
|
|
421
|
-
expansionTimeout: 5000,
|
|
422
429
|
rerankCandidates: 20,
|
|
423
430
|
rrf: DEFAULT_RRF_CONFIG,
|
|
424
431
|
blendingSchedule: DEFAULT_BLENDING_SCHEDULE,
|
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 {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../core/findings-run-state";
|
|
31
31
|
import { acquireCliWriteLease } from "../core/write-lease";
|
|
32
32
|
|
|
33
|
-
/** Audit report cap
|
|
33
|
+
/** Audit report cap for the daemon findings pass. */
|
|
34
34
|
const FINDINGS_AUDIT_MAX_FINDINGS = 1000;
|
|
35
35
|
const LEASE_HOLDER_COMMAND = "gno daemon (findings pass)";
|
|
36
36
|
const CONTROL_CHARS = /\p{Cc}/gu;
|
|
@@ -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
|
|
107
|
-
|
|
108
|
-
const
|
|
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();
|
|
@@ -79,7 +79,12 @@ export async function handleImpact(
|
|
|
79
79
|
): Promise<Response> {
|
|
80
80
|
const ref = url.searchParams.get("ref")?.trim();
|
|
81
81
|
if (!ref) return errorResponse("VALIDATION", "ref is required");
|
|
82
|
-
const
|
|
82
|
+
const collections = url.searchParams
|
|
83
|
+
.getAll("collection")
|
|
84
|
+
.map((value) => value.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
const input: KnowledgeImpactInput =
|
|
87
|
+
collections.length > 0 ? { collections } : {};
|
|
83
88
|
for (const [queryName, inputName] of [
|
|
84
89
|
["maxDepth", "maxDepth"],
|
|
85
90
|
["maxNodes", "maxNodes"],
|
|
@@ -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 {
|
|
10
|
+
import {
|
|
11
|
+
readStoredDocumentVectors,
|
|
12
|
+
resolveStoredVectorSource,
|
|
13
|
+
similarityHitDocuments,
|
|
14
|
+
storedVectorSearchOptions,
|
|
15
|
+
} from "../../store/vector/stored-vectors";
|
|
11
16
|
|
|
12
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
18
|
// Types
|
|
@@ -34,6 +39,8 @@ export interface LinkResponse {
|
|
|
34
39
|
resolvedUri?: string;
|
|
35
40
|
/** Resolved target title (if found) */
|
|
36
41
|
resolvedTitle?: string;
|
|
42
|
+
/** Collection of the resolved target (may differ from the source's) */
|
|
43
|
+
resolvedCollection?: string;
|
|
37
44
|
}>;
|
|
38
45
|
meta: {
|
|
39
46
|
docid: string;
|
|
@@ -49,6 +56,8 @@ export interface BacklinkResponse {
|
|
|
49
56
|
sourceDocid: string;
|
|
50
57
|
sourceUri: string;
|
|
51
58
|
sourceTitle?: string;
|
|
59
|
+
/** Collection of the linking document */
|
|
60
|
+
sourceCollection?: string;
|
|
52
61
|
linkText?: string;
|
|
53
62
|
startLine: number;
|
|
54
63
|
startCol: number;
|
|
@@ -189,6 +198,11 @@ export async function handleDocLinks(
|
|
|
189
198
|
targetRefNorm: l.targetRefNorm,
|
|
190
199
|
targetCollection: l.targetCollection || doc.collection,
|
|
191
200
|
linkType: l.linkType,
|
|
201
|
+
source: {
|
|
202
|
+
collection: doc.collection,
|
|
203
|
+
relPath: doc.relPath,
|
|
204
|
+
explicit: Boolean(l.targetCollection),
|
|
205
|
+
},
|
|
192
206
|
}))
|
|
193
207
|
);
|
|
194
208
|
const resolutionAvailable = resolvedResult.ok;
|
|
@@ -217,6 +231,9 @@ export async function handleDocLinks(
|
|
|
217
231
|
resolvedDocid: resolved.docid,
|
|
218
232
|
resolvedUri: resolved.uri,
|
|
219
233
|
resolvedTitle: resolved.title ?? undefined,
|
|
234
|
+
...(resolved.collection && {
|
|
235
|
+
resolvedCollection: resolved.collection,
|
|
236
|
+
}),
|
|
220
237
|
}),
|
|
221
238
|
}),
|
|
222
239
|
};
|
|
@@ -273,6 +290,7 @@ export async function handleDocBacklinks(
|
|
|
273
290
|
backlinks: backlinks.map((b) => ({
|
|
274
291
|
sourceDocid: b.sourceDocid,
|
|
275
292
|
sourceUri: b.sourceDocUri,
|
|
293
|
+
...(b.sourceCollection && { sourceCollection: b.sourceCollection }),
|
|
276
294
|
...(b.sourceDocTitle && { sourceTitle: b.sourceDocTitle }),
|
|
277
295
|
...(b.linkText && { linkText: b.linkText }),
|
|
278
296
|
startLine: b.startLine,
|
|
@@ -360,31 +378,27 @@ export async function handleDocSimilar(
|
|
|
360
378
|
} satisfies SimilarDocResponse);
|
|
361
379
|
}
|
|
362
380
|
|
|
363
|
-
//
|
|
364
|
-
const embedModel = ctx.vectorIndex.model;
|
|
365
|
-
|
|
366
|
-
// Get document embedding from content_vectors (prefer seq=0)
|
|
381
|
+
// Stored vector of the document's first chunk, from the active partition
|
|
367
382
|
const db = store.getRawDb();
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
);
|
|
371
399
|
}
|
|
372
400
|
|
|
373
|
-
|
|
374
|
-
.query<VectorRow, [string, string]>(
|
|
375
|
-
"SELECT embedding FROM content_vectors WHERE mirror_hash = ? AND model = ? AND seq = 0 LIMIT 1"
|
|
376
|
-
)
|
|
377
|
-
.get(doc.mirrorHash, embedModel);
|
|
378
|
-
|
|
379
|
-
const fallbackRow =
|
|
380
|
-
vectorRow ??
|
|
381
|
-
db
|
|
382
|
-
.query<VectorRow, [string, string]>(
|
|
383
|
-
"SELECT embedding FROM content_vectors WHERE mirror_hash = ? AND model = ? ORDER BY seq LIMIT 1"
|
|
384
|
-
)
|
|
385
|
-
.get(doc.mirrorHash, embedModel);
|
|
386
|
-
|
|
387
|
-
if (!fallbackRow) {
|
|
401
|
+
if (!embedding) {
|
|
388
402
|
return jsonResponse({
|
|
389
403
|
similar: [],
|
|
390
404
|
meta: {
|
|
@@ -396,20 +410,7 @@ export async function handleDocSimilar(
|
|
|
396
410
|
},
|
|
397
411
|
} satisfies SimilarDocResponse);
|
|
398
412
|
}
|
|
399
|
-
|
|
400
|
-
let dimensions: number;
|
|
401
|
-
let embedding: Float32Array;
|
|
402
|
-
|
|
403
|
-
try {
|
|
404
|
-
embedding = decodeEmbedding(fallbackRow.embedding);
|
|
405
|
-
dimensions = embedding.length;
|
|
406
|
-
} catch (e) {
|
|
407
|
-
return errorResponse(
|
|
408
|
-
"RUNTIME",
|
|
409
|
-
`Invalid stored embedding data: ${e instanceof Error ? e.message : String(e)}`,
|
|
410
|
-
500
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
+
const dimensions = embedding.length;
|
|
413
414
|
|
|
414
415
|
// Normalize embedding for cosine similarity
|
|
415
416
|
let norm = 0;
|
|
@@ -429,7 +430,7 @@ export async function handleDocSimilar(
|
|
|
429
430
|
const searchResult = await ctx.vectorIndex.searchNearest(
|
|
430
431
|
embedding,
|
|
431
432
|
candidateLimit,
|
|
432
|
-
|
|
433
|
+
storedVectorSearchOptions(source)
|
|
433
434
|
);
|
|
434
435
|
|
|
435
436
|
if (!searchResult.ok) {
|
|
@@ -444,22 +445,16 @@ export async function handleDocSimilar(
|
|
|
444
445
|
return errorResponse("RUNTIME", docsResult.error.message, 500);
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
|
|
448
|
-
docsResult.value
|
|
449
|
-
.filter((d) => d.mirrorHash && d.active)
|
|
450
|
-
.map((d) => [d.mirrorHash!, d])
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
// Build similar docs list, excluding self
|
|
448
|
+
// Build similar docs list from each hit's owning documents, excluding self
|
|
454
449
|
const similar: SimilarDocResponse["similar"] = [];
|
|
455
450
|
const seenDocids = new Set<string>();
|
|
456
451
|
|
|
457
|
-
for (const
|
|
452
|
+
for (const { document: similarDoc, distance } of similarityHitDocuments(
|
|
453
|
+
searchResult.value,
|
|
454
|
+
docsResult.value.filter((d) => d.mirrorHash && d.active)
|
|
455
|
+
)) {
|
|
458
456
|
if (similar.length >= limit) break;
|
|
459
457
|
|
|
460
|
-
const similarDoc = docsByHash.get(vec.mirrorHash);
|
|
461
|
-
if (!similarDoc) continue;
|
|
462
|
-
|
|
463
458
|
// Exclude self
|
|
464
459
|
if (similarDoc.docid === doc.docid) continue;
|
|
465
460
|
|
|
@@ -468,7 +463,7 @@ export async function handleDocSimilar(
|
|
|
468
463
|
|
|
469
464
|
// Compute similarity score from cosine distance
|
|
470
465
|
// sqlite-vec with cosine metric returns distance where similarity = 1 - distance
|
|
471
|
-
const score = Math.max(0, Math.min(1, 1 -
|
|
466
|
+
const score = Math.max(0, Math.min(1, 1 - distance));
|
|
472
467
|
if (score < threshold) continue;
|
|
473
468
|
|
|
474
469
|
similar.push({
|
|
@@ -16,7 +16,6 @@ import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
|
16
16
|
import type { RequestPeerServer } from "../request-locality";
|
|
17
17
|
import type { ContextHolder } from "./api";
|
|
18
18
|
|
|
19
|
-
import { getIndexDbPath } from "../../app/constants";
|
|
20
19
|
import { getConfigPaths, loadConfig } from "../../config";
|
|
21
20
|
import { withContentTypeRules } from "../../ingestion";
|
|
22
21
|
import {
|
|
@@ -28,8 +27,14 @@ import {
|
|
|
28
27
|
runAutomationProfile,
|
|
29
28
|
setAutomationProfile,
|
|
30
29
|
} from "../../sessions/automation";
|
|
31
|
-
import { assertSessionBinding } from "../../sessions/binding";
|
|
32
30
|
import { SessionSourceSchema, watchedCollections } from "../../sessions/config";
|
|
31
|
+
import {
|
|
32
|
+
adoptServedConfig,
|
|
33
|
+
assertInstanceBinding as assertConfigBinding,
|
|
34
|
+
readInstanceConfig,
|
|
35
|
+
refreshServedConfig,
|
|
36
|
+
type ServedSessionsConfig,
|
|
37
|
+
} from "../../sessions/config-refresh";
|
|
33
38
|
import { importInChildProcess } from "../../sessions/import-child";
|
|
34
39
|
import { SessionsService } from "../../sessions/service";
|
|
35
40
|
import {
|
|
@@ -186,18 +191,11 @@ function instanceIdentity(ctxHolder: ContextHolder): {
|
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
/** Refuse an archive config opened against a different index (and vice versa). */
|
|
189
|
-
|
|
194
|
+
function assertInstanceBinding(
|
|
190
195
|
ctxHolder: ContextHolder,
|
|
191
196
|
config: Config = ctxHolder.config
|
|
192
197
|
): Promise<void> {
|
|
193
|
-
|
|
194
|
-
if (!config.sessions) return;
|
|
195
|
-
await assertSessionBinding({
|
|
196
|
-
config,
|
|
197
|
-
configPath,
|
|
198
|
-
indexName,
|
|
199
|
-
dbPath: getIndexDbPath(indexName),
|
|
200
|
-
});
|
|
198
|
+
return assertConfigBinding(instanceIdentity(ctxHolder), config);
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
/** Service over the instance's own config/index pair, binding checked. */
|
|
@@ -215,55 +213,47 @@ async function archiveService(
|
|
|
215
213
|
});
|
|
216
214
|
}
|
|
217
215
|
|
|
216
|
+
/** This instance's served config, as the shared config refresh sees it. */
|
|
217
|
+
function servedConfig(
|
|
218
|
+
ctxHolder: ContextHolder,
|
|
219
|
+
store: SqliteAdapter
|
|
220
|
+
): ServedSessionsConfig {
|
|
221
|
+
return {
|
|
222
|
+
...instanceIdentity(ctxHolder),
|
|
223
|
+
store,
|
|
224
|
+
config: ctxHolder.config,
|
|
225
|
+
setConfig: (config) => {
|
|
226
|
+
ctxHolder.config = config;
|
|
227
|
+
ctxHolder.current = { ...ctxHolder.current, config };
|
|
228
|
+
ctxHolder.watchService?.updateCollections(
|
|
229
|
+
watchedCollections(config),
|
|
230
|
+
withContentTypeRules({}, config)
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
invalidateEgressPolicy: async () => {
|
|
234
|
+
await ctxHolder.invalidateEgressPolicy?.();
|
|
235
|
+
},
|
|
236
|
+
markContentMutation: () => ctxHolder.markContentMutation?.(),
|
|
237
|
+
markIndexMutation: () => ctxHolder.markIndexMutation?.(),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
218
241
|
/**
|
|
219
242
|
* Adopt a config the sessions service already persisted: project collections
|
|
220
243
|
* and contexts into the open store, swap the in-memory context, and refresh
|
|
221
|
-
* the watcher, egress policy and mutation generations
|
|
222
|
-
* config-sync route helpers).
|
|
244
|
+
* the watcher, egress policy and mutation generations.
|
|
223
245
|
*/
|
|
224
|
-
|
|
246
|
+
function adoptConfig(
|
|
225
247
|
ctxHolder: ContextHolder,
|
|
226
248
|
store: SqliteAdapter,
|
|
227
249
|
config: Config
|
|
228
250
|
): Promise<void> {
|
|
229
|
-
|
|
230
|
-
if (!collections.ok) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Config saved but collection sync failed: ${collections.error.message}`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const contexts = await store.syncContexts(config.contexts ?? []);
|
|
236
|
-
if (!contexts.ok) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`Config saved but context sync failed: ${contexts.error.message}`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
ctxHolder.config = config;
|
|
242
|
-
ctxHolder.current = { ...ctxHolder.current, config };
|
|
243
|
-
ctxHolder.watchService?.updateCollections(
|
|
244
|
-
watchedCollections(config),
|
|
245
|
-
withContentTypeRules({}, config)
|
|
246
|
-
);
|
|
247
|
-
await ctxHolder.invalidateEgressPolicy?.();
|
|
248
|
-
ctxHolder.markContentMutation?.();
|
|
249
|
-
ctxHolder.markIndexMutation?.();
|
|
251
|
+
return adoptServedConfig(servedConfig(ctxHolder, store), config);
|
|
250
252
|
}
|
|
251
253
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* before it can touch this one.
|
|
256
|
-
*/
|
|
257
|
-
async function readConfigFile(ctxHolder: ContextHolder): Promise<Config> {
|
|
258
|
-
const loaded = await loadConfig(instanceIdentity(ctxHolder).configPath);
|
|
259
|
-
if (!loaded.ok) {
|
|
260
|
-
throw new SessionsError(
|
|
261
|
-
"SESSIONS_RUNTIME_FAILURE",
|
|
262
|
-
"The server could not read its config file; fix the file (gno doctor shows the error) and reload."
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
await assertInstanceBinding(ctxHolder, loaded.value);
|
|
266
|
-
return loaded.value;
|
|
254
|
+
/** Read this instance's config file, binding checked (see readInstanceConfig). */
|
|
255
|
+
function readConfigFile(ctxHolder: ContextHolder): Promise<Config> {
|
|
256
|
+
return readInstanceConfig(instanceIdentity(ctxHolder));
|
|
267
257
|
}
|
|
268
258
|
|
|
269
259
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -282,9 +272,7 @@ export async function refreshSessionsConfig(
|
|
|
282
272
|
store: SqliteAdapter
|
|
283
273
|
): Promise<Response | null> {
|
|
284
274
|
try {
|
|
285
|
-
|
|
286
|
-
if (Bun.deepEquals(config, ctxHolder.config)) return null;
|
|
287
|
-
await adoptConfig(ctxHolder, store, config);
|
|
275
|
+
await refreshServedConfig(servedConfig(ctxHolder, store));
|
|
288
276
|
return null;
|
|
289
277
|
} catch (error) {
|
|
290
278
|
return sessionsErrorResponse(error);
|
package/src/serve/server.ts
CHANGED
|
@@ -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
|
);
|
package/src/serve/status.ts
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep a running server's session-archive config current with its config
|
|
3
|
+
* file. `gno sessions source add/remove` and automation changes rewrite the
|
|
4
|
+
* file from another process; the REST routes and the MCP session tools both
|
|
5
|
+
* re-read it here, binding checked, and adopt it only when it changed.
|
|
6
|
+
*
|
|
7
|
+
* @module src/sessions/config-refresh
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Config } from "../config/types";
|
|
11
|
+
import type { SqliteAdapter } from "../store/sqlite/adapter";
|
|
12
|
+
|
|
13
|
+
import { getIndexDbPath } from "../app/constants";
|
|
14
|
+
import { loadConfig } from "../config";
|
|
15
|
+
import { collectionEgressPolicyEpoch } from "../core/collection-egress-policy-service";
|
|
16
|
+
import { assertSessionBinding } from "./binding";
|
|
17
|
+
import { SessionsError } from "./types";
|
|
18
|
+
|
|
19
|
+
/** The config/index pair a server instance was opened on. */
|
|
20
|
+
export interface SessionsInstance {
|
|
21
|
+
configPath: string;
|
|
22
|
+
indexName: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A running server's served config and the hooks that follow a swap. */
|
|
26
|
+
export interface ServedSessionsConfig extends SessionsInstance {
|
|
27
|
+
store: SqliteAdapter;
|
|
28
|
+
config: Config;
|
|
29
|
+
/** Swap the served config in memory (and anything derived from it). */
|
|
30
|
+
setConfig: (config: Config) => void;
|
|
31
|
+
invalidateEgressPolicy?: () => Promise<unknown>;
|
|
32
|
+
markContentMutation?: () => void;
|
|
33
|
+
markIndexMutation?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Refuse an archive config opened against a different index (and vice versa). */
|
|
37
|
+
export async function assertInstanceBinding(
|
|
38
|
+
instance: SessionsInstance,
|
|
39
|
+
config: Config
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
if (!config.sessions) return;
|
|
42
|
+
await assertSessionBinding({
|
|
43
|
+
config,
|
|
44
|
+
configPath: instance.configPath,
|
|
45
|
+
indexName: instance.indexName,
|
|
46
|
+
dbPath: getIndexDbPath(instance.indexName),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read this instance's config file, binding checked: unreadable is an error,
|
|
52
|
+
* never served stale, and a config rebound to another index is refused
|
|
53
|
+
* before it can touch this one.
|
|
54
|
+
*/
|
|
55
|
+
export async function readInstanceConfig(
|
|
56
|
+
instance: SessionsInstance
|
|
57
|
+
): Promise<Config> {
|
|
58
|
+
const loaded = await loadConfig(instance.configPath);
|
|
59
|
+
if (!loaded.ok) {
|
|
60
|
+
throw new SessionsError(
|
|
61
|
+
"SESSIONS_RUNTIME_FAILURE",
|
|
62
|
+
"The server could not read its config file; fix the file (gno doctor shows the error) and reload."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
await assertInstanceBinding(instance, loaded.value);
|
|
66
|
+
return loaded.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Adopt a config already persisted to the file: project collections and
|
|
71
|
+
* contexts into the open store, swap the served config, then refresh the
|
|
72
|
+
* egress policy (only when it changed) and mutation generations.
|
|
73
|
+
*/
|
|
74
|
+
export async function adoptServedConfig(
|
|
75
|
+
served: ServedSessionsConfig,
|
|
76
|
+
config: Config
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const collections = await served.store.syncCollections(config.collections);
|
|
79
|
+
if (!collections.ok) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Config saved but collection sync failed: ${collections.error.message}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const contexts = await served.store.syncContexts(config.contexts ?? []);
|
|
85
|
+
if (!contexts.ok) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Config saved but context sync failed: ${contexts.error.message}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const policyChanged =
|
|
91
|
+
collectionEgressPolicyEpoch(config) !==
|
|
92
|
+
collectionEgressPolicyEpoch(served.config);
|
|
93
|
+
served.setConfig(config);
|
|
94
|
+
if (policyChanged) await served.invalidateEgressPolicy?.();
|
|
95
|
+
served.markContentMutation?.();
|
|
96
|
+
served.markIndexMutation?.();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Adopt the config file when it changed underneath the running server.
|
|
101
|
+
* Returns the config to serve; throws a `SessionsError` when the file is
|
|
102
|
+
* unreadable or bound to another index, leaving the served config as it was.
|
|
103
|
+
*/
|
|
104
|
+
export async function refreshServedConfig(
|
|
105
|
+
served: ServedSessionsConfig
|
|
106
|
+
): Promise<Config> {
|
|
107
|
+
const config = await readInstanceConfig(served);
|
|
108
|
+
if (Bun.deepEquals(config, served.config)) return served.config;
|
|
109
|
+
await adoptServedConfig(served, config);
|
|
110
|
+
return config;
|
|
111
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: drop the low-selectivity documents(active) index.
|
|
3
|
+
*
|
|
4
|
+
* GNO keeps no planner statistics, so SQLite treated `idx_documents_active`
|
|
5
|
+
* (matching nearly every row) as a peer of selective indexes and chose it
|
|
6
|
+
* for `<column> = ? AND active = 1` lookups, turning point lookups and
|
|
7
|
+
* per-chunk EXISTS probes into whole-table walks. No query filters on
|
|
8
|
+
* inactive documents, so the index serves no lookup.
|
|
9
|
+
*
|
|
10
|
+
* @module src/store/migrations/033-drop-documents-active-index
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Database } from "bun:sqlite";
|
|
14
|
+
|
|
15
|
+
import type { Migration } from "./runner";
|
|
16
|
+
|
|
17
|
+
export const migration: Migration = {
|
|
18
|
+
version: 33,
|
|
19
|
+
name: "drop_documents_active_index",
|
|
20
|
+
|
|
21
|
+
up(db: Database): void {
|
|
22
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_active");
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
down(db: Database): void {
|
|
26
|
+
db.exec(
|
|
27
|
+
"CREATE INDEX IF NOT EXISTS idx_documents_active ON documents(active)"
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store each collection's effective link workspace (derived from the
|
|
3
|
+
* filesystem at config sync) so query-time link resolution, status output
|
|
4
|
+
* and graph projection fingerprints read one consistent membership.
|
|
5
|
+
*/
|
|
6
|
+
import type { Migration } from "./runner";
|
|
7
|
+
|
|
8
|
+
const COLUMNS: Array<[string, string]> = [
|
|
9
|
+
["real_path", "TEXT"],
|
|
10
|
+
["workspace_root", "TEXT"],
|
|
11
|
+
[
|
|
12
|
+
"workspace_source",
|
|
13
|
+
"TEXT NOT NULL DEFAULT 'none' CHECK (workspace_source IN ('none', 'detected', 'configured', 'disabled', 'unavailable'))",
|
|
14
|
+
],
|
|
15
|
+
["workspace_nested", "TEXT"],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const existingColumns = (db: Parameters<Migration["up"]>[0]): Set<string> =>
|
|
19
|
+
new Set(
|
|
20
|
+
db
|
|
21
|
+
.query<{ name: string }, []>("PRAGMA table_info(collections)")
|
|
22
|
+
.all()
|
|
23
|
+
.map((row) => row.name)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const migration: Migration = {
|
|
27
|
+
version: 34,
|
|
28
|
+
name: "collection_link_workspace",
|
|
29
|
+
|
|
30
|
+
up(db): void {
|
|
31
|
+
const columns = existingColumns(db);
|
|
32
|
+
for (const [name, definition] of COLUMNS) {
|
|
33
|
+
if (!columns.has(name)) {
|
|
34
|
+
db.exec(`ALTER TABLE collections ADD COLUMN ${name} ${definition}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
down(db): void {
|
|
40
|
+
const columns = existingColumns(db);
|
|
41
|
+
for (const [name] of [...COLUMNS].reverse()) {
|
|
42
|
+
if (columns.has(name)) {
|
|
43
|
+
db.exec(`ALTER TABLE collections DROP COLUMN ${name}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -46,6 +46,8 @@ import { migration as m029 } from "./029-graph-reference-state";
|
|
|
46
46
|
import { migration as m030 } from "./030-typed-metadata";
|
|
47
47
|
import { migration as m031 } from "./031-runtime-independent-vectors";
|
|
48
48
|
import { migration as m032 } from "./032-vector-runtime-callers";
|
|
49
|
+
import { migration as m033 } from "./033-drop-documents-active-index";
|
|
50
|
+
import { migration as m034 } from "./034-collection-link-workspace";
|
|
49
51
|
|
|
50
52
|
/** All migrations in order */
|
|
51
53
|
export const migrations = [
|
|
@@ -81,4 +83,6 @@ export const migrations = [
|
|
|
81
83
|
m030,
|
|
82
84
|
m031,
|
|
83
85
|
m032,
|
|
86
|
+
m033,
|
|
87
|
+
m034,
|
|
84
88
|
];
|