@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.
Files changed (37) hide show
  1. package/README.md +25 -30
  2. package/assets/spa-production.json.gz +0 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.7.1.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v2.7.1.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +2 -1
  7. package/spec/output-schemas/collection-list.schema.json +2 -2
  8. package/spec/output-schemas/status.schema.json +12 -16
  9. package/src/cli/commands/ask.ts +31 -12
  10. package/src/cli/commands/ls.ts +6 -1
  11. package/src/core/host-paths.ts +23 -5
  12. package/src/core/request-receipts.ts +63 -9
  13. package/src/core/windows-private-path.ts +136 -1
  14. package/src/embed/backlog.ts +22 -10
  15. package/src/embed/variant-backlog.ts +34 -9
  16. package/src/llm/inference-scope.ts +18 -0
  17. package/src/mcp/tools/status.ts +19 -6
  18. package/src/serve/embed-scheduler.ts +2 -2
  19. package/src/serve/host-path-redaction.ts +51 -14
  20. package/src/serve/public/components/BootstrapStatus.tsx +5 -3
  21. package/src/serve/public/components/CaptureModal.tsx +1 -1
  22. package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
  23. package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
  24. package/src/serve/public/components/FirstRunWizard.tsx +4 -2
  25. package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
  26. package/src/serve/public/pages/Collections.tsx +16 -13
  27. package/src/serve/public/pages/Connectors.tsx +7 -4
  28. package/src/serve/public/pages/Dashboard.tsx +8 -6
  29. package/src/serve/resident-runtime.ts +7 -0
  30. package/src/serve/routes/sessions.ts +61 -5
  31. package/src/serve/server.ts +5 -3
  32. package/src/serve/status-model.ts +10 -6
  33. package/src/serve/status.ts +2 -7
  34. package/src/store/sqlite/adapter.ts +2 -2
  35. package/src/store/sqlite/change-journal-store.ts +1 -1
  36. package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
  37. 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: string;
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
- <TooltipProvider>
266
- <Tooltip>
267
- <TooltipTrigger asChild>
268
- <p className="truncate font-mono text-muted-foreground text-xs">
269
- {truncatePath(collection.path)}
270
- </p>
271
- </TooltipTrigger>
272
- <TooltipContent className="max-w-xs break-all">
273
- <p className="font-mono text-xs">{collection.path}</p>
274
- </TooltipContent>
275
- </Tooltip>
276
- </TooltipProvider>
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: string;
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
- <div className="font-mono text-muted-foreground text-xs">
238
- {connector.path}
239
- </div>
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
- <div
805
- className="truncate font-mono text-muted-foreground text-sm"
806
- title={collection.path}
807
- >
808
- {collection.path}
809
- </div>
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(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
  }
@@ -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 handleResidentRead(runtime as ResidentRuntime, req, () =>
865
- handleSessionsStatus(ctxHolder)
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: string;
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: string;
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: string;
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: string | null;
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: string;
198
- dbPath: string;
201
+ configPath?: string;
202
+ dbPath?: string;
199
203
  collections: StatusCollection[];
200
204
  totalDocuments: number;
201
205
  totalChunks: number;
@@ -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 {
@@ -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 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);
@@ -1 +0,0 @@
1
- 2116eeada49b4479ac5967ec1bb79d1288b70529aba691b2b4ddaf33d131bc2e gno-browser-clipper-v2.7.0.zip