@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.
Files changed (106) hide show
  1. package/README.md +25 -30
  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.1.zip} +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +1 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/package.json +2 -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/collection-list.schema.json +2 -2
  15. package/spec/output-schemas/doctor.schema.json +88 -0
  16. package/spec/output-schemas/error.schema.json +11 -2
  17. package/spec/output-schemas/get.schema.json +1 -1
  18. package/spec/output-schemas/mcp-capture-result.schema.json +1 -2
  19. package/spec/output-schemas/memory-remember.schema.json +2 -2
  20. package/spec/output-schemas/multi-get.schema.json +4 -1
  21. package/spec/output-schemas/peek.schema.json +2 -9
  22. package/spec/output-schemas/resident-status.schema.json +22 -0
  23. package/spec/output-schemas/search-result.schema.json +1 -1
  24. package/spec/output-schemas/search-results.schema.json +1 -1
  25. package/spec/output-schemas/status.schema.json +110 -16
  26. package/src/cli/commands/ask.ts +31 -12
  27. package/src/cli/commands/doctor.ts +54 -20
  28. package/src/cli/commands/embed.ts +41 -3
  29. package/src/cli/commands/ls.ts +6 -1
  30. package/src/cli/commands/query.ts +5 -0
  31. package/src/cli/commands/status.ts +63 -5
  32. package/src/cli/commands/vec.ts +54 -0
  33. package/src/cli/detach.ts +29 -1
  34. package/src/cli/errors.ts +13 -9
  35. package/src/cli/program.ts +53 -1
  36. package/src/core/capture-sync.ts +9 -2
  37. package/src/core/host-paths.ts +49 -0
  38. package/src/core/memory-remember.ts +4 -3
  39. package/src/core/request-receipts.ts +63 -9
  40. package/src/core/shutdown-budget.ts +6 -0
  41. package/src/core/vector-partition-status.ts +52 -0
  42. package/src/core/windows-private-path.ts +136 -1
  43. package/src/embed/backlog.ts +145 -27
  44. package/src/embed/fingerprint.ts +6 -3
  45. package/src/embed/retry.ts +66 -27
  46. package/src/embed/variant-backlog.ts +48 -18
  47. package/src/embed/variant-retry.ts +31 -22
  48. package/src/index.ts +21 -2
  49. package/src/llm/inference-scope.ts +18 -0
  50. package/src/llm/native-worker/dispatcher.ts +2 -0
  51. package/src/llm/native-worker/embedding-identity.ts +42 -0
  52. package/src/llm/native-worker/protocol.ts +1 -0
  53. package/src/llm/types.ts +3 -0
  54. package/src/mcp/context.ts +9 -0
  55. package/src/mcp/resources/index.ts +6 -5
  56. package/src/mcp/tool-descriptions-core.ts +1 -1
  57. package/src/mcp/tools/capture.ts +1 -3
  58. package/src/mcp/tools/index.ts +11 -4
  59. package/src/mcp/tools/memory-remember.ts +1 -1
  60. package/src/mcp/tools/status.ts +23 -6
  61. package/src/pipeline/hybrid.ts +37 -7
  62. package/src/pipeline/vsearch.ts +14 -2
  63. package/src/serve/embed-scheduler.ts +133 -19
  64. package/src/serve/host-path-redaction.ts +116 -0
  65. package/src/serve/public/components/BootstrapStatus.tsx +5 -3
  66. package/src/serve/public/components/CaptureModal.tsx +1 -1
  67. package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
  68. package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
  69. package/src/serve/public/components/FirstRunWizard.tsx +4 -2
  70. package/src/serve/public/components/sessions/SessionSearch.tsx +2 -2
  71. package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
  72. package/src/serve/public/globals.built.css +1 -1
  73. package/src/serve/public/hooks/use-api.ts +17 -2
  74. package/src/serve/public/lib/request-intent.ts +8 -0
  75. package/src/serve/public/{components/sessions → lib}/snippet.tsx +2 -3
  76. package/src/serve/public/pages/Collections.tsx +16 -13
  77. package/src/serve/public/pages/Connectors.tsx +7 -4
  78. package/src/serve/public/pages/Dashboard.tsx +16 -11
  79. package/src/serve/public/pages/DocView.tsx +10 -5
  80. package/src/serve/public/pages/DocumentEditor.tsx +91 -14
  81. package/src/serve/public/pages/Search.tsx +1 -41
  82. package/src/serve/resident-runtime.ts +33 -1
  83. package/src/serve/resident-status.ts +13 -1
  84. package/src/serve/routes/sessions.ts +61 -5
  85. package/src/serve/server.ts +15 -12
  86. package/src/serve/status-model.ts +26 -6
  87. package/src/serve/status.ts +4 -7
  88. package/src/serve/watch-reconciliation-shared.ts +3 -0
  89. package/src/serve/watch-service-events.ts +3 -2
  90. package/src/serve/watch-service-run-flush.ts +35 -2
  91. package/src/serve/watch-service.ts +5 -0
  92. package/src/store/migrations/031-runtime-independent-vectors.ts +29 -0
  93. package/src/store/migrations/032-vector-runtime-callers.ts +17 -0
  94. package/src/store/migrations/index.ts +4 -0
  95. package/src/store/sqlite/adapter.ts +24 -3
  96. package/src/store/sqlite/change-journal-store.ts +1 -1
  97. package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
  98. package/src/store/types.ts +11 -1
  99. package/src/store/vector/lazy.ts +46 -43
  100. package/src/store/vector/runtime-compat.ts +651 -0
  101. package/src/store/vector/sqlite-vec.ts +20 -2
  102. package/src/store/vector/status.ts +276 -35
  103. package/src/store/vector/types.ts +2 -0
  104. package/src/store/vector/variant-search.ts +71 -23
  105. package/src/store/vector/variants.ts +49 -14
  106. package/browser-extension/artifacts/gno-browser-clipper-v2.6.0.zip.sha256 +0 -1
