@gmickel/gno 2.6.0 → 2.7.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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +5 -3
  3. package/assets/skill/cli-reference.md +9 -2
  4. package/assets/skill/mcp-reference.md +2 -1
  5. package/assets/spa-production.json.gz +0 -0
  6. package/browser-extension/artifacts/{gno-browser-clipper-v2.6.0.zip → gno-browser-clipper-v2.7.0.zip} +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +1 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/package.json +1 -1
  10. package/spec/cli.md +109 -18
  11. package/spec/mcp.md +36 -2
  12. package/spec/output-schemas/ask.schema.json +1 -1
  13. package/spec/output-schemas/capture-receipt.schema.json +1 -1
  14. package/spec/output-schemas/doctor.schema.json +88 -0
  15. package/spec/output-schemas/error.schema.json +11 -2
  16. package/spec/output-schemas/get.schema.json +1 -1
  17. package/spec/output-schemas/mcp-capture-result.schema.json +1 -2
  18. package/spec/output-schemas/memory-remember.schema.json +2 -2
  19. package/spec/output-schemas/multi-get.schema.json +4 -1
  20. package/spec/output-schemas/peek.schema.json +2 -9
  21. package/spec/output-schemas/resident-status.schema.json +22 -0
  22. package/spec/output-schemas/search-result.schema.json +1 -1
  23. package/spec/output-schemas/search-results.schema.json +1 -1
  24. package/spec/output-schemas/status.schema.json +98 -0
  25. package/src/cli/commands/doctor.ts +54 -20
  26. package/src/cli/commands/embed.ts +41 -3
  27. package/src/cli/commands/query.ts +5 -0
  28. package/src/cli/commands/status.ts +63 -5
  29. package/src/cli/commands/vec.ts +54 -0
  30. package/src/cli/detach.ts +29 -1
  31. package/src/cli/errors.ts +13 -9
  32. package/src/cli/program.ts +53 -1
  33. package/src/core/capture-sync.ts +9 -2
  34. package/src/core/host-paths.ts +31 -0
  35. package/src/core/memory-remember.ts +4 -3
  36. package/src/core/shutdown-budget.ts +6 -0
  37. package/src/core/vector-partition-status.ts +52 -0
  38. package/src/embed/backlog.ts +124 -18
  39. package/src/embed/fingerprint.ts +6 -3
  40. package/src/embed/retry.ts +66 -27
  41. package/src/embed/variant-backlog.ts +15 -10
  42. package/src/embed/variant-retry.ts +31 -22
  43. package/src/index.ts +21 -2
  44. package/src/llm/native-worker/dispatcher.ts +2 -0
  45. package/src/llm/native-worker/embedding-identity.ts +42 -0
  46. package/src/llm/native-worker/protocol.ts +1 -0
  47. package/src/llm/types.ts +3 -0
  48. package/src/mcp/context.ts +9 -0
  49. package/src/mcp/resources/index.ts +6 -5
  50. package/src/mcp/tool-descriptions-core.ts +1 -1
  51. package/src/mcp/tools/capture.ts +1 -3
  52. package/src/mcp/tools/index.ts +11 -4
  53. package/src/mcp/tools/memory-remember.ts +1 -1
  54. package/src/mcp/tools/status.ts +4 -0
  55. package/src/pipeline/hybrid.ts +37 -7
  56. package/src/pipeline/vsearch.ts +14 -2
  57. package/src/serve/embed-scheduler.ts +133 -19
  58. package/src/serve/host-path-redaction.ts +79 -0
  59. package/src/serve/public/components/sessions/SessionSearch.tsx +2 -2
  60. package/src/serve/public/globals.built.css +1 -1
  61. package/src/serve/public/hooks/use-api.ts +17 -2
  62. package/src/serve/public/lib/request-intent.ts +8 -0
  63. package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
  64. package/src/serve/public/pages/Dashboard.tsx +12 -9
  65. package/src/serve/public/pages/DocView.tsx +10 -5
  66. package/src/serve/public/pages/DocumentEditor.tsx +91 -14
  67. package/src/serve/public/pages/Search.tsx +1 -41
  68. package/src/serve/resident-runtime.ts +26 -1
  69. package/src/serve/resident-status.ts +13 -1
  70. package/src/serve/server.ts +10 -9
  71. package/src/serve/status-model.ts +16 -0
  72. package/src/serve/status.ts +2 -0
  73. package/src/serve/watch-reconciliation-shared.ts +3 -0
  74. package/src/serve/watch-service-events.ts +3 -2
  75. package/src/serve/watch-service-run-flush.ts +35 -2
  76. package/src/serve/watch-service.ts +5 -0
  77. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  78. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  79. package/src/store/migrations/index.ts +4 -0
  80. package/src/store/sqlite/adapter.ts +22 -1
  81. package/src/store/types.ts +11 -1
  82. package/src/store/vector/lazy.ts +46 -43
  83. package/src/store/vector/runtime-compat.ts +651 -0
  84. package/src/store/vector/sqlite-vec.ts +20 -2
  85. package/src/store/vector/status.ts +276 -35
  86. package/src/store/vector/types.ts +2 -0
  87. package/src/store/vector/variant-search.ts +71 -23
  88. package/src/store/vector/variants.ts +49 -14
  89. package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +0 -1
