@gmickel/gno 2.7.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/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.7.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/output-schemas/collection-list.schema.json +2 -2
- package/spec/output-schemas/status.schema.json +12 -16
- package/src/cli/commands/ask.ts +31 -12
- package/src/cli/commands/ls.ts +6 -1
- package/src/core/host-paths.ts +23 -5
- package/src/core/request-receipts.ts +63 -9
- package/src/core/windows-private-path.ts +136 -1
- package/src/embed/backlog.ts +22 -10
- package/src/embed/variant-backlog.ts +34 -9
- package/src/llm/inference-scope.ts +18 -0
- package/src/mcp/tools/status.ts +19 -6
- package/src/serve/embed-scheduler.ts +2 -2
- package/src/serve/host-path-redaction.ts +51 -14
- package/src/serve/public/components/BootstrapStatus.tsx +5 -3
- package/src/serve/public/components/CaptureModal.tsx +1 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
- package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
- package/src/serve/public/components/FirstRunWizard.tsx +4 -2
- package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
- package/src/serve/public/pages/Collections.tsx +16 -13
- package/src/serve/public/pages/Connectors.tsx +7 -4
- package/src/serve/public/pages/Dashboard.tsx +8 -6
- package/src/serve/resident-runtime.ts +7 -0
- package/src/serve/routes/sessions.ts +61 -5
- package/src/serve/server.ts +5 -3
- package/src/serve/status-model.ts +10 -6
- package/src/serve/status.ts +2 -7
- package/src/store/sqlite/adapter.ts +2 -2
- package/src/store/sqlite/change-journal-store.ts +1 -1
- package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
- package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
|
@@ -76,7 +76,8 @@ interface PageProps {
|
|
|
76
76
|
interface CollectionStats {
|
|
77
77
|
activePresetId?: string;
|
|
78
78
|
name: string;
|
|
79
|
-
path
|
|
79
|
+
/** Host path; omitted for remote callers. */
|
|
80
|
+
path?: string;
|
|
80
81
|
documentCount: number;
|
|
81
82
|
chunkCount: number;
|
|
82
83
|
embeddedCount: number;
|
|
@@ -262,18 +263,20 @@ function CollectionCard({
|
|
|
262
263
|
</DropdownMenu>
|
|
263
264
|
</div>
|
|
264
265
|
|
|
265
|
-
|
|
266
|
-
<
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
266
|
+
{collection.path && (
|
|
267
|
+
<TooltipProvider>
|
|
268
|
+
<Tooltip>
|
|
269
|
+
<TooltipTrigger asChild>
|
|
270
|
+
<p className="truncate font-mono text-muted-foreground text-xs">
|
|
271
|
+
{truncatePath(collection.path)}
|
|
272
|
+
</p>
|
|
273
|
+
</TooltipTrigger>
|
|
274
|
+
<TooltipContent className="max-w-xs break-all">
|
|
275
|
+
<p className="font-mono text-xs">{collection.path}</p>
|
|
276
|
+
</TooltipContent>
|
|
277
|
+
</Tooltip>
|
|
278
|
+
</TooltipProvider>
|
|
279
|
+
)}
|
|
277
280
|
</CardHeader>
|
|
278
281
|
|
|
279
282
|
<CardContent className="pt-2">
|
|
@@ -30,7 +30,8 @@ interface ConnectorStatus {
|
|
|
30
30
|
target: string;
|
|
31
31
|
scope: "user" | "project";
|
|
32
32
|
installed: boolean;
|
|
33
|
-
path
|
|
33
|
+
/** Host path; omitted for remote callers. */
|
|
34
|
+
path?: string;
|
|
34
35
|
summary: string;
|
|
35
36
|
nextAction: string;
|
|
36
37
|
mode: {
|
|
@@ -234,9 +235,11 @@ export default function Connectors({ navigate }: PageProps) {
|
|
|
234
235
|
<span className="font-medium">Scope:</span>{" "}
|
|
235
236
|
{connector.scope}
|
|
236
237
|
</div>
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
{connector.path && (
|
|
239
|
+
<div className="font-mono text-muted-foreground text-xs">
|
|
240
|
+
{connector.path}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
240
243
|
</div>
|
|
241
244
|
{connector.error && (
|
|
242
245
|
<p className="text-destructive text-sm">
|
|
@@ -801,12 +801,14 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
801
801
|
<div className="truncate font-medium text-lg transition-colors group-hover:text-primary">
|
|
802
802
|
{collection.name}
|
|
803
803
|
</div>
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
804
|
+
{collection.path && (
|
|
805
|
+
<div
|
|
806
|
+
className="truncate font-mono text-muted-foreground text-sm"
|
|
807
|
+
title={collection.path}
|
|
808
|
+
>
|
|
809
|
+
{collection.path}
|
|
810
|
+
</div>
|
|
811
|
+
)}
|
|
810
812
|
</div>
|
|
811
813
|
</div>
|
|
812
814
|
<div className="flex shrink-0 items-center gap-3 text-right">
|
|
@@ -377,6 +377,13 @@ export async function startResidentRuntime(
|
|
|
377
377
|
acquireWriteLease: leaseFor(`gno ${mode} (watch sync)`),
|
|
378
378
|
});
|
|
379
379
|
watchService.start();
|
|
380
|
+
// A backlog left by `--no-embed` or an earlier run gets a pass without
|
|
381
|
+
// waiting for a file change.
|
|
382
|
+
const startupStatus = await store.getStatus({
|
|
383
|
+
embedModel: getActivePreset(initialConfig).embed,
|
|
384
|
+
});
|
|
385
|
+
if (startupStatus.ok && startupStatus.value.embeddingBacklog > 0)
|
|
386
|
+
scheduler.notifySyncComplete([]);
|
|
380
387
|
ctxHolder.watchService = watchService;
|
|
381
388
|
ctxHolder.current.watchService = watchService;
|
|
382
389
|
|
|
@@ -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
|
@@ -116,6 +116,7 @@ import {
|
|
|
116
116
|
handleSessionsInit,
|
|
117
117
|
handleSessionsRemoveSource,
|
|
118
118
|
handleSessionsStatus,
|
|
119
|
+
refreshSessionsConfig,
|
|
119
120
|
} from "./routes/sessions";
|
|
120
121
|
|
|
121
122
|
/** `/api/sessions/automation/:id[/...]` path parameter. */
|
|
@@ -861,9 +862,10 @@ export async function startServer(
|
|
|
861
862
|
"/api/sessions/status": {
|
|
862
863
|
GET: async (req: Request) =>
|
|
863
864
|
withSecurityHeaders(
|
|
864
|
-
await
|
|
865
|
-
|
|
866
|
-
|
|
865
|
+
(await refreshSessionsConfig(ctxHolder, store)) ??
|
|
866
|
+
(await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
867
|
+
handleSessionsStatus(ctxHolder)
|
|
868
|
+
)),
|
|
867
869
|
isDev
|
|
868
870
|
),
|
|
869
871
|
},
|
|
@@ -18,9 +18,13 @@ export type HealthActionKind =
|
|
|
18
18
|
|
|
19
19
|
export type OnboardingStepStatus = "complete" | "current" | "upcoming";
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Host path fields below (`path`, `configPath`, `dbPath`) reach same-host
|
|
23
|
+
* callers only; a remote caller's response omits them.
|
|
24
|
+
*/
|
|
21
25
|
export interface StatusCollection {
|
|
22
26
|
name: string;
|
|
23
|
-
path
|
|
27
|
+
path?: string;
|
|
24
28
|
documentCount: number;
|
|
25
29
|
chunkCount: number;
|
|
26
30
|
embeddedCount: number;
|
|
@@ -28,7 +32,7 @@ export interface StatusCollection {
|
|
|
28
32
|
|
|
29
33
|
export interface SuggestedCollection {
|
|
30
34
|
label: string;
|
|
31
|
-
path
|
|
35
|
+
path?: string;
|
|
32
36
|
reason: string;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -167,7 +171,7 @@ export interface BootstrapState {
|
|
|
167
171
|
summary: string;
|
|
168
172
|
};
|
|
169
173
|
cache: {
|
|
170
|
-
path
|
|
174
|
+
path?: string;
|
|
171
175
|
totalSizeBytes: number;
|
|
172
176
|
totalSizeLabel: string;
|
|
173
177
|
};
|
|
@@ -183,7 +187,7 @@ export interface BootstrapState {
|
|
|
183
187
|
role: "embed" | "rerank" | "expand" | "gen";
|
|
184
188
|
uri: string;
|
|
185
189
|
cached: boolean;
|
|
186
|
-
path
|
|
190
|
+
path?: string | null;
|
|
187
191
|
sizeBytes: number | null;
|
|
188
192
|
statusLabel: string;
|
|
189
193
|
}>;
|
|
@@ -194,8 +198,8 @@ export interface AppStatusResponse {
|
|
|
194
198
|
chunking?: ChunkingStatus;
|
|
195
199
|
resident: ResidentStatus;
|
|
196
200
|
indexName: string;
|
|
197
|
-
configPath
|
|
198
|
-
dbPath
|
|
201
|
+
configPath?: string;
|
|
202
|
+
dbPath?: string;
|
|
199
203
|
collections: StatusCollection[];
|
|
200
204
|
totalDocuments: number;
|
|
201
205
|
totalChunks: number;
|
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 {
|
|
@@ -1946,7 +1946,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1946
1946
|
const db = this.ensureOpen();
|
|
1947
1947
|
const rows = db
|
|
1948
1948
|
.query<DbDocumentRow, [string, string]>(
|
|
1949
|
-
`SELECT * FROM documents
|
|
1949
|
+
`SELECT * FROM documents INDEXED BY idx_documents_record_source_path
|
|
1950
1950
|
WHERE collection = ? AND record_source_path = ?
|
|
1951
1951
|
ORDER BY rel_path ASC, id ASC`
|
|
1952
1952
|
)
|
|
@@ -3326,7 +3326,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3326
3326
|
|
|
3327
3327
|
const docs = db
|
|
3328
3328
|
.query<DocInfo, [string]>(
|
|
3329
|
-
"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"
|
|
3330
3330
|
)
|
|
3331
3331
|
.all(mirrorHash);
|
|
3332
3332
|
|
|
@@ -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);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2116eeada49b4479ac5967ec1bb79d1288b70529aba691b2b4ddaf33d131bc2e gno-browser-clipper-v2.7.0.zip
|