@gmickel/gno 2.6.0 → 2.7.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 +25 -30
- package/assets/skill/SKILL.md +5 -3
- package/assets/skill/cli-reference.md +9 -2
- package/assets/skill/mcp-reference.md +2 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.6.0.zip → gno-browser-clipper-v2.7.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -1
- package/spec/cli.md +109 -18
- package/spec/mcp.md +36 -2
- package/spec/output-schemas/ask.schema.json +1 -1
- package/spec/output-schemas/capture-receipt.schema.json +1 -1
- package/spec/output-schemas/collection-list.schema.json +2 -2
- package/spec/output-schemas/doctor.schema.json +88 -0
- package/spec/output-schemas/error.schema.json +11 -2
- package/spec/output-schemas/get.schema.json +1 -1
- package/spec/output-schemas/mcp-capture-result.schema.json +1 -2
- package/spec/output-schemas/memory-remember.schema.json +2 -2
- package/spec/output-schemas/multi-get.schema.json +4 -1
- package/spec/output-schemas/peek.schema.json +2 -9
- package/spec/output-schemas/resident-status.schema.json +22 -0
- package/spec/output-schemas/search-result.schema.json +1 -1
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +110 -16
- package/src/cli/commands/ask.ts +31 -12
- package/src/cli/commands/doctor.ts +54 -20
- package/src/cli/commands/embed.ts +41 -3
- package/src/cli/commands/ls.ts +6 -1
- package/src/cli/commands/query.ts +5 -0
- package/src/cli/commands/status.ts +63 -5
- package/src/cli/commands/vec.ts +54 -0
- package/src/cli/detach.ts +29 -1
- package/src/cli/errors.ts +13 -9
- package/src/cli/program.ts +53 -1
- package/src/core/capture-sync.ts +9 -2
- package/src/core/host-paths.ts +49 -0
- package/src/core/memory-remember.ts +4 -3
- package/src/core/request-receipts.ts +63 -9
- package/src/core/shutdown-budget.ts +6 -0
- package/src/core/vector-partition-status.ts +52 -0
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +145 -27
- package/src/embed/fingerprint.ts +6 -3
- package/src/embed/retry.ts +66 -27
- package/src/embed/variant-backlog.ts +48 -18
- package/src/embed/variant-retry.ts +31 -22
- package/src/index.ts +21 -2
- package/src/llm/inference-scope.ts +18 -0
- package/src/llm/native-worker/dispatcher.ts +2 -0
- package/src/llm/native-worker/embedding-identity.ts +42 -0
- package/src/llm/native-worker/protocol.ts +1 -0
- package/src/llm/types.ts +3 -0
- package/src/mcp/context.ts +9 -0
- package/src/mcp/resources/index.ts +6 -5
- package/src/mcp/tool-descriptions-core.ts +1 -1
- package/src/mcp/tools/capture.ts +1 -3
- package/src/mcp/tools/index.ts +11 -4
- package/src/mcp/tools/memory-remember.ts +1 -1
- package/src/mcp/tools/status.ts +23 -6
- package/src/pipeline/hybrid.ts +37 -7
- package/src/pipeline/vsearch.ts +14 -2
- package/src/serve/embed-scheduler.ts +133 -19
- package/src/serve/host-path-redaction.ts +116 -0
- 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/SessionSearch.tsx +2 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/hooks/use-api.ts +17 -2
- package/src/serve/public/lib/request-intent.ts +8 -0
- package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
- 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 +16 -11
- package/src/serve/public/pages/DocView.tsx +10 -5
- package/src/serve/public/pages/DocumentEditor.tsx +91 -14
- package/src/serve/public/pages/Search.tsx +1 -41
- package/src/serve/resident-runtime.ts +33 -1
- package/src/serve/resident-status.ts +13 -1
- package/src/serve/routes/sessions.ts +61 -5
- package/src/serve/server.ts +15 -12
- package/src/serve/status-model.ts +26 -6
- package/src/serve/status.ts +4 -7
- package/src/serve/watch-reconciliation-shared.ts +3 -0
- package/src/serve/watch-service-events.ts +3 -2
- package/src/serve/watch-service-run-flush.ts +35 -2
- package/src/serve/watch-service.ts +5 -0
- package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
- package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +24 -3
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/src/store/types.ts +11 -1
- package/src/store/vector/lazy.ts +46 -43
- package/src/store/vector/runtime-compat.ts +651 -0
- package/src/store/vector/sqlite-vec.ts +20 -2
- package/src/store/vector/status.ts +276 -35
- package/src/store/vector/types.ts +2 -0
- package/src/store/vector/variant-search.ts +71 -23
- package/src/store/vector/variants.ts +49 -14
- package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +0 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ContentTypeBoostStatus } from "../../config/content-types";
|
|
9
9
|
import type { ActivationStatus } from "../../core/activation-status";
|
|
10
10
|
import type { MemoryStatus } from "../../core/memory-diagnostics";
|
|
11
|
+
import type { BackgroundIssue } from "../../serve/status-model";
|
|
11
12
|
import type { IndexStatus } from "../../store/types";
|
|
12
13
|
|
|
13
14
|
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
@@ -24,11 +25,18 @@ import {
|
|
|
24
25
|
buildMemoryStatus,
|
|
25
26
|
formatMemoryStatusLines,
|
|
26
27
|
} from "../../core/memory-diagnostics";
|
|
28
|
+
import { formatVectorPartitionLines } from "../../core/vector-partition-status";
|
|
27
29
|
import { ModelCache } from "../../llm/cache";
|
|
28
30
|
import { getActivePreset, resolveModelUri } from "../../llm/registry";
|
|
29
31
|
import { getConnectorVerificationTargets } from "../../serve/connectors";
|
|
30
32
|
import { createStandaloneResidentStatus } from "../../serve/resident-status";
|
|
31
33
|
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
34
|
+
import {
|
|
35
|
+
formatBackgroundIssue,
|
|
36
|
+
residentIssues,
|
|
37
|
+
resolveProcessPaths,
|
|
38
|
+
statusProcess,
|
|
39
|
+
} from "../detach";
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* Options for status command.
|
|
@@ -54,9 +62,35 @@ export type StatusResult =
|
|
|
54
62
|
activation: ActivationStatus;
|
|
55
63
|
contentTypeBoost: ContentTypeBoostStatus;
|
|
56
64
|
memory: MemoryStatus;
|
|
65
|
+
backgroundIssues: ResidentBackgroundIssue[];
|
|
57
66
|
}
|
|
58
67
|
| { success: false; error: string };
|
|
59
68
|
|
|
69
|
+
/** A background issue reported by (or about) a running detached resident. */
|
|
70
|
+
export type ResidentBackgroundIssue = BackgroundIssue & {
|
|
71
|
+
process: "serve" | "daemon";
|
|
72
|
+
pid: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ask each detached resident for its background issues. Each request is
|
|
77
|
+
* bounded (500ms), so a hung resident is reported, never waited on.
|
|
78
|
+
*/
|
|
79
|
+
async function collectResidentIssues(): Promise<ResidentBackgroundIssue[]> {
|
|
80
|
+
const statuses = await Promise.all(
|
|
81
|
+
(["serve", "daemon"] as const).map((kind) =>
|
|
82
|
+
statusProcess({ kind, ...resolveProcessPaths(kind) })
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
return statuses.flatMap((processStatus) =>
|
|
86
|
+
residentIssues(processStatus).map((issue) => ({
|
|
87
|
+
process: processStatus.cmd,
|
|
88
|
+
pid: processStatus.pid ?? 0,
|
|
89
|
+
...issue,
|
|
90
|
+
}))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
60
94
|
function connectorProjectionLine(activation: ActivationStatus): string | null {
|
|
61
95
|
const { projected, total, truncated } = activation.connectorProjection;
|
|
62
96
|
if (!truncated) {
|
|
@@ -117,6 +151,12 @@ function formatTerminal(
|
|
|
117
151
|
if (indexStatus.embeddingBacklog > 0) {
|
|
118
152
|
lines.push(`Embedding backlog: ${indexStatus.embeddingBacklog} chunks`);
|
|
119
153
|
}
|
|
154
|
+
lines.push(
|
|
155
|
+
...formatVectorPartitionLines(
|
|
156
|
+
indexStatus.vectorPartitions,
|
|
157
|
+
indexStatus.vectorRuntime
|
|
158
|
+
)
|
|
159
|
+
);
|
|
120
160
|
|
|
121
161
|
const chunking = formatChunkingStatus(indexStatus.chunking);
|
|
122
162
|
if (chunking) lines.push(chunking);
|
|
@@ -205,6 +245,11 @@ function formatMarkdown(
|
|
|
205
245
|
lines.push(`- **Documents**: ${indexStatus.activeDocuments}`);
|
|
206
246
|
lines.push(`- **Chunks**: ${indexStatus.totalChunks}`);
|
|
207
247
|
lines.push(`- **Embedding backlog**: ${indexStatus.embeddingBacklog}`);
|
|
248
|
+
for (const line of formatVectorPartitionLines(
|
|
249
|
+
indexStatus.vectorPartitions,
|
|
250
|
+
indexStatus.vectorRuntime
|
|
251
|
+
))
|
|
252
|
+
lines.push(`- ${line.trim()}`);
|
|
208
253
|
if (indexStatus.typedMetadata)
|
|
209
254
|
lines.push(
|
|
210
255
|
`- **Typed metadata**: ${indexStatus.typedMetadata.pending} pending sync, ${indexStatus.typedMetadata.invalid} invalid`
|
|
@@ -318,6 +363,7 @@ export async function status(
|
|
|
318
363
|
activation,
|
|
319
364
|
contentTypeBoost: buildContentTypeBoostStatus(config.contentTypes ?? []),
|
|
320
365
|
memory: await buildMemoryStatus(store, config.collections),
|
|
366
|
+
backgroundIssues: await collectResidentIssues(),
|
|
321
367
|
};
|
|
322
368
|
} finally {
|
|
323
369
|
await store.close();
|
|
@@ -356,6 +402,8 @@ export function formatStatus(
|
|
|
356
402
|
totalDocuments: s.activeDocuments,
|
|
357
403
|
totalChunks: s.totalChunks,
|
|
358
404
|
embeddingBacklog: s.embeddingBacklog,
|
|
405
|
+
vectorPartitions: s.vectorPartitions,
|
|
406
|
+
vectorRuntime: s.vectorRuntime,
|
|
359
407
|
typedMetadata: s.typedMetadata,
|
|
360
408
|
chunking: s.chunking,
|
|
361
409
|
lastUpdated: s.lastUpdatedAt,
|
|
@@ -363,6 +411,9 @@ export function formatStatus(
|
|
|
363
411
|
contentTypeBoost: result.contentTypeBoost,
|
|
364
412
|
activation: result.activation,
|
|
365
413
|
memory: result.memory,
|
|
414
|
+
...(result.backgroundIssues.length
|
|
415
|
+
? { backgroundIssues: result.backgroundIssues }
|
|
416
|
+
: {}),
|
|
366
417
|
},
|
|
367
418
|
null,
|
|
368
419
|
2
|
|
@@ -378,10 +429,17 @@ export function formatStatus(
|
|
|
378
429
|
);
|
|
379
430
|
}
|
|
380
431
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
result.contentTypeBoost,
|
|
385
|
-
result.memory
|
|
432
|
+
const issueLines = result.backgroundIssues.map(
|
|
433
|
+
(issue) =>
|
|
434
|
+
` ${issue.process} (pid ${issue.pid}): ${formatBackgroundIssue(issue)}`
|
|
386
435
|
);
|
|
436
|
+
return [
|
|
437
|
+
formatTerminal(
|
|
438
|
+
result.status,
|
|
439
|
+
result.activation,
|
|
440
|
+
result.contentTypeBoost,
|
|
441
|
+
result.memory
|
|
442
|
+
),
|
|
443
|
+
...(issueLines.length ? ["", "Background issues:", ...issueLines] : []),
|
|
444
|
+
].join("\n");
|
|
387
445
|
}
|
package/src/cli/commands/vec.ts
CHANGED
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
createVectorIndexPort,
|
|
14
14
|
createVectorStatsPort,
|
|
15
15
|
} from "../../store/vector";
|
|
16
|
+
import {
|
|
17
|
+
dropVectorPartition,
|
|
18
|
+
type VectorPartitionStatus,
|
|
19
|
+
} from "../../store/vector/status";
|
|
16
20
|
|
|
17
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
22
|
// Types
|
|
@@ -294,3 +298,53 @@ export function formatVecRebuild(
|
|
|
294
298
|
|
|
295
299
|
return `Vec index rebuilt: ${result.count.toLocaleString()} vectors`;
|
|
296
300
|
}
|
|
301
|
+
|
|
302
|
+
export type VecDropResult =
|
|
303
|
+
| { success: true; partition: VectorPartitionStatus }
|
|
304
|
+
| { success: false; error: string };
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Drop an abandoned vector partition (shadow or legacy). Active partitions
|
|
308
|
+
* that retrieval uses are refused.
|
|
309
|
+
*/
|
|
310
|
+
export async function vecDrop(
|
|
311
|
+
partition: string,
|
|
312
|
+
options: VecOptions = {}
|
|
313
|
+
): Promise<VecDropResult> {
|
|
314
|
+
if (!(await isInitialized(options.configPath)))
|
|
315
|
+
return { success: false, error: "GNO not initialized. Run: gno init" };
|
|
316
|
+
const configResult = await loadConfig(options.configPath);
|
|
317
|
+
if (!configResult.ok)
|
|
318
|
+
return { success: false, error: configResult.error.message };
|
|
319
|
+
const store = new SqliteAdapter();
|
|
320
|
+
const openResult = await store.open(
|
|
321
|
+
getIndexDbPath(options.indexName),
|
|
322
|
+
configResult.value.ftsTokenizer,
|
|
323
|
+
configResult.value.busyTimeoutMs
|
|
324
|
+
);
|
|
325
|
+
if (!openResult.ok)
|
|
326
|
+
return { success: false, error: openResult.error.message };
|
|
327
|
+
try {
|
|
328
|
+
const dropped = await dropVectorPartition(store.getRawDb(), partition);
|
|
329
|
+
return dropped.ok
|
|
330
|
+
? { success: true, partition: dropped.partition }
|
|
331
|
+
: { success: false, error: dropped.error };
|
|
332
|
+
} finally {
|
|
333
|
+
await store.close();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function formatVecDrop(
|
|
338
|
+
result: VecDropResult,
|
|
339
|
+
options: { json?: boolean }
|
|
340
|
+
): string {
|
|
341
|
+
if (!result.success) {
|
|
342
|
+
return options.json
|
|
343
|
+
? JSON.stringify({ error: { code: "RUNTIME", message: result.error } })
|
|
344
|
+
: `Error: ${result.error}`;
|
|
345
|
+
}
|
|
346
|
+
const { partition } = result;
|
|
347
|
+
return options.json
|
|
348
|
+
? JSON.stringify({ dropped: partition }, null, 2)
|
|
349
|
+
: `Dropped ${partition.state} partition ${partition.id.slice(0, 12)} (${partition.provenance}, ${partition.owners} chunks)`;
|
|
350
|
+
}
|
package/src/cli/detach.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { mkdir, stat, unlink } from "node:fs/promises";
|
|
|
20
20
|
// node:path — no Bun path utils.
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
|
|
23
|
-
import type { ResidentStatus } from "../serve/status-model";
|
|
23
|
+
import type { BackgroundIssue, ResidentStatus } from "../serve/status-model";
|
|
24
24
|
|
|
25
25
|
import { VERSION, resolveDirs } from "../app/constants";
|
|
26
26
|
import { toAbsolutePath } from "../config/paths";
|
|
@@ -840,6 +840,34 @@ export async function statusProcess(
|
|
|
840
840
|
};
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
+
/** Issues a running detached resident reports, or that it failed to answer at all. */
|
|
844
|
+
export function residentIssues(status: ProcessStatus): BackgroundIssue[] {
|
|
845
|
+
if (!status.running) return [];
|
|
846
|
+
if (!status.resident)
|
|
847
|
+
return [
|
|
848
|
+
{
|
|
849
|
+
job: "resident",
|
|
850
|
+
state: "unresponsive",
|
|
851
|
+
consecutiveFailures: 0,
|
|
852
|
+
runningSeconds: null,
|
|
853
|
+
},
|
|
854
|
+
];
|
|
855
|
+
return status.resident.backgroundIssues ?? [];
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function formatBackgroundIssue(issue: BackgroundIssue): string {
|
|
859
|
+
switch (issue.state) {
|
|
860
|
+
case "unresponsive":
|
|
861
|
+
return "resident did not answer its status request within 500ms (busy or hung)";
|
|
862
|
+
case "failing":
|
|
863
|
+
return `background embed failing (${issue.consecutiveFailures} failed passes in a row; details in the log)`;
|
|
864
|
+
case "parked":
|
|
865
|
+
return `background embed parked after ${issue.consecutiveFailures} failed passes; pending chunks wait for new changes or \`gno embed\``;
|
|
866
|
+
case "overrunning":
|
|
867
|
+
return `background embed pass running for ${issue.runningSeconds ?? 0}s`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
843
871
|
/**
|
|
844
872
|
* Inspect the pid-file for a "live-foreign" signal — a live pid whose
|
|
845
873
|
* recorded gno version disagrees with the currently running binary. Returns
|
package/src/cli/errors.ts
CHANGED
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
// Error Types
|
|
10
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
/** Every code the CLI error model can emit; `error.schema.json` must list each. */
|
|
13
|
+
export const CLI_ERROR_CODES = [
|
|
14
|
+
"VALIDATION",
|
|
15
|
+
"RUNTIME",
|
|
16
|
+
"NOT_RUNNING",
|
|
17
|
+
"BUSY",
|
|
18
|
+
"AUDIT_FINDINGS",
|
|
19
|
+
"AUDIT_PARTIAL",
|
|
20
|
+
"CONTEXT_STALE",
|
|
21
|
+
"CONTEXT_CONFLICT",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type CliErrorCode = (typeof CLI_ERROR_CODES)[number];
|
|
21
25
|
|
|
22
26
|
export interface CliErrorOptions {
|
|
23
27
|
details?: Record<string, unknown>;
|
package/src/cli/program.ts
CHANGED
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
type GlobalOptions,
|
|
42
42
|
parseGlobalOptions,
|
|
43
43
|
} from "./context";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
DETACHED_CHILD_FLAG,
|
|
46
|
+
formatBackgroundIssue,
|
|
47
|
+
residentIssues,
|
|
48
|
+
} from "./detach";
|
|
45
49
|
import { CliError } from "./errors";
|
|
46
50
|
import {
|
|
47
51
|
assertFormatSupported,
|
|
@@ -3544,6 +3548,10 @@ function wireManagementCommands(program: Command): void {
|
|
|
3544
3548
|
.option("--model <uri>", "embedding model URI")
|
|
3545
3549
|
.option("--batch-size <num>", "batch size", "32")
|
|
3546
3550
|
.option("--force", "regenerate all embeddings")
|
|
3551
|
+
.option(
|
|
3552
|
+
"--new-partition",
|
|
3553
|
+
"confirm building a separate vector partition for an incompatible runtime"
|
|
3554
|
+
)
|
|
3547
3555
|
.option("--dry-run", "show what would be done")
|
|
3548
3556
|
.option("--json", "JSON output")
|
|
3549
3557
|
).action(
|
|
@@ -3567,6 +3575,7 @@ function wireManagementCommands(program: Command): void {
|
|
|
3567
3575
|
force: Boolean(cmdOpts.force),
|
|
3568
3576
|
dryRun: Boolean(cmdOpts.dryRun),
|
|
3569
3577
|
yes: globals.yes,
|
|
3578
|
+
newPartition: Boolean(cmdOpts.newPartition),
|
|
3570
3579
|
json: format === "json",
|
|
3571
3580
|
verbose: globals.verbose,
|
|
3572
3581
|
offline: globals.offline,
|
|
@@ -3675,6 +3684,43 @@ function wireVecCommands(program: Command): void {
|
|
|
3675
3684
|
);
|
|
3676
3685
|
});
|
|
3677
3686
|
|
|
3687
|
+
// vec drop
|
|
3688
|
+
addWriteLeaseFlags(
|
|
3689
|
+
vecCmd
|
|
3690
|
+
.command("drop <partition>")
|
|
3691
|
+
.description(
|
|
3692
|
+
"Drop an abandoned shadow vector partition (id prefix from gno status)"
|
|
3693
|
+
)
|
|
3694
|
+
.option("--json", "JSON output")
|
|
3695
|
+
).action(async (partition: string, cmdOpts: Record<string, unknown>) => {
|
|
3696
|
+
const format = getFormat(cmdOpts);
|
|
3697
|
+
const globals = getGlobals();
|
|
3698
|
+
|
|
3699
|
+
const { vecDrop, formatVecDrop } = await import("./commands/vec");
|
|
3700
|
+
const lease = parseWriteLeaseFlags(cmdOpts);
|
|
3701
|
+
const result = await withCliWriteLease(
|
|
3702
|
+
{
|
|
3703
|
+
indexName: globals.index,
|
|
3704
|
+
lockWaitMs: lease.lockWaitMs,
|
|
3705
|
+
noWait: lease.noWait,
|
|
3706
|
+
},
|
|
3707
|
+
() =>
|
|
3708
|
+
vecDrop(partition, {
|
|
3709
|
+
configPath: globals.config,
|
|
3710
|
+
indexName: globals.index,
|
|
3711
|
+
})
|
|
3712
|
+
);
|
|
3713
|
+
throwIfWriteLeaseBusy(result, format === "json");
|
|
3714
|
+
|
|
3715
|
+
if (!result.success) {
|
|
3716
|
+
throw new CliError("VALIDATION", result.error);
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
process.stdout.write(
|
|
3720
|
+
`${formatVecDrop(result, { json: format === "json" })}\n`
|
|
3721
|
+
);
|
|
3722
|
+
});
|
|
3723
|
+
|
|
3678
3724
|
// vec rebuild
|
|
3679
3725
|
addWriteLeaseFlags(
|
|
3680
3726
|
vecCmd
|
|
@@ -5017,6 +5063,9 @@ async function runDaemonStatus(deps: DaemonStatusDeps): Promise<void> {
|
|
|
5017
5063
|
} else {
|
|
5018
5064
|
process.stdout.write(` (${status.log_size_bytes} bytes)\n`);
|
|
5019
5065
|
}
|
|
5066
|
+
for (const issue of residentIssues(status)) {
|
|
5067
|
+
process.stdout.write(` issue ${formatBackgroundIssue(issue)}\n`);
|
|
5068
|
+
}
|
|
5020
5069
|
if (findings) {
|
|
5021
5070
|
process.stdout.write(
|
|
5022
5071
|
` findings ${formatFindingsRunStatusLine(findings)}\n`
|
|
@@ -5372,6 +5421,9 @@ async function runServeStatus(deps: ServeStatusDeps): Promise<void> {
|
|
|
5372
5421
|
} else {
|
|
5373
5422
|
process.stdout.write(` (${status.log_size_bytes} bytes)\n`);
|
|
5374
5423
|
}
|
|
5424
|
+
for (const issue of residentIssues(status)) {
|
|
5425
|
+
process.stdout.write(` issue ${formatBackgroundIssue(issue)}\n`);
|
|
5426
|
+
}
|
|
5375
5427
|
|
|
5376
5428
|
if (foreign) {
|
|
5377
5429
|
// Terminal mode: emit the operator-facing warning on stderr. JSON
|
package/src/core/capture-sync.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Collection, Config } from "../config/types";
|
|
|
10
10
|
import type { StorePort } from "../store/types";
|
|
11
11
|
import type { CaptureIndexStatus } from "./capture";
|
|
12
12
|
|
|
13
|
+
import { buildUri } from "../app/constants";
|
|
13
14
|
import {
|
|
14
15
|
type CollectionSyncResult,
|
|
15
16
|
defaultSyncService,
|
|
@@ -30,9 +31,14 @@ export class CaptureSyncError extends Error {
|
|
|
30
31
|
/** The sync failure itself, without the write half of the message. */
|
|
31
32
|
readonly syncError: string;
|
|
32
33
|
|
|
33
|
-
constructor(input: {
|
|
34
|
+
constructor(input: {
|
|
35
|
+
absPath: string;
|
|
36
|
+
relPath: string;
|
|
37
|
+
uri: string;
|
|
38
|
+
cause: string;
|
|
39
|
+
}) {
|
|
34
40
|
super(
|
|
35
|
-
`Capture written to ${input.
|
|
41
|
+
`Capture written to ${input.uri} but lexical sync failed: ${input.cause}. Run gno update to retry indexing.`
|
|
36
42
|
);
|
|
37
43
|
this.name = "CaptureSyncError";
|
|
38
44
|
this.absPath = input.absPath;
|
|
@@ -82,6 +88,7 @@ export async function syncCapturedFile(
|
|
|
82
88
|
throw new CaptureSyncError({
|
|
83
89
|
absPath: input.absPath,
|
|
84
90
|
relPath: input.relPath,
|
|
91
|
+
uri: buildUri(input.collection.name, input.relPath),
|
|
85
92
|
cause,
|
|
86
93
|
});
|
|
87
94
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host path redaction for callers that are not on the owner's machine.
|
|
3
|
+
*
|
|
4
|
+
* Result payloads name a source file's host location in `absPath`
|
|
5
|
+
* (`source.absPath` on search/get results, top-level on capture, memory and
|
|
6
|
+
* peek receipts). A remote caller identifies documents by `uri` and
|
|
7
|
+
* collection-relative `relPath` instead, so every `absPath` key is removed
|
|
8
|
+
* before a payload leaves a remote-reachable surface.
|
|
9
|
+
*
|
|
10
|
+
* Status, collection, and connector payloads also name owner configuration
|
|
11
|
+
* locations: `configPath`, `dbPath`, and `path` (collection roots, suggested
|
|
12
|
+
* folders, model cache and model files, connector targets). A remote caller
|
|
13
|
+
* identifies a collection by `name`, so those keys are removed from those
|
|
14
|
+
* payloads too.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Document host path key of result payloads. */
|
|
18
|
+
export const HOST_PATH_FIELDS: ReadonlySet<string> = new Set(["absPath"]);
|
|
19
|
+
|
|
20
|
+
/** Owner configuration path keys of status, collection, and connector payloads. */
|
|
21
|
+
export const OWNER_CONFIG_PATH_FIELDS: ReadonlySet<string> = new Set([
|
|
22
|
+
"configPath",
|
|
23
|
+
"dbPath",
|
|
24
|
+
"path",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const isPlainObject = (value: object): boolean => {
|
|
28
|
+
const proto = Object.getPrototypeOf(value) as object | null;
|
|
29
|
+
return proto === Object.prototype || proto === null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Deep copy of a JSON-shaped value with every key in `fields` removed. */
|
|
33
|
+
export function withoutFields<T>(value: T, fields: ReadonlySet<string>): T {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return value.map((item: unknown) => withoutFields(item, fields)) as T;
|
|
36
|
+
}
|
|
37
|
+
if (value === null || typeof value !== "object" || !isPlainObject(value)) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
const copy: Record<string, unknown> = {};
|
|
41
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
42
|
+
if (!fields.has(key)) copy[key] = withoutFields(entry, fields);
|
|
43
|
+
}
|
|
44
|
+
return copy as T;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Deep copy of a JSON-shaped value with every `absPath` key removed. */
|
|
48
|
+
export const withoutHostPaths = <T>(value: T): T =>
|
|
49
|
+
withoutFields(value, HOST_PATH_FIELDS);
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
RememberResult,
|
|
25
25
|
} from "./memory-types";
|
|
26
26
|
|
|
27
|
+
import { buildUri } from "../app/constants";
|
|
27
28
|
import { defaultSyncService, withContentTypeRules } from "../ingestion";
|
|
28
29
|
import { withWriteLock } from "./file-lock";
|
|
29
30
|
import { atomicCreate } from "./file-ops";
|
|
@@ -399,7 +400,7 @@ export async function rememberFact(
|
|
|
399
400
|
if (sync.status === "failed") {
|
|
400
401
|
throw new MemoryError(
|
|
401
402
|
"MEMORY_SYNC_FAILED",
|
|
402
|
-
`Memory record written to ${
|
|
403
|
+
`Memory record written to ${buildUri(collection.name, relPath)} but lexical sync failed: ${sync.error}. Run gno update to retry indexing.`
|
|
403
404
|
);
|
|
404
405
|
}
|
|
405
406
|
const written = (doc as { value: DocumentRow }).value;
|
|
@@ -415,13 +416,13 @@ export async function rememberFact(
|
|
|
415
416
|
if (!projected) {
|
|
416
417
|
throw new MemoryError(
|
|
417
418
|
"MEMORY_SUPERSEDE_PROJECTION_FAILED",
|
|
418
|
-
`Successor written to ${
|
|
419
|
+
`Successor written to ${buildUri(collection.name, relPath)} but its supersedes edge did not project${projectionErrors ? ` (${projectionErrors})` : ""}; the predecessor still reads as current. Run gno update to retry the projection.`
|
|
419
420
|
);
|
|
420
421
|
}
|
|
421
422
|
} else if (projectionErrors.length > 0) {
|
|
422
423
|
throw new MemoryError(
|
|
423
424
|
"MEMORY_SYNC_FAILED",
|
|
424
|
-
`Memory record written to ${
|
|
425
|
+
`Memory record written to ${buildUri(collection.name, relPath)} but typed-edge projection failed: ${projectionErrors}. Run gno update to retry indexing.`
|
|
425
426
|
);
|
|
426
427
|
}
|
|
427
428
|
const record: MemoryFact = {
|
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { Database } from "bun:sqlite";
|
|
14
|
-
// node:fs/promises chmod/mkdir: filesystem structure ops, no Bun equivalent
|
|
15
|
-
import { chmod, mkdir } from "node:fs/promises";
|
|
14
|
+
// node:fs/promises chmod/mkdir/lstat/readdir: filesystem structure ops, no Bun equivalent
|
|
15
|
+
import { chmod, lstat, mkdir, readdir } from "node:fs/promises";
|
|
16
16
|
// node:path has no Bun path utilities
|
|
17
17
|
import { basename, dirname, join } from "node:path";
|
|
18
18
|
|
|
19
19
|
import { MCP_ERRORS } from "./errors";
|
|
20
20
|
import { withWriteLock } from "./file-lock";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
isOwnerOnlyDescriptor,
|
|
23
|
+
windowsDirectoryDescriptor,
|
|
24
|
+
windowsPrivatePath,
|
|
25
|
+
} from "./windows-private-path";
|
|
22
26
|
import { writeLeasePath } from "./write-lease";
|
|
23
27
|
|
|
24
28
|
/** Committed receipts keep their full outcome this long, then become tombstones. */
|
|
@@ -221,25 +225,75 @@ CREATE TABLE IF NOT EXISTS request_receipts (
|
|
|
221
225
|
/** Ledger directories whose owner-only Windows DACL this process verified. */
|
|
222
226
|
const privateLedgerDirs = new Set<string>();
|
|
223
227
|
|
|
228
|
+
/** Inside the ledger directory: what the last authoritative check verified. */
|
|
229
|
+
const LEDGER_DIR_MARKER = ".owner-only-verified";
|
|
230
|
+
|
|
231
|
+
export interface PrivateDirAcl {
|
|
232
|
+
/** Authoritative check (spawns PowerShell); `create` sets the owner-only DACL first. */
|
|
233
|
+
verify: (dir: string, create: boolean) => Promise<void>;
|
|
234
|
+
/** In-process owner and DACL bytes, or null when they cannot be read. */
|
|
235
|
+
descriptor: (dir: string) => Uint8Array | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const WINDOWS_ACL: PrivateDirAcl = {
|
|
239
|
+
verify: windowsPrivatePath,
|
|
240
|
+
descriptor: windowsDirectoryDescriptor,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** Directory identity plus descriptor digest; null unless owner-only. */
|
|
244
|
+
async function ledgerDirStamp(
|
|
245
|
+
dir: string,
|
|
246
|
+
acl: PrivateDirAcl
|
|
247
|
+
): Promise<string | null> {
|
|
248
|
+
const { dev, ino } = await lstat(dir, { bigint: true });
|
|
249
|
+
const descriptor = acl.descriptor(dir);
|
|
250
|
+
if (!descriptor || !isOwnerOnlyDescriptor(descriptor)) return null;
|
|
251
|
+
const digest = new Bun.CryptoHasher("sha256")
|
|
252
|
+
.update(descriptor)
|
|
253
|
+
.digest("hex");
|
|
254
|
+
return `${dev}:${ino}:${digest}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
224
257
|
/**
|
|
225
258
|
* Windows ignores POSIX modes: give a new ledger directory the current-user
|
|
226
259
|
* DACL before SQLite creates the database or its WAL/SHM (they inherit it),
|
|
227
260
|
* and refuse an existing one that grants another principal access.
|
|
261
|
+
*
|
|
262
|
+
* The PowerShell check costs a process start, so its success is recorded in a
|
|
263
|
+
* marker inside the directory. A later open skips it only when the directory
|
|
264
|
+
* is the same object and its owner and DACL are byte-identical to the verified
|
|
265
|
+
* ones and still owner-only; reading the marker at all requires access that
|
|
266
|
+
* owner-only DACL grants. Anything else re-runs the authoritative check.
|
|
267
|
+
*
|
|
268
|
+
* An existing but empty directory is secured like a new one: a first open
|
|
269
|
+
* interrupted before its DACL was set leaves exactly that, and nothing inside
|
|
270
|
+
* it could have been exposed.
|
|
228
271
|
*/
|
|
229
|
-
async function
|
|
272
|
+
export async function securePrivateLedgerDir(
|
|
230
273
|
dir: string,
|
|
231
|
-
created:
|
|
274
|
+
created: boolean,
|
|
275
|
+
acl: PrivateDirAcl = WINDOWS_ACL
|
|
232
276
|
): Promise<void> {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
const marker = Bun.file(join(dir, LEDGER_DIR_MARKER));
|
|
278
|
+
if (!created) {
|
|
279
|
+
const stamp = await ledgerDirStamp(dir, acl);
|
|
280
|
+
if (stamp !== null && stamp === (await marker.text().catch(() => null)))
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const secure = created || (await readdir(dir)).length === 0;
|
|
284
|
+
await acl.verify(dir, secure);
|
|
285
|
+
const stamp = await ledgerDirStamp(dir, acl);
|
|
286
|
+
if (stamp !== null) await Bun.write(marker, stamp);
|
|
236
287
|
}
|
|
237
288
|
|
|
238
289
|
async function openLedger(path: string): Promise<Database> {
|
|
239
290
|
try {
|
|
240
291
|
const dir = dirname(path);
|
|
241
292
|
const created = await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
242
|
-
|
|
293
|
+
if (process.platform === "win32" && !privateLedgerDirs.has(dir)) {
|
|
294
|
+
await securePrivateLedgerDir(dir, created !== undefined);
|
|
295
|
+
privateLedgerDirs.add(dir);
|
|
296
|
+
}
|
|
243
297
|
const db = new Database(path, { create: true, strict: true });
|
|
244
298
|
try {
|
|
245
299
|
// POSIX: private before any journal file exists (they inherit this mode).
|
|
@@ -4,6 +4,12 @@ export const SHUTDOWN_ABORT_MS = 5_000;
|
|
|
4
4
|
export const SHUTDOWN_EXIT_MS = 1_000;
|
|
5
5
|
// The detached parent must not be killed before it can reap its native child.
|
|
6
6
|
export const RESIDENT_STOP_GRACE_MS = 12_000;
|
|
7
|
+
/**
|
|
8
|
+
* Longest synchronous SQLite busy wait a resident allows itself. A signal is
|
|
9
|
+
* handled only after the wait in progress ends, so this plus the shutdown
|
|
10
|
+
* clock must fit inside the stop grace (asserted in shutdown-budget tests).
|
|
11
|
+
*/
|
|
12
|
+
export const RESIDENT_BUSY_TIMEOUT_MS = 500;
|
|
7
13
|
|
|
8
14
|
/** Observe settlement without abandoning rejection handling or retaining a timer. */
|
|
9
15
|
export async function settlesBy(
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
VectorPartitionStatus,
|
|
3
|
+
VectorRuntimeStatus,
|
|
4
|
+
} from "../store/vector/status";
|
|
5
|
+
|
|
6
|
+
const shortId = (id: string): string => id.slice(0, 12);
|
|
7
|
+
|
|
8
|
+
function runtimeLine(runtime: VectorRuntimeStatus): string {
|
|
9
|
+
const label = runtime.label ? ` (${runtime.label})` : "";
|
|
10
|
+
if (runtime.state === "vectors")
|
|
11
|
+
return ` This runtime${label} reads ${shortId(runtime.partition ?? "")}`;
|
|
12
|
+
if (runtime.state === "unavailable")
|
|
13
|
+
return ` This runtime${label} uses lexical retrieval only: ${runtime.reason}`;
|
|
14
|
+
return " This runtime has not resolved a partition yet; run a query or `gno embed`";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The caller's runtime, then one line per partition. The healthy case (one
|
|
19
|
+
* current partition this runtime reads) prints nothing, so default status
|
|
20
|
+
* output is unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export function formatVectorPartitionLines(
|
|
23
|
+
partitions?: VectorPartitionStatus[],
|
|
24
|
+
runtime?: VectorRuntimeStatus
|
|
25
|
+
): string[] {
|
|
26
|
+
if (!partitions?.length) return [];
|
|
27
|
+
const [only] = partitions;
|
|
28
|
+
if (
|
|
29
|
+
partitions.length === 1 &&
|
|
30
|
+
only?.retrieval &&
|
|
31
|
+
!only.legacy &&
|
|
32
|
+
!only.incompatibleRuntimes.length
|
|
33
|
+
)
|
|
34
|
+
return [];
|
|
35
|
+
const lines = ["Vector partitions:"];
|
|
36
|
+
if (runtime) lines.push(runtimeLine(runtime));
|
|
37
|
+
for (const p of partitions) {
|
|
38
|
+
const role = p.retrieval
|
|
39
|
+
? " (used by this runtime's retrieval)"
|
|
40
|
+
: p.droppable
|
|
41
|
+
? ` (drop with: gno vec drop ${shortId(p.id)})`
|
|
42
|
+
: "";
|
|
43
|
+
lines.push(
|
|
44
|
+
` ${p.retrieval ? "*" : " "} ${shortId(p.id)} ${p.state}${p.legacy ? " legacy" : ""}, ${p.owners} chunks, ${p.provenance}${role}`
|
|
45
|
+
);
|
|
46
|
+
if (p.compatibleRuntimes.length)
|
|
47
|
+
lines.push(` read by: ${p.compatibleRuntimes.join("; ")}`);
|
|
48
|
+
if (p.incompatibleRuntimes.length)
|
|
49
|
+
lines.push(` incompatible: ${p.incompatibleRuntimes.join("; ")}`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|