@gmickel/gno 2.7.0 → 2.8.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 +26 -30
- package/assets/skill/SKILL.md +8 -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/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +48 -6
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +18 -3
- 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 +15 -2
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/status.schema.json +27 -16
- package/src/cli/commands/ask.ts +31 -12
- 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/links.ts +34 -131
- package/src/cli/commands/ls.ts +6 -1
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +5 -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 +145 -25
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +19 -4
- package/src/core/audit.ts +67 -15
- 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 +24 -5
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-workspace.ts +324 -0
- package/src/core/request-receipts.ts +63 -9
- package/src/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +22 -10
- package/src/embed/variant-backlog.ts +34 -9
- 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 +22 -1
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +19 -0
- 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 +3 -0
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +22 -6
- 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/types.ts +6 -3
- package/src/serve/embed-scheduler.ts +2 -2
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/host-path-redaction.ts +51 -14
- package/src/serve/public/components/BootstrapStatus.tsx +5 -3
- package/src/serve/public/components/CaptureModal.tsx +1 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
- package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
- package/src/serve/public/components/FirstRunWizard.tsx +4 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/pages/Collections.tsx +16 -13
- package/src/serve/public/pages/Connectors.tsx +7 -4
- package/src/serve/public/pages/Dashboard.tsx +8 -6
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/resident-runtime.ts +7 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/links.ts +13 -0
- package/src/serve/routes/sessions.ts +81 -37
- package/src/serve/server.ts +5 -3
- package/src/serve/status-model.ts +10 -6
- package/src/serve/status.ts +2 -7
- 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 +392 -233
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +252 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/src/store/sqlite/workspace-link-resolver.ts +654 -0
- package/src/store/types.ts +49 -3
- package/src/store/vector/stats.ts +1 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
package/src/mcp/tools/links.ts
CHANGED
|
@@ -219,6 +219,8 @@ interface BacklinksInput {
|
|
|
219
219
|
interface BacklinkOutput {
|
|
220
220
|
sourceDocUri: string;
|
|
221
221
|
sourceDocTitle?: string;
|
|
222
|
+
/** Collection of the linking document. */
|
|
223
|
+
sourceCollection?: string;
|
|
222
224
|
linkText?: string;
|
|
223
225
|
position: { startLine: number; startCol: number };
|
|
224
226
|
}
|
|
@@ -308,6 +310,7 @@ export function handleBacklinks(
|
|
|
308
310
|
(b: BacklinkRow) => ({
|
|
309
311
|
sourceDocUri: b.sourceDocUri,
|
|
310
312
|
...(b.sourceDocTitle && { sourceDocTitle: b.sourceDocTitle }),
|
|
313
|
+
...(b.sourceCollection && { sourceCollection: b.sourceCollection }),
|
|
311
314
|
...(b.linkText && { linkText: b.linkText }),
|
|
312
315
|
position: { startLine: b.startLine, startCol: b.startCol },
|
|
313
316
|
})
|
|
@@ -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
|
@@ -4,27 +4,38 @@
|
|
|
4
4
|
* @module src/mcp/tools/status
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { IndexStatus } from "../../store/types";
|
|
7
|
+
import type { CollectionStatus, IndexStatus } from "../../store/types";
|
|
8
8
|
import type { ToolContext } from "../server";
|
|
9
9
|
|
|
10
10
|
import { buildContentTypeBoostStatus } from "../../config/content-types";
|
|
11
11
|
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
12
|
+
import { OWNER_CONFIG_PATH_FIELDS, withoutFields } from "../../core/host-paths";
|
|
13
|
+
import { formatLinkWorkspace } from "../../core/link-workspace";
|
|
12
14
|
import { formatVectorPartitionLines } from "../../core/vector-partition-status";
|
|
13
15
|
import { resolveModelUri } from "../../llm/registry";
|
|
14
16
|
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
17
|
+
import { exposesHostPaths } from "../context";
|
|
15
18
|
import { runTool, type ToolResult } from "./index";
|
|
16
19
|
|
|
17
20
|
type StatusInput = Record<string, never>;
|
|
18
21
|
|
|
22
|
+
/** Status as the caller sees it: HTTP callers get no owner config paths. */
|
|
23
|
+
type StatusView = Omit<IndexStatus, "configPath" | "dbPath" | "collections"> &
|
|
24
|
+
Partial<Pick<IndexStatus, "configPath" | "dbPath">> & {
|
|
25
|
+
collections: Array<
|
|
26
|
+
Omit<CollectionStatus, "path"> & Partial<Pick<CollectionStatus, "path">>
|
|
27
|
+
>;
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
/**
|
|
20
31
|
* Format status as text for MCP content.
|
|
21
32
|
*/
|
|
22
|
-
function formatStatus(status:
|
|
33
|
+
function formatStatus(status: StatusView): string {
|
|
23
34
|
const lines: string[] = [];
|
|
24
35
|
|
|
25
36
|
lines.push(`Index: ${status.indexName}`);
|
|
26
|
-
lines.push(`Config: ${status.configPath}`);
|
|
27
|
-
lines.push(`Database: ${status.dbPath}`);
|
|
37
|
+
if (status.configPath) lines.push(`Config: ${status.configPath}`);
|
|
38
|
+
if (status.dbPath) lines.push(`Database: ${status.dbPath}`);
|
|
28
39
|
lines.push(`Health: ${status.healthy ? "OK" : "DEGRADED"}`);
|
|
29
40
|
if ("resident" in status) {
|
|
30
41
|
const resident = status.resident as NonNullable<
|
|
@@ -45,6 +56,8 @@ function formatStatus(status: IndexStatus): string {
|
|
|
45
56
|
` ${c.name}: ${c.activeDocuments} docs, ${c.totalChunks} chunks` +
|
|
46
57
|
(c.embeddedChunks > 0 ? `, ${c.embeddedChunks} embedded` : "")
|
|
47
58
|
);
|
|
59
|
+
const workspace = formatLinkWorkspace(c);
|
|
60
|
+
if (workspace) lines.push(` Link workspace: ${workspace}`);
|
|
48
61
|
}
|
|
49
62
|
}
|
|
50
63
|
|
|
@@ -88,7 +101,7 @@ export function handleStatus(
|
|
|
88
101
|
return runTool(
|
|
89
102
|
ctx,
|
|
90
103
|
"gno_status",
|
|
91
|
-
async () => {
|
|
104
|
+
async (): Promise<StatusView> => {
|
|
92
105
|
const result = await ctx.store.getStatus({
|
|
93
106
|
embedModel: resolveModelUri(ctx.config, "embed"),
|
|
94
107
|
chunking: ctx.config.chunking ?? {},
|
|
@@ -98,7 +111,7 @@ export function handleStatus(
|
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
// Override configPath with actual path from context
|
|
101
|
-
|
|
114
|
+
const status = {
|
|
102
115
|
...result.value,
|
|
103
116
|
configPath: ctx.actualConfigPath,
|
|
104
117
|
contentTypeBoost: buildContentTypeBoostStatus(
|
|
@@ -107,6 +120,9 @@ export function handleStatus(
|
|
|
107
120
|
resident:
|
|
108
121
|
ctx.getResidentStatus?.() ?? createStandaloneResidentStatus("stdio"),
|
|
109
122
|
};
|
|
123
|
+
return exposesHostPaths(ctx)
|
|
124
|
+
? status
|
|
125
|
+
: withoutFields(status, OWNER_CONFIG_PATH_FIELDS);
|
|
110
126
|
},
|
|
111
127
|
formatStatus
|
|
112
128
|
);
|
|
@@ -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/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;
|
|
@@ -406,8 +412,6 @@ export type RerankedCandidate = FusionCandidate & {
|
|
|
406
412
|
|
|
407
413
|
/** Search pipeline configuration */
|
|
408
414
|
export interface PipelineConfig {
|
|
409
|
-
/** Expansion timeout in ms */
|
|
410
|
-
expansionTimeout: number;
|
|
411
415
|
/** Max candidates to rerank */
|
|
412
416
|
rerankCandidates: number;
|
|
413
417
|
/** RRF configuration */
|
|
@@ -418,7 +422,6 @@ export interface PipelineConfig {
|
|
|
418
422
|
|
|
419
423
|
/** Default pipeline configuration */
|
|
420
424
|
export const DEFAULT_PIPELINE_CONFIG: PipelineConfig = {
|
|
421
|
-
expansionTimeout: 5000,
|
|
422
425
|
rerankCandidates: 20,
|
|
423
426
|
rrf: DEFAULT_RRF_CONFIG,
|
|
424
427
|
blendingSchedule: DEFAULT_BLENDING_SCHEDULE,
|
|
@@ -202,8 +202,8 @@ export function createEmbedScheduler(deps: EmbedSchedulerDeps): EmbedScheduler {
|
|
|
202
202
|
} catch (cause) {
|
|
203
203
|
if (controller.signal.aborted)
|
|
204
204
|
return { result: { embedded: 0, errors: 0 } };
|
|
205
|
-
//
|
|
206
|
-
// failed pass like any other, never a silently dropped rejection.
|
|
205
|
+
// A worker exit aborts the whole pass (a deadline fails only its page).
|
|
206
|
+
// It is a failed pass like any other, never a silently dropped rejection.
|
|
207
207
|
return {
|
|
208
208
|
result: { embedded: 0, errors: 0 },
|
|
209
209
|
failure: cause instanceof Error ? cause.message : String(cause),
|
|
@@ -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;
|
|
@@ -1,36 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* REST boundary for host paths: a caller the request-locality rule judges
|
|
3
|
-
* remote never receives an `absPath` field from any `/api/*` JSON response
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* remote never receives an `absPath` field from any `/api/*` JSON response,
|
|
4
|
+
* nor the host `path` fields of the status, collection, connector, and
|
|
5
|
+
* document-mutation routes (plus `configPath`/`dbPath` on status and the
|
|
6
|
+
* `file://` `uri` of a document save). Same-host callers (the loopback Web
|
|
7
|
+
* UI) keep them for Reveal, "Open original", and the Collections page.
|
|
8
|
+
* Routes that stream original source bytes are exempt: their body is the
|
|
9
|
+
* owner's file, not an API envelope.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
HOST_PATH_FIELDS,
|
|
14
|
+
OWNER_CONFIG_PATH_FIELDS,
|
|
15
|
+
withoutFields,
|
|
16
|
+
} from "../core/host-paths";
|
|
10
17
|
import {
|
|
11
18
|
isLocalClientRequest,
|
|
12
19
|
type RequestPeerServer,
|
|
13
20
|
} from "./request-locality";
|
|
14
21
|
|
|
15
|
-
const HOST_PATH_KEY = '"absPath"';
|
|
16
22
|
const SOURCE_BYTE_ROUTES = new Set(["/api/doc-asset"]);
|
|
23
|
+
const OWNER_CONFIG_FIELDS: ReadonlySet<string> = new Set([
|
|
24
|
+
...HOST_PATH_FIELDS,
|
|
25
|
+
...OWNER_CONFIG_PATH_FIELDS,
|
|
26
|
+
]);
|
|
27
|
+
/** Document mutations answer with the file's host `path`. */
|
|
28
|
+
const DOC_PATH_FIELDS: ReadonlySet<string> = new Set([
|
|
29
|
+
...HOST_PATH_FIELDS,
|
|
30
|
+
"path",
|
|
31
|
+
]);
|
|
32
|
+
/** A document save also names the file by a `file://` `uri`. */
|
|
33
|
+
const DOC_SAVE_FIELDS: ReadonlySet<string> = new Set([
|
|
34
|
+
...DOC_PATH_FIELDS,
|
|
35
|
+
"uri",
|
|
36
|
+
]);
|
|
37
|
+
const ROUTE_FIELDS = new Map<string, ReadonlySet<string>>([
|
|
38
|
+
["/api/status", OWNER_CONFIG_FIELDS],
|
|
39
|
+
["/api/collections", OWNER_CONFIG_FIELDS],
|
|
40
|
+
["/api/collections/:name", OWNER_CONFIG_FIELDS],
|
|
41
|
+
["/api/connectors", OWNER_CONFIG_FIELDS],
|
|
42
|
+
["/api/connectors/install", OWNER_CONFIG_FIELDS],
|
|
43
|
+
["/api/docs", DOC_PATH_FIELDS],
|
|
44
|
+
["/api/docs/:id", DOC_SAVE_FIELDS],
|
|
45
|
+
["/api/docs/:id/rename", DOC_PATH_FIELDS],
|
|
46
|
+
["/api/docs/:id/move", DOC_PATH_FIELDS],
|
|
47
|
+
["/api/docs/:id/duplicate", DOC_PATH_FIELDS],
|
|
48
|
+
["/api/docs/:id/trash", DOC_PATH_FIELDS],
|
|
49
|
+
["/api/docs/:id/editable-copy", DOC_PATH_FIELDS],
|
|
50
|
+
["/api/folders", DOC_PATH_FIELDS],
|
|
51
|
+
]);
|
|
17
52
|
|
|
18
53
|
type RouteHandler = (
|
|
19
54
|
req: Request,
|
|
20
55
|
server: RequestPeerServer
|
|
21
56
|
) => Response | Promise<Response>;
|
|
22
57
|
|
|
23
|
-
/** The JSON response with every `
|
|
58
|
+
/** The JSON response with every key in `fields` removed. */
|
|
24
59
|
export async function redactResponseHostPaths(
|
|
25
|
-
response: Response
|
|
60
|
+
response: Response,
|
|
61
|
+
fields: ReadonlySet<string>
|
|
26
62
|
): Promise<Response> {
|
|
27
63
|
const contentType = response.headers.get("content-type") ?? "";
|
|
28
64
|
if (!contentType.includes("application/json") || response.body === null) {
|
|
29
65
|
return response;
|
|
30
66
|
}
|
|
31
67
|
const text = await response.text();
|
|
32
|
-
const body = text.includes(
|
|
33
|
-
? JSON.stringify(
|
|
68
|
+
const body = [...fields].some((field) => text.includes(`"${field}"`))
|
|
69
|
+
? JSON.stringify(withoutFields(JSON.parse(text) as unknown, fields))
|
|
34
70
|
: text;
|
|
35
71
|
const headers = new Headers(response.headers);
|
|
36
72
|
headers.delete("content-length");
|
|
@@ -42,12 +78,12 @@ export async function redactResponseHostPaths(
|
|
|
42
78
|
}
|
|
43
79
|
|
|
44
80
|
const redacting =
|
|
45
|
-
(handler: RouteHandler): RouteHandler =>
|
|
81
|
+
(handler: RouteHandler, fields: ReadonlySet<string>): RouteHandler =>
|
|
46
82
|
async (req, server) => {
|
|
47
83
|
const response = await handler(req, server);
|
|
48
84
|
return isLocalClientRequest(req, server)
|
|
49
85
|
? response
|
|
50
|
-
: redactResponseHostPaths(response);
|
|
86
|
+
: redactResponseHostPaths(response, fields);
|
|
51
87
|
};
|
|
52
88
|
|
|
53
89
|
const isMethodTable = (route: unknown): route is Record<string, unknown> =>
|
|
@@ -62,14 +98,15 @@ export function withRemoteHostPathRedaction<T extends Record<string, unknown>>(
|
|
|
62
98
|
const wrapped: Record<string, unknown> = { ...routes };
|
|
63
99
|
for (const [path, route] of Object.entries(routes)) {
|
|
64
100
|
if (!path.startsWith("/api/") || SOURCE_BYTE_ROUTES.has(path)) continue;
|
|
101
|
+
const fields = ROUTE_FIELDS.get(path) ?? HOST_PATH_FIELDS;
|
|
65
102
|
if (typeof route === "function") {
|
|
66
|
-
wrapped[path] = redacting(route as RouteHandler);
|
|
103
|
+
wrapped[path] = redacting(route as RouteHandler, fields);
|
|
67
104
|
} else if (isMethodTable(route)) {
|
|
68
105
|
wrapped[path] = Object.fromEntries(
|
|
69
106
|
Object.entries(route).map(([method, handler]) => [
|
|
70
107
|
method,
|
|
71
108
|
typeof handler === "function"
|
|
72
|
-
? redacting(handler as RouteHandler)
|
|
109
|
+
? redacting(handler as RouteHandler, fields)
|
|
73
110
|
: handler,
|
|
74
111
|
])
|
|
75
112
|
);
|
|
@@ -105,9 +105,11 @@ export function BootstrapStatus({
|
|
|
105
105
|
</CardHeader>
|
|
106
106
|
<CardContent className="space-y-2 text-sm">
|
|
107
107
|
<p>Total cache: {bootstrap.cache.totalSizeLabel}</p>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
{bootstrap.cache.path && (
|
|
109
|
+
<p className="font-mono text-muted-foreground text-xs">
|
|
110
|
+
{bootstrap.cache.path}
|
|
111
|
+
</p>
|
|
112
|
+
)}
|
|
111
113
|
</CardContent>
|
|
112
114
|
</Card>
|
|
113
115
|
|
|
@@ -54,7 +54,8 @@ export interface CollectionModelDetails {
|
|
|
54
54
|
modelSources?: Record<ModelRole, ModelSource>;
|
|
55
55
|
models?: Partial<Record<ModelRole, string>>;
|
|
56
56
|
name: string;
|
|
57
|
-
path
|
|
57
|
+
/** Host path; omitted for remote callers. */
|
|
58
|
+
path?: string;
|
|
58
59
|
pattern?: string;
|
|
59
60
|
egressPolicy?: {
|
|
60
61
|
schemaVersion: "1.0";
|
|
@@ -110,7 +111,7 @@ const CODE_EXT_HINTS = [
|
|
|
110
111
|
] as const;
|
|
111
112
|
|
|
112
113
|
function collectionLooksCodeHeavy(collection: CollectionModelDetails): boolean {
|
|
113
|
-
const path = collection.path.toLowerCase();
|
|
114
|
+
const path = (collection.path ?? "").toLowerCase();
|
|
114
115
|
if (
|
|
115
116
|
CODE_PATH_HINTS.some(
|
|
116
117
|
(hint) => path.endsWith(hint) || path.includes(`${hint}/`)
|
|
@@ -284,17 +285,19 @@ export function CollectionModelDialog({
|
|
|
284
285
|
active preset for the rest of the workspace.
|
|
285
286
|
</DialogDescription>
|
|
286
287
|
</div>
|
|
287
|
-
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
288
|
+
{collection?.path && (
|
|
289
|
+
<div className="hidden shrink-0 max-w-[200px] space-y-1 border-border/15 border-l pl-4 lg:block">
|
|
290
|
+
<p className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
291
|
+
Path
|
|
292
|
+
</p>
|
|
293
|
+
<p
|
|
294
|
+
className="break-all font-mono text-[10px] leading-relaxed text-muted-foreground/50"
|
|
295
|
+
title={collection.path}
|
|
296
|
+
>
|
|
297
|
+
{collection.path}
|
|
298
|
+
</p>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
298
301
|
</div>
|
|
299
302
|
</DialogHeader>
|
|
300
303
|
|
|
@@ -13,6 +13,8 @@ export function CollectionsEmptyState({
|
|
|
13
13
|
suggestedCollections,
|
|
14
14
|
onAddCollection,
|
|
15
15
|
}: CollectionsEmptyStateProps) {
|
|
16
|
+
// A remote caller's suggestions carry no host path to prefill.
|
|
17
|
+
const quickPicks = suggestedCollections.filter(({ path }) => path);
|
|
16
18
|
return (
|
|
17
19
|
<div className="mx-auto max-w-3xl py-16 text-center">
|
|
18
20
|
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
|
|
@@ -29,7 +31,7 @@ export function CollectionsEmptyState({
|
|
|
29
31
|
<FolderPlusIcon className="mr-2 size-4" />
|
|
30
32
|
Add Folder
|
|
31
33
|
</Button>
|
|
32
|
-
{
|
|
34
|
+
{quickPicks.map((suggestion) => (
|
|
33
35
|
<Button
|
|
34
36
|
key={suggestion.path}
|
|
35
37
|
onClick={() => onAddCollection(suggestion.path)}
|
|
@@ -40,9 +42,9 @@ export function CollectionsEmptyState({
|
|
|
40
42
|
))}
|
|
41
43
|
</div>
|
|
42
44
|
|
|
43
|
-
{
|
|
45
|
+
{quickPicks.length > 0 && (
|
|
44
46
|
<div className="grid gap-3 text-left md:grid-cols-2">
|
|
45
|
-
{
|
|
47
|
+
{quickPicks.map((suggestion) => (
|
|
46
48
|
<div
|
|
47
49
|
className="rounded-xl border border-border/60 bg-background/70 p-4"
|
|
48
50
|
key={`${suggestion.path}-detail`}
|
|
@@ -235,6 +235,8 @@ export function FirstRunWizard({
|
|
|
235
235
|
? steps[activeIndex + 1]
|
|
236
236
|
: null;
|
|
237
237
|
const progressValue = getStepProgress(onboarding);
|
|
238
|
+
// A remote caller's suggestions carry no host path to prefill.
|
|
239
|
+
const quickPicks = onboarding.suggestedCollections.filter(({ path }) => path);
|
|
238
240
|
const isShowingRecommended = activeStep?.id === recommendedStepId;
|
|
239
241
|
const runRecommendedAction = () => {
|
|
240
242
|
if (!activeStep) {
|
|
@@ -277,7 +279,7 @@ export function FirstRunWizard({
|
|
|
277
279
|
</Button>
|
|
278
280
|
</div>
|
|
279
281
|
|
|
280
|
-
{
|
|
282
|
+
{quickPicks.length > 0 && (
|
|
281
283
|
<div className="space-y-3">
|
|
282
284
|
<div className="flex items-center justify-between gap-3">
|
|
283
285
|
<div>
|
|
@@ -295,7 +297,7 @@ export function FirstRunWizard({
|
|
|
295
297
|
<Badge variant="outline">Quick picks</Badge>
|
|
296
298
|
</div>
|
|
297
299
|
<div className="grid gap-3 md:grid-cols-2">
|
|
298
|
-
{
|
|
300
|
+
{quickPicks.map((suggestion) => (
|
|
299
301
|
<button
|
|
300
302
|
className="rounded-xl border border-border/70 bg-background/80 p-4 text-left shadow-sm transition-all hover:border-primary/35 hover:bg-background hover:shadow-md"
|
|
301
303
|
key={suggestion.path}
|