@@ -78,7 +78,7 @@
78
78
  "properties": {
79
79
  "absPath": {
80
80
  "type": "string",
81
- "description": "Absolute path (included with --source or in MCP)"
81
+ "description": "Absolute source path. Host path: local callers only (CLI, stdio MCP, same-host Web UI/REST); omitted for remote REST and HTTP MCP callers, which identify the document by uri + relPath."
82
82
  },
83
83
  "relPath": {
84
84
  "type": "string",
@@ -237,7 +237,7 @@
237
237
  "properties": {
238
238
  "absPath": {
239
239
  "type": "string",
240
- "description": "Absolute filesystem path when resolvable from collection root + relPath. Default field on gno search --json (no --source flag). Omitted when unresolvable; consumers fall back to the URI tail for display and disable file-open."
240
+ "description": "Absolute filesystem path when resolvable from collection root + relPath. Default field on gno search --json (no --source flag). Omitted when unresolvable, and always omitted for remote REST and HTTP MCP callers (host paths reach only CLI, stdio MCP and same-host Web UI/REST callers); consumers fall back to uri + relPath for display and disable file-open."
241
241
  },
242
242
  "relPath": {
243
243
  "type": "string",
@@ -150,6 +150,104 @@
150
150
  "description": "Chunks awaiting embedding",
151
151
  "minimum": 0
152
152
  },
153
+ "vectorPartitions": {
154
+ "type": "array",
155
+ "description": "Vector partitions of the embedding model; omitted when none exist",
156
+ "items": {
157
+ "type": "object",
158
+ "additionalProperties": false,
159
+ "required": [
160
+ "id",
161
+ "model",
162
+ "dimensions",
163
+ "state",
164
+ "legacy",
165
+ "retrieval",
166
+ "droppable",
167
+ "owners",
168
+ "provenance",
169
+ "compatibleRuntimes",
170
+ "incompatibleRuntimes"
171
+ ],
172
+ "properties": {
173
+ "id": {
174
+ "type": "string",
175
+ "description": "Partition id (drop with `gno vec drop <id-prefix>`)"
176
+ },
177
+ "model": {
178
+ "type": "string",
179
+ "description": "Embedding model URI"
180
+ },
181
+ "dimensions": {
182
+ "type": "integer",
183
+ "minimum": 1
184
+ },
185
+ "state": {
186
+ "type": "string",
187
+ "enum": ["active", "shadow"],
188
+ "description": "active = activated for retrieval; shadow = incomplete or abandoned"
189
+ },
190
+ "legacy": {
191
+ "type": "boolean",
192
+ "description": "Pre-runtime-independent key, awaiting measured re-keying"
193
+ },
194
+ "retrieval": {
195
+ "type": "boolean",
196
+ "description": "Status counts use this partition: the activated runtime-independent partition every compatible runtime reads"
197
+ },
198
+ "droppable": {
199
+ "type": "boolean",
200
+ "description": "`gno vec drop` accepts it; never the partition this runtime's retrieval uses"
201
+ },
202
+ "owners": {
203
+ "type": "integer",
204
+ "minimum": 0,
205
+ "description": "Current chunks bound to this partition"
206
+ },
207
+ "provenance": {
208
+ "type": "string",
209
+ "description": "Runtime that built the partition, e.g. \"CUDA, Bun 1.4.2\""
210
+ },
211
+ "compatibleRuntimes": {
212
+ "type": "array",
213
+ "items": {
214
+ "type": "string"
215
+ },
216
+ "description": "Runtimes measured (or recorded as the builder) to read this partition"
217
+ },
218
+ "incompatibleRuntimes": {
219
+ "type": "array",
220
+ "items": {
221
+ "type": "string"
222
+ },
223
+ "description": "Runtimes measured incompatible; they read another partition or use lexical retrieval only"
224
+ }
225
+ }
226
+ }
227
+ },
228
+ "vectorRuntime": {
229
+ "type": "object",
230
+ "additionalProperties": false,
231
+ "required": ["label", "state", "partition"],
232
+ "description": "This process's runtime, resolved by the same selection rule its queries use",
233
+ "properties": {
234
+ "label": {
235
+ "type": ["string", "null"],
236
+ "description": "Runtime recorded by this caller's last query or embed"
237
+ },
238
+ "state": {
239
+ "type": "string",
240
+ "enum": ["vectors", "unavailable", "unresolved"],
241
+ "description": "vectors: queries read `partition`; unavailable: lexical retrieval only; unresolved: no query or embed has resolved this caller yet"
242
+ },
243
+ "partition": {
244
+ "type": ["string", "null"]
245
+ },
246
+ "reason": {
247
+ "type": "string"
248
+ }
249
+ }
250
+ },
153
251
  "recentErrors": {
154
252
  "type": "integer",
155
253
  "description": "Recent ingest errors in the last 24 hours",
@@ -12,6 +12,10 @@ import { arch, platform } from "node:os";
12
12
 
13
13
  import type { Config } from "../../config/types";
14
14
  import type { ActivationStatus } from "../../core/activation-status";
15
+ import type {
16
+ VectorPartitionStatus,
17
+ VectorRuntimeStatus,
18
+ } from "../../store/vector/status";
15
19
 
16
20
  import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
17
21
  import {
@@ -26,6 +30,7 @@ import {
26
30
  readFindingsRunStatus,
27
31
  resolveFindingsSchedule,
28
32
  } from "../../core/findings-run-state";
33
+ import { formatVectorPartitionLines } from "../../core/vector-partition-status";
29
34
  import { getCodeChunkingStatus } from "../../ingestion/chunker";
30
35
  import { ModelCache } from "../../llm/cache";
31
36
  import { getActivePreset, resolveModelUri } from "../../llm/registry";
@@ -57,6 +62,9 @@ export interface DoctorCheck {
57
62
  details?: string[];
58
63
  /** Embedding fingerprint diagnostics for machine consumers */
59
64
  embeddingFingerprint?: EmbeddingFingerprintHealth;
65
+ /** Vector partitions; `retrieval` marks the one status and search use */
66
+ vectorPartitions?: VectorPartitionStatus[];
67
+ vectorRuntime?: VectorRuntimeStatus;
60
68
  }
61
69
 
62
70
  export interface EmbeddingFingerprintGroup {
@@ -261,16 +269,18 @@ function describeFingerprintGroup(group: EmbeddingFingerprintGroup): string {
261
269
  async function checkEmbeddingFingerprints(
262
270
  config: Config,
263
271
  indexName?: string
264
- ): Promise<DoctorCheck> {
272
+ ): Promise<DoctorCheck[]> {
265
273
  const dbPath = getIndexDbPath(indexName);
266
274
  try {
267
275
  await stat(dbPath);
268
276
  } catch {
269
- return {
270
- name: "embedding-fingerprint",
271
- status: "warn",
272
- message: "Database not found. Run: gno init",
273
- };
277
+ return [
278
+ {
279
+ name: "embedding-fingerprint",
280
+ status: "warn",
281
+ message: "Database not found. Run: gno init",
282
+ },
283
+ ];
274
284
  }
275
285
 
276
286
  const store = new SqliteAdapter();
@@ -283,12 +293,14 @@ async function checkEmbeddingFingerprints(
283
293
  config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS
284
294
  );
285
295
  if (!openResult.ok) {
286
- return {
287
- name: "embedding-fingerprint",
288
- status: "warn",
289
- message: `Fingerprint health unavailable: ${openResult.error.message}`,
290
- details: ["Run: gno doctor --json", "Then run: gno embed"],
291
- };
296
+ return [
297
+ {
298
+ name: "embedding-fingerprint",
299
+ status: "warn",
300
+ message: `Fingerprint health unavailable: ${openResult.error.message}`,
301
+ details: ["Run: gno doctor --json", "Then run: gno embed"],
302
+ },
303
+ ];
292
304
  }
293
305
 
294
306
  try {
@@ -297,12 +309,14 @@ async function checkEmbeddingFingerprints(
297
309
  const currentFingerprint = getStoredEmbeddingFingerprint(db, model);
298
310
  const statusResult = await store.getStatus({ embedModel: model });
299
311
  if (!statusResult.ok) {
300
- return {
301
- name: "embedding-fingerprint",
302
- status: "warn",
303
- message: `Fingerprint health unavailable: ${statusResult.error.message}`,
304
- details: ["Run: gno embed"],
305
- };
312
+ return [
313
+ {
314
+ name: "embedding-fingerprint",
315
+ status: "warn",
316
+ message: `Fingerprint health unavailable: ${statusResult.error.message}`,
317
+ details: ["Run: gno embed"],
318
+ },
319
+ ];
306
320
  }
307
321
 
308
322
  const legacyChunks =
@@ -389,13 +403,33 @@ async function checkEmbeddingFingerprints(
389
403
  }
390
404
  }
391
405
 
392
- return {
406
+ const fingerprintCheck: DoctorCheck = {
393
407
  name: "embedding-fingerprint",
394
408
  status: hasWarnings ? "warn" : "ok",
395
409
  message,
396
410
  details: details.length > 0 ? details : undefined,
397
411
  embeddingFingerprint: health,
398
412
  };
413
+ const partitions = statusResult.value.vectorPartitions;
414
+ if (!partitions) return [fingerprintCheck];
415
+ const runtime = statusResult.value.vectorRuntime;
416
+ const partitionLines = formatVectorPartitionLines(partitions, runtime);
417
+ const retrieval = partitions.find((p) => p.retrieval);
418
+ return [
419
+ fingerprintCheck,
420
+ {
421
+ name: "vector-partitions",
422
+ status: partitionLines.length ? "warn" : "ok",
423
+ message: retrieval
424
+ ? `this runtime's retrieval uses ${retrieval.id.slice(0, 12)} (${retrieval.state}, ${retrieval.owners} chunks, ${retrieval.provenance}); ${partitions.length - 1} other partition(s)`
425
+ : runtime?.state === "unavailable"
426
+ ? `this runtime uses lexical retrieval only: ${runtime.reason}`
427
+ : `this runtime has not resolved a partition yet; ${partitions.length} partition(s)`,
428
+ details: partitionLines.length ? partitionLines.slice(1) : undefined,
429
+ vectorPartitions: partitions,
430
+ vectorRuntime: runtime,
431
+ },
432
+ ];
399
433
  } finally {
400
434
  await store.close();
401
435
  }
@@ -669,7 +703,7 @@ export async function doctor(
669
703
  checks.push(checkCodeChunking());
670
704
 
671
705
  // Embedding fingerprint freshness
672
- checks.push(await checkEmbeddingFingerprints(config, options.indexName));
706
+ checks.push(...(await checkEmbeddingFingerprints(config, options.indexName)));
673
707
 
674
708
  // Scheduled findings pass (daemon-only, opt-in)
675
709
  checks.push(await checkFindingsPass(config, options.indexName));
@@ -7,6 +7,9 @@
7
7
 
8
8
  import type { Database } from "bun:sqlite";
9
9
 
10
+ // node:readline/promises is the platform line-input API; Bun has no equivalent
11
+ import { createInterface } from "node:readline/promises";
12
+
10
13
  import type { EmbeddingPort } from "../../llm/types";
11
14
  import type { StoreResult } from "../../store/types";
12
15
 
@@ -77,8 +80,10 @@ export interface EmbedOptions extends CliWriteLeaseOptions {
77
80
  force?: boolean;
78
81
  /** Show what would be done without embedding */
79
82
  dryRun?: boolean;
80
- /** Skip confirmation prompts */
83
+ /** Skip confirmation prompts (never confirms a separate vector partition) */
81
84
  yes?: boolean;
85
+ /** Explicitly confirm building a separate vector partition */
86
+ newPartition?: boolean;
82
87
  /** Output as JSON */
83
88
  json?: boolean;
84
89
  /** Verbose error logging */
@@ -450,6 +455,24 @@ async function initEmbedContext(
450
455
  return { ok: true, config, modelUri, store };
451
456
  }
452
457
 
458
+ async function confirmSeparatePartition(message: string): Promise<boolean> {
459
+ process.stderr.write(`${message}\n`);
460
+ const prompt = createInterface({
461
+ input: process.stdin,
462
+ output: process.stderr,
463
+ });
464
+ try {
465
+ const answer = await prompt.question(
466
+ "Build the separate partition? [y/N] "
467
+ );
468
+ return /^(?:y|yes)$/i.test(answer.trim());
469
+ } catch {
470
+ return false;
471
+ } finally {
472
+ prompt.close();
473
+ }
474
+ }
475
+
453
476
  // ─────────────────────────────────────────────────────────────────────────────
454
477
  // Main Command
455
478
  // ─────────────────────────────────────────────────────────────────────────────
@@ -574,7 +597,7 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
574
597
  }
575
598
  vectorIndex = vectorResult.value;
576
599
 
577
- const prepared = await prepareEmbeddingBacklog({
600
+ const backlogDeps = {
578
601
  statsPort: stats,
579
602
  embedPort,
580
603
  vectorIndex,
@@ -582,7 +605,22 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
582
605
  collection: options.collection,
583
606
  batchSize,
584
607
  force,
585
- });
608
+ allowNewPartition: options.newPartition,
609
+ };
610
+ let prepared = await prepareEmbeddingBacklog(backlogDeps);
611
+ if (
612
+ !prepared.ok &&
613
+ prepared.error.code === "VECTOR_PARTITION_FORK" &&
614
+ !dryRun &&
615
+ !options.json &&
616
+ !options.yes &&
617
+ process.stdin.isTTY &&
618
+ (await confirmSeparatePartition(prepared.error.message))
619
+ )
620
+ prepared = await prepareEmbeddingBacklog({
621
+ ...backlogDeps,
622
+ allowNewPartition: true,
623
+ });
586
624
  if (!prepared.ok)
587
625
  return { success: false, error: prepared.error.message };
588
626
  if (prepared.value.variantStore) {
@@ -37,6 +37,7 @@ import {
37
37
  createVectorIndexPort,
38
38
  type VectorIndexPort,
39
39
  } from "../../store/vector";
40
+ import { VECTOR_RUNTIME_INCOMPATIBLE } from "../../store/vector/variant-search";
40
41
  import { getGlobals } from "../program";
41
42
  import {
42
43
  createProgressRenderer,
@@ -517,6 +518,10 @@ export function formatQuery(
517
518
  }
518
519
  // Output explain to stderr if present (async but best-effort)
519
520
  outputExplainToStderr(result.data);
521
+ if (options.format !== "json")
522
+ for (const warning of result.data.meta.warnings ?? [])
523
+ if (warning.code === VECTOR_RUNTIME_INCOMPATIBLE)
524
+ process.stderr.write(`Warning: ${warning.message}\n`);
520
525
 
521
526
  // Use shared formatter for consistent output
522
527
  // Dynamic import to keep module loading fast
@@ -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
- return formatTerminal(
382
- result.status,
383
- result.activation,
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
  }
@@ -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
- export type CliErrorCode =
13
- | "VALIDATION"
14
- | "RUNTIME"
15
- | "NOT_RUNNING"
16
- | "BUSY"
17
- | "AUDIT_FINDINGS"
18
- | "AUDIT_PARTIAL"
19
- | "CONTEXT_STALE"
20
- | "CONTEXT_CONFLICT";
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>;