@@ -42,6 +42,7 @@ import {
42
42
  type SessionHarness,
43
43
  type SessionImportReceipt,
44
44
  remoteSafeSessionsError,
45
+ SessionsError,
45
46
  type SessionsErrorCode,
46
47
  SESSIONS_VALIDATION_CODES,
47
48
  } from "../../sessions/types";
@@ -185,11 +186,14 @@ function instanceIdentity(ctxHolder: ContextHolder): {
185
186
  }
186
187
 
187
188
  /** Refuse an archive config opened against a different index (and vice versa). */
188
- async function assertInstanceBinding(ctxHolder: ContextHolder): Promise<void> {
189
+ async function assertInstanceBinding(
190
+ ctxHolder: ContextHolder,
191
+ config: Config = ctxHolder.config
192
+ ): Promise<void> {
189
193
  const { configPath, indexName } = instanceIdentity(ctxHolder);
190
- if (!ctxHolder.config.sessions) return;
194
+ if (!config.sessions) return;
191
195
  await assertSessionBinding({
192
- config: ctxHolder.config,
196
+ config,
193
197
  configPath,
194
198
  indexName,
195
199
  dbPath: getIndexDbPath(indexName),
@@ -245,10 +249,48 @@ async function adoptConfig(
245
249
  ctxHolder.markIndexMutation?.();
246
250
  }
247
251
 
252
+ /**
253
+ * Read this instance's config file, binding checked: unreadable is an error,
254
+ * never served stale, and a config rebound to another index is refused
255
+ * before it can touch this one.
256
+ */
257
+ async function readConfigFile(ctxHolder: ContextHolder): Promise<Config> {
258
+ const loaded = await loadConfig(instanceIdentity(ctxHolder).configPath);
259
+ if (!loaded.ok) {
260
+ throw new SessionsError(
261
+ "SESSIONS_RUNTIME_FAILURE",
262
+ "The server could not read its config file; fix the file (gno doctor shows the error) and reload."
263
+ );
264
+ }
265
+ await assertInstanceBinding(ctxHolder, loaded.value);
266
+ return loaded.value;
267
+ }
268
+
248
269
  // ─────────────────────────────────────────────────────────────────────────────
249
270
  // Handlers
250
271
  // ─────────────────────────────────────────────────────────────────────────────
251
272
 
273
+ /**
274
+ * Adopt the config file when it changed underneath the running server, for
275
+ * example after `gno sessions source add/remove` on the same pair. Runs
276
+ * before the status read is admitted: adopting new collections moves the
277
+ * authorization epoch, which would void a read already in flight. Returns an
278
+ * error response, or null when the served config is current.
279
+ */
280
+ export async function refreshSessionsConfig(
281
+ ctxHolder: ContextHolder,
282
+ store: SqliteAdapter
283
+ ): Promise<Response | null> {
284
+ try {
285
+ const config = await readConfigFile(ctxHolder);
286
+ if (Bun.deepEquals(config, ctxHolder.config)) return null;
287
+ await adoptConfig(ctxHolder, store, config);
288
+ return null;
289
+ } catch (error) {
290
+ return sessionsErrorResponse(error);
291
+ }
292
+ }
293
+
252
294
  /** GET /api/sessions/status */
253
295
  export async function handleSessionsStatus(
254
296
  ctxHolder: ContextHolder
@@ -427,8 +469,22 @@ export async function handleSessionsRemoveSource(
427
469
  try {
428
470
  const { configPath } = instanceIdentity(ctxHolder);
429
471
  await assertInstanceBinding(ctxHolder);
430
- const config = await removeSessionSource({ configPath, id });
431
- await adoptConfig(ctxHolder, store, config);
472
+ const config = await removeSessionSource({ configPath, id }).catch(
473
+ (error: unknown) => {
474
+ // Already unregistered (for example from the CLI): the goal holds.
475
+ if (
476
+ error instanceof SessionsError &&
477
+ error.code === "SESSIONS_UNKNOWN_SOURCE"
478
+ ) {
479
+ return readConfigFile(ctxHolder);
480
+ }
481
+ throw error;
482
+ }
483
+ );
484
+ // A no-op remove leaves the served config current; adopting it anyway
485
+ // would reset the egress policy under concurrent reads.
486
+ if (!Bun.deepEquals(config, ctxHolder.config))
487
+ await adoptConfig(ctxHolder, store, config);
432
488
  } catch (error) {
433
489
  return sessionsErrorResponse(error);
434
490
  }
@@ -1,11 +1,4 @@
1
1
  import type { HttpGatewayOverrides } from "../mcp/http-security";
2
- /**
3
- * Bun.serve() web server for GNO web UI.
4
- * Uses Bun's fullstack dev server with HTML imports.
5
- * Opens DB once at startup, closes on shutdown.
6
- *
7
- * @module src/serve/server
8
- */
9
2
  import type { RequestPeerServer } from "./request-locality";
10
3
  import type { ResidentRuntime } from "./resident-runtime";
11
4
  import type { ContextHolder } from "./routes/api";
@@ -23,6 +16,14 @@ import {
23
16
  handlePdfjsVendorRequest,
24
17
  isPdfjsVendorPath,
25
18
  } from "./fn112-routes";
19
+ /**
20
+ * Bun.serve() web server for GNO web UI.
21
+ * Uses Bun's fullstack dev server with HTML imports.
22
+ * Opens DB once at startup, closes on shutdown.
23
+ *
24
+ * @module src/serve/server
25
+ */
26
+ import { withRemoteHostPathRedaction } from "./host-path-redaction";
26
27
  import { PDFJS_ASSET_CACHE_CONTROL } from "./pdfjs-assets";
27
28
  // HTML import - Bun handles bundling TSX/CSS automatically via routes
28
29
  import homepage from "./public/index.html";
@@ -115,6 +116,7 @@ import {
115
116
  handleSessionsInit,
116
117
  handleSessionsRemoveSource,
117
118
  handleSessionsStatus,
119
+ refreshSessionsConfig,
118
120
  } from "./routes/sessions";
119
121
 
120
122
  /** `/api/sessions/automation/:id[/...]` path parameter. */
@@ -571,7 +573,7 @@ export async function startServer(
571
573
  development: isDev,
572
574
 
573
575
  // Static routes - Bun handles HTML bundling and /_bun/* assets automatically
574
- routes: {
576
+ routes: withRemoteHostPathRedaction({
575
577
  "/mcp": gateway.route,
576
578
  ...clipperRoutesForBind(
577
579
  isHttpGatewayLoopbackBind(gatewayConfig.host),
@@ -860,9 +862,10 @@ export async function startServer(
860
862
  "/api/sessions/status": {
861
863
  GET: async (req: Request) =>
862
864
  withSecurityHeaders(
863
- await handleResidentRead(runtime as ResidentRuntime, req, () =>
864
- handleSessionsStatus(ctxHolder)
865
- ),
865
+ (await refreshSessionsConfig(ctxHolder, store)) ??
866
+ (await handleResidentRead(runtime as ResidentRuntime, req, () =>
867
+ handleSessionsStatus(ctxHolder)
868
+ )),
866
869
  isDev
867
870
  ),
868
871
  },
@@ -1743,7 +1746,7 @@ export async function startServer(
1743
1746
  );
1744
1747
  },
1745
1748
  },
1746
- },
1749
+ }),
1747
1750
  // Production catch-all: /vendor/pdfjs prefix, then hashed SPA chunks
1748
1751
  // (gzip + immutable) and the private SPA source — the same factory the
1749
1752
  // tests mount (no test-only fallback path).
@@ -1,6 +1,10 @@
1
1
  import type { ContentTypeBoostStatus } from "../config/content-types";
2
2
  import type { ActivationStatus } from "../core/activation-status";
3
3
  import type { ChunkingStatus } from "../store/chunking";
4
+ import type {
5
+ VectorPartitionStatus,
6
+ VectorRuntimeStatus,
7
+ } from "../store/vector/status";
4
8
 
5
9
  export type HealthCheckStatus = "ok" | "warn" | "error";
6
10
 
@@ -14,9 +18,13 @@ export type HealthActionKind =
14
18
 
15
19
  export type OnboardingStepStatus = "complete" | "current" | "upcoming";
16
20
 
21
+ /**
22
+ * Host path fields below (`path`, `configPath`, `dbPath`) reach same-host
23
+ * callers only; a remote caller's response omits them.
24
+ */
17
25
  export interface StatusCollection {
18
26
  name: string;
19
- path: string;
27
+ path?: string;
20
28
  documentCount: number;
21
29
  chunkCount: number;
22
30
  embeddedCount: number;
@@ -24,7 +32,7 @@ export interface StatusCollection {
24
32
 
25
33
  export interface SuggestedCollection {
26
34
  label: string;
27
- path: string;
35
+ path?: string;
28
36
  reason: string;
29
37
  }
30
38
 
@@ -109,6 +117,16 @@ export interface ResidentStatus {
109
117
  content: number;
110
118
  index: number;
111
119
  };
120
+ /** Present only while a background job is in trouble. */
121
+ backgroundIssues?: BackgroundIssue[];
122
+ }
123
+
124
+ /** A resident background job that keeps failing, has stopped retrying, or overruns. */
125
+ export interface BackgroundIssue {
126
+ job: "embed" | "resident";
127
+ state: "failing" | "parked" | "overrunning" | "unresponsive";
128
+ consecutiveFailures: number;
129
+ runningSeconds: number | null;
112
130
  }
113
131
 
114
132
  export interface BackgroundServiceState {
@@ -153,7 +171,7 @@ export interface BootstrapState {
153
171
  summary: string;
154
172
  };
155
173
  cache: {
156
- path: string;
174
+ path?: string;
157
175
  totalSizeBytes: number;
158
176
  totalSizeLabel: string;
159
177
  };
@@ -169,7 +187,7 @@ export interface BootstrapState {
169
187
  role: "embed" | "rerank" | "expand" | "gen";
170
188
  uri: string;
171
189
  cached: boolean;
172
- path: string | null;
190
+ path?: string | null;
173
191
  sizeBytes: number | null;
174
192
  statusLabel: string;
175
193
  }>;
@@ -180,12 +198,14 @@ export interface AppStatusResponse {
180
198
  chunking?: ChunkingStatus;
181
199
  resident: ResidentStatus;
182
200
  indexName: string;
183
- configPath: string;
184
- dbPath: string;
201
+ configPath?: string;
202
+ dbPath?: string;
185
203
  collections: StatusCollection[];
186
204
  totalDocuments: number;
187
205
  totalChunks: number;
188
206
  embeddingBacklog: number;
207
+ vectorPartitions?: VectorPartitionStatus[];
208
+ vectorRuntime?: VectorRuntimeStatus;
189
209
  lastUpdated: string | null;
190
210
  recentErrors: number;
191
211
  healthy: boolean;
@@ -95,11 +95,6 @@ function summarizeCount(
95
95
  return `${count} ${count === 1 ? singular : plural}`;
96
96
  }
97
97
 
98
- function toDisplayPath(path: string): string {
99
- const home = homedir();
100
- return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
101
- }
102
-
103
98
  function extractEstimatedFootprint(name: string): string | null {
104
99
  const match = name.match(SIZE_REGEX);
105
100
  return match ? match[0] : null;
@@ -432,11 +427,11 @@ async function buildDiskCheck(
432
427
  title: "Disk",
433
428
  status: "warn",
434
429
  summary: "Disk space could not be inspected",
435
- detail: `GNO could not read filesystem capacity near ${toDisplayPath(getModelsCachePath())}.`,
430
+ detail: "GNO could not read filesystem capacity for the model cache.",
436
431
  };
437
432
  }
438
433
 
439
- const summary = `${formatBytes(snapshot.freeBytes)} free near ${toDisplayPath(snapshot.path)}`;
434
+ const summary = `${formatBytes(snapshot.freeBytes)} free for the model cache`;
440
435
 
441
436
  if (snapshot.freeBytes < DISK_ERROR_BYTES) {
442
437
  return {
@@ -734,6 +729,8 @@ export async function buildAppStatus(
734
729
  totalDocuments: status.activeDocuments,
735
730
  totalChunks: status.totalChunks,
736
731
  embeddingBacklog: status.embeddingBacklog,
732
+ vectorPartitions: status.vectorPartitions,
733
+ vectorRuntime: status.vectorRuntime,
737
734
  chunking: status.chunking,
738
735
  lastUpdated: status.lastUpdatedAt,
739
736
  recentErrors: status.recentErrors,
@@ -45,6 +45,9 @@ export const WATCHER_MAX_SUPPRESSION_ENTRIES = 4_096;
45
45
  /** Bounded retry delay after failed classification/sync. */
46
46
  export const WATCHER_RETRY_BACKOFF_MS = 500;
47
47
 
48
+ /** Retry delay while another writer holds the shared writer lease. */
49
+ export const WATCHER_LEASE_RETRY_MS = 5_000;
50
+
48
51
  /**
49
52
  * Single fixed budget for fallback classification across visited directories,
50
53
  * candidates, removals, dirty dirs, and aggregate store rows.
@@ -219,7 +219,8 @@ export function requeueAfterFailure(
219
219
  collectionName: string,
220
220
  exact: string[],
221
221
  dirty: string[],
222
- forceFlags?: PendingForceFlags
222
+ forceFlags?: PendingForceFlags,
223
+ delayMs = WATCHER_RETRY_BACKOFF_MS
223
224
  ): void {
224
225
  queueWithoutSchedule(host, collectionName, exact, dirty, forceFlags);
225
226
  if (host.disposed()) {
@@ -241,7 +242,7 @@ export function requeueAfterFailure(
241
242
  host.timers.delete(collectionName);
242
243
  host.retryScheduled.delete(collectionName);
243
244
  startFlush(host, collectionName);
244
- }, WATCHER_RETRY_BACKOFF_MS)
245
+ }, delayMs)
245
246
  );
246
247
  }
247
248
 
@@ -14,6 +14,7 @@ import type { WatchQueueHost } from "./watch-service-events";
14
14
  import type { CollectionPending } from "./watch-service-state";
15
15
  import type { WatcherSnapshot, WatcherSnapshotFs } from "./watch-snapshot";
16
16
 
17
+ import { WATCHER_LEASE_RETRY_MS } from "./watch-reconciliation-shared";
17
18
  import {
18
19
  requeueAfterFailure,
19
20
  requeueGenerationReconcile,
@@ -65,6 +66,11 @@ export interface RunFlushContext {
65
66
  snapshotFs?: WatcherSnapshotFs;
66
67
  /** Test seam: lower snapshot entry ceiling for overflow→full proofs. */
67
68
  snapshotEntryCeiling?: number;
69
+ /**
70
+ * Shared writer lease for this flush; null when another writer holds it,
71
+ * which leaves the work queued for the retry timer instead of waiting.
72
+ */
73
+ acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
68
74
  }
69
75
 
70
76
  /**
@@ -86,19 +92,44 @@ export async function runOwnedCollectionFlush(
86
92
  return;
87
93
  }
88
94
 
95
+ ctx.syncing.add(collectionName);
96
+ let releaseLease: (() => Promise<void>) | null = null;
97
+ if (ctx.acquireWriteLease) {
98
+ releaseLease = await ctx.acquireWriteLease();
99
+ if (!releaseLease || ctx.disposed()) {
100
+ ctx.syncing.delete(collectionName);
101
+ await releaseLease?.();
102
+ if (!ctx.disposed()) {
103
+ requeueAfterFailure(
104
+ ctx.queueHost,
105
+ collectionName,
106
+ [],
107
+ [],
108
+ undefined,
109
+ WATCHER_LEASE_RETRY_MS
110
+ );
111
+ }
112
+ return;
113
+ }
114
+ }
115
+
89
116
  const collection = ctx
90
117
  .collections()
91
118
  .find((entry) => entry.name === collectionName);
92
119
  if (!collection) {
120
+ await releaseLease?.();
121
+ ctx.syncing.delete(collectionName);
93
122
  ctx.pendingByCollection.delete(collectionName);
94
123
  ctx.flushDeadlineAt.delete(collectionName);
95
124
  return;
96
125
  }
97
126
 
98
- const taken = takePending(pending);
127
+ // Events that arrived while the lease was being taken join this flush.
128
+ const taken = takePending(
129
+ ctx.pendingByCollection.get(collectionName) ?? pending
130
+ );
99
131
  ctx.pendingByCollection.set(collectionName, emptyPending());
100
132
  ctx.flushDeadlineAt.delete(collectionName);
101
- ctx.syncing.add(collectionName);
102
133
 
103
134
  const ownerGeneration = ctx.collectionGenerations.get(collectionName) ?? 0;
104
135
  const ownerRoot = normalize(collection.path);
@@ -219,6 +250,8 @@ export async function runOwnedCollectionFlush(
219
250
  throw outcome.error;
220
251
  }
221
252
  } finally {
253
+ // Release before any follow-up flush below tries to take the lease again.
254
+ await releaseLease?.();
222
255
  ctx.syncing.delete(collectionName);
223
256
  ctx.clearLifecycleTombstones(collectionName);
224
257
  ctx.pruneSuppression();
@@ -108,6 +108,8 @@ interface CollectionWatchServiceOptions {
108
108
  * Production leaves this unset (uses WATCHER_SNAPSHOT_ENTRY_CEILING).
109
109
  */
110
110
  snapshotEntryCeiling?: number;
111
+ /** Shared writer lease taken (no wait) around each flush's writes. */
112
+ acquireWriteLease?: () => Promise<(() => Promise<void>) | null>;
111
113
  }
112
114
 
113
115
  export class CollectionWatchService {
@@ -146,6 +148,7 @@ export class CollectionWatchService {
146
148
  | undefined;
147
149
  readonly #snapshotFs: WatcherSnapshotFs | undefined;
148
150
  readonly #snapshotEntryCeiling: number | undefined;
151
+ readonly #acquireWriteLease: CollectionWatchServiceOptions["acquireWriteLease"];
149
152
  #nextCollectionGeneration = 0;
150
153
  #disposed = false;
151
154
  #lastEventAt: string | null = null;
@@ -169,6 +172,7 @@ export class CollectionWatchService {
169
172
  this.#buildSnapshot = options.buildSnapshot;
170
173
  this.#snapshotFs = options.snapshotFs;
171
174
  this.#snapshotEntryCeiling = options.snapshotEntryCeiling;
175
+ this.#acquireWriteLease = options.acquireWriteLease;
172
176
  }
173
177
 
174
178
  start(): void {
@@ -394,6 +398,7 @@ export class CollectionWatchService {
394
398
  },
395
399
  snapshotFs: this.#snapshotFs,
396
400
  snapshotEntryCeiling: this.#snapshotEntryCeiling,
401
+ acquireWriteLease: this.#acquireWriteLease,
397
402
  });
398
403
  }
399
404
 
@@ -0,0 +1,29 @@
1
+ /** Runtime details become provenance; existing partitions await measured re-keying. */
2
+ import type { Migration } from "./runner";
3
+
4
+ export const migration: Migration = {
5
+ version: 31,
6
+ name: "runtime_independent_vectors",
7
+ up(db): void {
8
+ db.exec(`
9
+ ALTER TABLE vector_partitions ADD COLUMN provenance TEXT;
10
+ ALTER TABLE vector_partitions ADD COLUMN legacy INTEGER NOT NULL DEFAULT 0;
11
+ -- Vector-defining key shared by a primary and its confirmed forks.
12
+ ALTER TABLE vector_partitions ADD COLUMN base_fingerprint TEXT;
13
+ -- Runtime fingerprint of a confirmed separate partition; NULL = primary.
14
+ ALTER TABLE vector_partitions ADD COLUMN fork TEXT;
15
+ UPDATE vector_partitions SET legacy = 1;
16
+ CREATE TABLE vector_runtime_verdicts (
17
+ partition_id TEXT NOT NULL,
18
+ runtime TEXT NOT NULL,
19
+ label TEXT NOT NULL,
20
+ verdict TEXT NOT NULL CHECK (verdict IN ('compatible', 'incompatible')),
21
+ min_cosine REAL NOT NULL,
22
+ samples INTEGER NOT NULL CHECK (samples >= 0),
23
+ sample_ms REAL NOT NULL,
24
+ PRIMARY KEY (partition_id, runtime)
25
+ );
26
+
27
+ `);
28
+ },
29
+ };
@@ -0,0 +1,17 @@
1
+ /** Identity each caller (process runtime + env) last resolved, for status. */
2
+ import type { Migration } from "./runner";
3
+
4
+ export const migration: Migration = {
5
+ version: 32,
6
+ name: "vector_runtime_callers",
7
+ up(db): void {
8
+ db.exec(`
9
+ CREATE TABLE IF NOT EXISTS vector_runtime_callers (
10
+ caller TEXT PRIMARY KEY,
11
+ runtime TEXT NOT NULL,
12
+ label TEXT NOT NULL,
13
+ identity TEXT NOT NULL
14
+ );
15
+ `);
16
+ },
17
+ };
@@ -44,6 +44,8 @@ import { migration as m027 } from "./027-memory-scopes";
44
44
  import { migration as m028 } from "./028-vector-variants";
45
45
  import { migration as m029 } from "./029-graph-reference-state";
46
46
  import { migration as m030 } from "./030-typed-metadata";
47
+ import { migration as m031 } from "./031-runtime-independent-vectors";
48
+ import { migration as m032 } from "./032-vector-runtime-callers";
47
49
 
48
50
  /** All migrations in order */
49
51
  export const migrations = [
@@ -77,4 +79,6 @@ export const migrations = [
77
79
  m028,
78
80
  m029,
79
81
  m030,
82
+ m031,
83
+ m032,
80
84
  ];
@@ -158,7 +158,11 @@ import { getSchemaVersion, migrations, runMigrations } from "../migrations";
158
158
  import { err, ok } from "../types";
159
159
  import { getStoredEmbeddingFingerprint } from "../vector/freshness";
160
160
  import { modelTableName } from "../vector/sqlite-vec";
161
- import { getVariantStatus } from "../vector/status";
161
+ import {
162
+ getVariantStatus,
163
+ listVectorPartitions,
164
+ vectorRuntimeStatus,
165
+ } from "../vector/status";
162
166
  import {
163
167
  deleteSavedCapsuleRegistration as deleteStoredSavedCapsuleRegistration,
164
168
  getSavedCapsuleRegistration as getStoredSavedCapsuleRegistration,
@@ -711,6 +715,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
711
715
  }
712
716
  }
713
717
 
718
+ /** Set this connection's SQLite busy_timeout (resident event-loop bound). */
719
+ setBusyTimeout(ms: number): void {
720
+ this.db?.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(ms))}`);
721
+ }
722
+
714
723
  /** Cap subsequent SQLite lock waits to the resident settlement deadline. */
715
724
  beginShutdown(deadline: number): void {
716
725
  this.shutdownDeadline = deadline;
@@ -1937,7 +1946,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1937
1946
  const db = this.ensureOpen();
1938
1947
  const rows = db
1939
1948
  .query<DbDocumentRow, [string, string]>(
1940
- `SELECT * FROM documents
1949
+ `SELECT * FROM documents INDEXED BY idx_documents_record_source_path
1941
1950
  WHERE collection = ? AND record_source_path = ?
1942
1951
  ORDER BY rel_path ASC, id ASC`
1943
1952
  )
@@ -3317,7 +3326,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
3317
3326
 
3318
3327
  const docs = db
3319
3328
  .query<DocInfo, [string]>(
3320
- "SELECT id, rel_path, title FROM documents WHERE mirror_hash = ? AND active = 1"
3329
+ "SELECT id, rel_path, title FROM documents INDEXED BY idx_documents_mirror_hash WHERE mirror_hash = ? AND active = 1"
3321
3330
  )
3322
3331
  .all(mirrorHash);
3323
3332
 
@@ -5893,6 +5902,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5893
5902
  (embedModel ? getStoredEmbeddingFingerprint(db, embedModel) : null);
5894
5903
 
5895
5904
  const variantStatus = getVariantStatus(db, options);
5905
+ const vectorPartitions = listVectorPartitions(
5906
+ db,
5907
+ embedModel ?? undefined
5908
+ );
5896
5909
 
5897
5910
  // Get version
5898
5911
  const versionRow = db
@@ -6077,6 +6090,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
6077
6090
  ? (variantStatus.embeddedByCollection.get(s.name) ?? 0)
6078
6091
  : s.embedded_count,
6079
6092
  })),
6093
+ ...(vectorPartitions.length
6094
+ ? {
6095
+ vectorPartitions,
6096
+ ...(embedModel
6097
+ ? { vectorRuntime: vectorRuntimeStatus(db, embedModel) }
6098
+ : {}),
6099
+ }
6100
+ : {}),
6080
6101
  totalDocuments: totalsRow?.total ?? 0,
6081
6102
  activeDocuments: totalsRow?.active ?? 0,
6082
6103
  totalChunks: chunkCount,
@@ -180,7 +180,7 @@ const enforceInTransaction = (
180
180
  db
181
181
  .query<{ sequence: number | null }, [number]>(
182
182
  `SELECT MAX(sequence) AS sequence
183
- FROM document_changes
183
+ FROM document_changes INDEXED BY idx_document_changes_retention
184
184
  WHERE observed_at_ms <= ?`
185
185
  )
186
186
  .get(ageBoundary)?.sequence ?? state.retention_floor;
@@ -9,7 +9,8 @@ export type LegacyTitleSnapshot = Map<string, (string | null)[]>;
9
9
  function activeTitle(db: Database, mirror: string): TitleRow | null {
10
10
  return db
11
11
  .query<TitleRow, [string]>(`
12
- SELECT title FROM documents WHERE mirror_hash = ? AND active = 1
12
+ SELECT title FROM documents INDEXED BY idx_documents_mirror_hash
13
+ WHERE mirror_hash = ? AND active = 1
13
14
  ORDER BY id LIMIT 1
14
15
  `)
15
16
  .get(mirror);
@@ -26,6 +26,10 @@ import type {
26
26
  ChunkingStatus,
27
27
  PendingChunkingMirror,
28
28
  } from "./chunking";
29
+ import type {
30
+ VectorPartitionStatus,
31
+ VectorRuntimeStatus,
32
+ } from "./vector/status";
29
33
 
30
34
  // ─────────────────────────────────────────────────────────────────────────────
31
35
  // Error Types
@@ -61,7 +65,9 @@ export type StoreErrorCode =
61
65
  | "VEC_SEARCH_UNAVAILABLE"
62
66
  | "VEC_SEARCH_FAILED"
63
67
  | "VEC_REBUILD_FAILED"
64
- | "VEC_SYNC_FAILED";
68
+ | "VEC_SYNC_FAILED"
69
+ /** Embedding would build a separate vector partition without confirmation. */
70
+ | "VECTOR_PARTITION_FORK";
65
71
 
66
72
  /** Store error with structured details */
67
73
  export interface StoreError {
@@ -820,6 +826,10 @@ export interface IndexStatus {
820
826
  totalChunks: number;
821
827
  /** Chunks without embeddings */
822
828
  embeddingBacklog: number;
829
+ /** Vector partitions of the status model; counts use the `retrieval` one. */
830
+ vectorPartitions?: VectorPartitionStatus[];
831
+ /** This process's runtime, resolved by retrieval's own selection rule. */
832
+ vectorRuntime?: VectorRuntimeStatus;
823
833
  /** Configuration and applied cached layouts; separate from source freshness. */
824
834
  chunking?: ChunkingStatus;
825
835
  /** Recent ingest errors (last 24h) */