@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
|
@@ -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(
|
|
189
|
+
async function assertInstanceBinding(
|
|
190
|
+
ctxHolder: ContextHolder,
|
|
191
|
+
config: Config = ctxHolder.config
|
|
192
|
+
): Promise<void> {
|
|
189
193
|
const { configPath, indexName } = instanceIdentity(ctxHolder);
|
|
190
|
-
if (!
|
|
194
|
+
if (!config.sessions) return;
|
|
191
195
|
await assertSessionBinding({
|
|
192
|
-
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
|
-
|
|
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
|
}
|
package/src/serve/server.ts
CHANGED
|
@@ -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
|
|
864
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
184
|
-
dbPath
|
|
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;
|
package/src/serve/status.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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);
|
package/src/store/types.ts
CHANGED
|
@@ -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) */
|