@gmickel/gno 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +26 -30
  2. package/assets/skill/SKILL.md +8 -1
  3. package/assets/skill/cli-reference.md +8 -1
  4. package/assets/skill/examples.md +2 -1
  5. package/assets/skill/mcp-reference.md +3 -1
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.7.0.zip → gno-browser-clipper-v2.8.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +2 -1
  11. package/spec/cli.md +48 -6
  12. package/spec/db/schema.sql +0 -1
  13. package/spec/mcp.md +18 -3
  14. package/spec/output-schemas/audit-report.schema.json +18 -4
  15. package/spec/output-schemas/backlinks.schema.json +4 -0
  16. package/spec/output-schemas/collection-list.schema.json +15 -2
  17. package/spec/output-schemas/graph.schema.json +2 -0
  18. package/spec/output-schemas/links-list.schema.json +4 -0
  19. package/spec/output-schemas/status.schema.json +27 -16
  20. package/src/cli/commands/ask.ts +31 -12
  21. package/src/cli/commands/audit.ts +23 -4
  22. package/src/cli/commands/collection/list.ts +39 -5
  23. package/src/cli/commands/embed.ts +3 -3
  24. package/src/cli/commands/links.ts +34 -131
  25. package/src/cli/commands/ls.ts +6 -1
  26. package/src/cli/commands/shared.ts +7 -0
  27. package/src/cli/commands/status.ts +5 -0
  28. package/src/cli/program.ts +12 -2
  29. package/src/config/loader.ts +43 -0
  30. package/src/config/types.ts +8 -0
  31. package/src/core/audit-contract.ts +16 -4
  32. package/src/core/audit-freshness.ts +11 -1
  33. package/src/core/audit-links.ts +145 -25
  34. package/src/core/audit-provenance.ts +11 -4
  35. package/src/core/audit-workspace.ts +19 -4
  36. package/src/core/audit.ts +67 -15
  37. package/src/core/context-compiler.ts +3 -0
  38. package/src/core/context-evidence.ts +11 -0
  39. package/src/core/graph-edge-confidence.ts +23 -1
  40. package/src/core/host-paths.ts +24 -5
  41. package/src/core/knowledge-impact.ts +28 -0
  42. package/src/core/link-workspace.ts +324 -0
  43. package/src/core/request-receipts.ts +63 -9
  44. package/src/core/retrieval-replay-candidate.ts +6 -0
  45. package/src/core/retrieval-trace-request.ts +3 -0
  46. package/src/core/windows-private-path.ts +136 -1
  47. package/src/embed/backlog.ts +22 -10
  48. package/src/embed/variant-backlog.ts +34 -9
  49. package/src/index.ts +14 -1
  50. package/src/ingestion/graph-reconciliation.ts +77 -15
  51. package/src/ingestion/source-availability/darwin-path.ts +9 -3
  52. package/src/ingestion/sync.ts +22 -1
  53. package/src/ingestion/types.ts +14 -0
  54. package/src/llm/inference-scope.ts +19 -0
  55. package/src/mcp/http-egress.ts +42 -3
  56. package/src/mcp/tools/audit.ts +11 -2
  57. package/src/mcp/tools/changes.ts +1 -0
  58. package/src/mcp/tools/links.ts +3 -0
  59. package/src/mcp/tools/sessions.ts +33 -4
  60. package/src/mcp/tools/status.ts +22 -6
  61. package/src/pipeline/expansion.ts +19 -31
  62. package/src/pipeline/graph-retrieval.ts +22 -2
  63. package/src/pipeline/hybrid.ts +1 -1
  64. package/src/pipeline/types.ts +6 -3
  65. package/src/serve/embed-scheduler.ts +2 -2
  66. package/src/serve/findings-pass.ts +1 -1
  67. package/src/serve/host-path-redaction.ts +51 -14
  68. package/src/serve/public/components/BootstrapStatus.tsx +5 -3
  69. package/src/serve/public/components/CaptureModal.tsx +1 -1
  70. package/src/serve/public/components/CollectionModelDialog.tsx +16 -13
  71. package/src/serve/public/components/CollectionsEmptyState.tsx +5 -3
  72. package/src/serve/public/components/FirstRunWizard.tsx +4 -2
  73. package/src/serve/public/components/sessions/SourcesPanel.tsx +77 -60
  74. package/src/serve/public/pages/Collections.tsx +16 -13
  75. package/src/serve/public/pages/Connectors.tsx +7 -4
  76. package/src/serve/public/pages/Dashboard.tsx +8 -6
  77. package/src/serve/public/pages/GraphView.tsx +2 -0
  78. package/src/serve/resident-runtime.ts +7 -0
  79. package/src/serve/routes/changes.ts +6 -1
  80. package/src/serve/routes/links.ts +13 -0
  81. package/src/serve/routes/sessions.ts +81 -37
  82. package/src/serve/server.ts +5 -3
  83. package/src/serve/status-model.ts +10 -6
  84. package/src/serve/status.ts +2 -7
  85. package/src/sessions/config-refresh.ts +111 -0
  86. package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
  87. package/src/store/migrations/034-collection-link-workspace.ts +47 -0
  88. package/src/store/migrations/index.ts +4 -0
  89. package/src/store/sqlite/adapter.ts +392 -233
  90. package/src/store/sqlite/change-journal-store.ts +1 -1
  91. package/src/store/sqlite/eligibility.ts +8 -2
  92. package/src/store/sqlite/graph-link-resolver.ts +252 -5
  93. package/src/store/sqlite/graph-neighbors.ts +147 -40
  94. package/src/store/sqlite/graph-reference-state.ts +13 -2
  95. package/src/store/sqlite/legacy-vector-ownership.ts +2 -1
  96. package/src/store/sqlite/workspace-link-resolver.ts +654 -0
  97. package/src/store/types.ts +49 -3
  98. package/src/store/vector/stats.ts +1 -1
  99. package/browser-extension/artifacts/gno-browser-clipper-v2.7.0.zip.sha256 +0 -1
@@ -394,8 +394,12 @@ export function SourcesPanel({
394
394
  const [notice, setNotice] = useState<{ text: string } | null>(null);
395
395
  const noticeRef = useRef<HTMLParagraphElement>(null);
396
396
  useFocusOnChange(notice, noticeRef);
397
+ // Bumped when the Discover button's run finishes: focus moves to its outcome.
398
+ const [discoveredCount, setDiscoveredCount] = useState(0);
399
+ const discoveryRef = useRef<HTMLDivElement>(null);
400
+ useFocusOnChange(discoveredCount, discoveryRef);
397
401
 
398
- const discover = async () => {
402
+ const discover = async (focusOutcome = false) => {
399
403
  setDiscovering(true);
400
404
  const result = await sessionsApi<SessionsDiscovery>(
401
405
  "/api/sessions/discover"
@@ -403,6 +407,7 @@ export function SourcesPanel({
403
407
  setDiscovering(false);
404
408
  setDiscoverError(result.error);
405
409
  setDiscovery(result.data);
410
+ if (focusOutcome) setDiscoveredCount((count) => count + 1);
406
411
  };
407
412
 
408
413
  const registered = async (sourceId: string, collection: string) => {
@@ -425,7 +430,7 @@ export function SourcesPanel({
425
430
  {localClient && (
426
431
  <Button
427
432
  disabled={discovering}
428
- onClick={() => void discover()}
433
+ onClick={() => void discover(true)}
429
434
  size="sm"
430
435
  variant="outline"
431
436
  >
@@ -473,67 +478,79 @@ export function SourcesPanel({
473
478
  </p>
474
479
  )}
475
480
 
476
- {discoverError && (
477
- <p className="break-words text-destructive text-sm" role="alert">
478
- {discoverError}
479
- </p>
480
- )}
481
- {discovery && (
482
- <div className="space-y-3 rounded-lg border border-dashed border-border/70 p-4">
483
- <h3 className="font-medium">Discovered on this machine</h3>
484
- <p className="text-muted-foreground text-xs">
485
- Preview only: discovery reads nothing into the archive. Register a
486
- source to permit manual imports from it.
487
- </p>
488
- {discovery.candidates.length === 0 && (
489
- <p className="text-muted-foreground text-sm">
490
- No supported session stores were found.
481
+ {(discoverError || discovery) && (
482
+ <div
483
+ aria-label="Discovery results"
484
+ className={`space-y-3 ${FOCUS_RING}`}
485
+ ref={discoveryRef}
486
+ role="region"
487
+ tabIndex={-1}
488
+ >
489
+ {discoverError && (
490
+ <p className="break-words text-destructive text-sm" role="alert">
491
+ {discoverError}
491
492
  </p>
492
493
  )}
493
- <ul className="space-y-4">
494
- {discovery.candidates.map((candidate) => (
495
- <li
496
- className="min-w-0 space-y-2"
497
- key={`${candidate.harness}:${candidate.path}`}
498
- >
499
- <div className="flex flex-wrap items-center gap-2">
500
- <Badge variant="secondary">
501
- {SESSION_HARNESS_LABELS[candidate.harness]}
502
- </Badge>
503
- <span className="min-w-0 break-all font-mono text-xs">
504
- {candidate.path}
505
- </span>
506
- </div>
507
- <p className="text-muted-foreground text-xs">
508
- {candidate.units}
509
- {candidate.truncated ? "+" : ""} units ·{" "}
510
- {formatBytes(candidate.bytes)}
511
- {candidate.formatVersions.length > 0 &&
512
- ` · format ${candidate.formatVersions.join(", ")}`}
494
+ {discovery && (
495
+ <div className="space-y-3 rounded-lg border border-dashed border-border/70 p-4">
496
+ <h3 className="font-medium">Discovered on this machine</h3>
497
+ <p className="text-muted-foreground text-xs">
498
+ Preview only: discovery reads nothing into the archive. Register
499
+ a source to permit manual imports from it.
500
+ </p>
501
+ {discovery.candidates.length === 0 && (
502
+ <p className="text-muted-foreground text-sm">
503
+ No supported session stores were found.
513
504
  </p>
514
- {candidate.registeredAs ? (
515
- <p className="text-sm">
516
- Registered as{" "}
517
- <span className="font-mono">{candidate.registeredAs}</span>
518
- </p>
519
- ) : (
520
- <RegisterForm
521
- candidate={candidate}
522
- archiveCollections={archiveCollections}
523
- onRegistered={registered}
524
- />
525
- )}
526
- </li>
527
- ))}
528
- </ul>
529
- {discovery.warnings.map((warning) => (
530
- <p
531
- className="text-amber-800 text-xs dark:text-amber-200"
532
- key={warning}
533
- >
534
- {warning}
535
- </p>
536
- ))}
505
+ )}
506
+ <ul className="space-y-4">
507
+ {discovery.candidates.map((candidate) => (
508
+ <li
509
+ className="min-w-0 space-y-2"
510
+ key={`${candidate.harness}:${candidate.path}`}
511
+ >
512
+ <div className="flex flex-wrap items-center gap-2">
513
+ <Badge variant="secondary">
514
+ {SESSION_HARNESS_LABELS[candidate.harness]}
515
+ </Badge>
516
+ <span className="min-w-0 break-all font-mono text-xs">
517
+ {candidate.path}
518
+ </span>
519
+ </div>
520
+ <p className="text-muted-foreground text-xs">
521
+ {candidate.units}
522
+ {candidate.truncated ? "+" : ""} units ·{" "}
523
+ {formatBytes(candidate.bytes)}
524
+ {candidate.formatVersions.length > 0 &&
525
+ ` · format ${candidate.formatVersions.join(", ")}`}
526
+ </p>
527
+ {candidate.registeredAs ? (
528
+ <p className="text-sm">
529
+ Registered as{" "}
530
+ <span className="font-mono">
531
+ {candidate.registeredAs}
532
+ </span>
533
+ </p>
534
+ ) : (
535
+ <RegisterForm
536
+ candidate={candidate}
537
+ archiveCollections={archiveCollections}
538
+ onRegistered={registered}
539
+ />
540
+ )}
541
+ </li>
542
+ ))}
543
+ </ul>
544
+ {discovery.warnings.map((warning) => (
545
+ <p
546
+ className="text-amber-800 text-xs dark:text-amber-200"
547
+ key={warning}
548
+ >
549
+ {warning}
550
+ </p>
551
+ ))}
552
+ </div>
553
+ )}
537
554
  </div>
538
555
  )}
539
556
  </section>
@@ -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">
@@ -83,6 +83,8 @@ interface GraphLink {
83
83
  resolution:
84
84
  | "exact-title"
85
85
  | "exact-path"
86
+ | "exact-name"
87
+ | "tie-break"
86
88
  | "path-fallback"
87
89
  | "ambiguous-fallback"
88
90
  | "similarity";
@@ -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
 
@@ -79,7 +79,12 @@ export async function handleImpact(
79
79
  ): Promise<Response> {
80
80
  const ref = url.searchParams.get("ref")?.trim();
81
81
  if (!ref) return errorResponse("VALIDATION", "ref is required");
82
- const input: KnowledgeImpactInput = {};
82
+ const collections = url.searchParams
83
+ .getAll("collection")
84
+ .map((value) => value.trim())
85
+ .filter(Boolean);
86
+ const input: KnowledgeImpactInput =
87
+ collections.length > 0 ? { collections } : {};
83
88
  for (const [queryName, inputName] of [
84
89
  ["maxDepth", "maxDepth"],
85
90
  ["maxNodes", "maxNodes"],
@@ -34,6 +34,8 @@ export interface LinkResponse {
34
34
  resolvedUri?: string;
35
35
  /** Resolved target title (if found) */
36
36
  resolvedTitle?: string;
37
+ /** Collection of the resolved target (may differ from the source's) */
38
+ resolvedCollection?: string;
37
39
  }>;
38
40
  meta: {
39
41
  docid: string;
@@ -49,6 +51,8 @@ export interface BacklinkResponse {
49
51
  sourceDocid: string;
50
52
  sourceUri: string;
51
53
  sourceTitle?: string;
54
+ /** Collection of the linking document */
55
+ sourceCollection?: string;
52
56
  linkText?: string;
53
57
  startLine: number;
54
58
  startCol: number;
@@ -189,6 +193,11 @@ export async function handleDocLinks(
189
193
  targetRefNorm: l.targetRefNorm,
190
194
  targetCollection: l.targetCollection || doc.collection,
191
195
  linkType: l.linkType,
196
+ source: {
197
+ collection: doc.collection,
198
+ relPath: doc.relPath,
199
+ explicit: Boolean(l.targetCollection),
200
+ },
192
201
  }))
193
202
  );
194
203
  const resolutionAvailable = resolvedResult.ok;
@@ -217,6 +226,9 @@ export async function handleDocLinks(
217
226
  resolvedDocid: resolved.docid,
218
227
  resolvedUri: resolved.uri,
219
228
  resolvedTitle: resolved.title ?? undefined,
229
+ ...(resolved.collection && {
230
+ resolvedCollection: resolved.collection,
231
+ }),
220
232
  }),
221
233
  }),
222
234
  };
@@ -273,6 +285,7 @@ export async function handleDocBacklinks(
273
285
  backlinks: backlinks.map((b) => ({
274
286
  sourceDocid: b.sourceDocid,
275
287
  sourceUri: b.sourceDocUri,
288
+ ...(b.sourceCollection && { sourceCollection: b.sourceCollection }),
276
289
  ...(b.sourceDocTitle && { sourceTitle: b.sourceDocTitle }),
277
290
  ...(b.linkText && { linkText: b.linkText }),
278
291
  startLine: b.startLine,
@@ -16,7 +16,6 @@ import type { SqliteAdapter } from "../../store/sqlite/adapter";
16
16
  import type { RequestPeerServer } from "../request-locality";
17
17
  import type { ContextHolder } from "./api";
18
18
 
19
- import { getIndexDbPath } from "../../app/constants";
20
19
  import { getConfigPaths, loadConfig } from "../../config";
21
20
  import { withContentTypeRules } from "../../ingestion";
22
21
  import {
@@ -28,8 +27,14 @@ import {
28
27
  runAutomationProfile,
29
28
  setAutomationProfile,
30
29
  } from "../../sessions/automation";
31
- import { assertSessionBinding } from "../../sessions/binding";
32
30
  import { SessionSourceSchema, watchedCollections } from "../../sessions/config";
31
+ import {
32
+ adoptServedConfig,
33
+ assertInstanceBinding as assertConfigBinding,
34
+ readInstanceConfig,
35
+ refreshServedConfig,
36
+ type ServedSessionsConfig,
37
+ } from "../../sessions/config-refresh";
33
38
  import { importInChildProcess } from "../../sessions/import-child";
34
39
  import { SessionsService } from "../../sessions/service";
35
40
  import {
@@ -42,6 +47,7 @@ import {
42
47
  type SessionHarness,
43
48
  type SessionImportReceipt,
44
49
  remoteSafeSessionsError,
50
+ SessionsError,
45
51
  type SessionsErrorCode,
46
52
  SESSIONS_VALIDATION_CODES,
47
53
  } from "../../sessions/types";
@@ -185,15 +191,11 @@ function instanceIdentity(ctxHolder: ContextHolder): {
185
191
  }
186
192
 
187
193
  /** Refuse an archive config opened against a different index (and vice versa). */
188
- async function assertInstanceBinding(ctxHolder: ContextHolder): Promise<void> {
189
- const { configPath, indexName } = instanceIdentity(ctxHolder);
190
- if (!ctxHolder.config.sessions) return;
191
- await assertSessionBinding({
192
- config: ctxHolder.config,
193
- configPath,
194
- indexName,
195
- dbPath: getIndexDbPath(indexName),
196
- });
194
+ function assertInstanceBinding(
195
+ ctxHolder: ContextHolder,
196
+ config: Config = ctxHolder.config
197
+ ): Promise<void> {
198
+ return assertConfigBinding(instanceIdentity(ctxHolder), config);
197
199
  }
198
200
 
199
201
  /** Service over the instance's own config/index pair, binding checked. */
@@ -211,44 +213,72 @@ async function archiveService(
211
213
  });
212
214
  }
213
215
 
216
+ /** This instance's served config, as the shared config refresh sees it. */
217
+ function servedConfig(
218
+ ctxHolder: ContextHolder,
219
+ store: SqliteAdapter
220
+ ): ServedSessionsConfig {
221
+ return {
222
+ ...instanceIdentity(ctxHolder),
223
+ store,
224
+ config: ctxHolder.config,
225
+ setConfig: (config) => {
226
+ ctxHolder.config = config;
227
+ ctxHolder.current = { ...ctxHolder.current, config };
228
+ ctxHolder.watchService?.updateCollections(
229
+ watchedCollections(config),
230
+ withContentTypeRules({}, config)
231
+ );
232
+ },
233
+ invalidateEgressPolicy: async () => {
234
+ await ctxHolder.invalidateEgressPolicy?.();
235
+ },
236
+ markContentMutation: () => ctxHolder.markContentMutation?.(),
237
+ markIndexMutation: () => ctxHolder.markIndexMutation?.(),
238
+ };
239
+ }
240
+
214
241
  /**
215
242
  * Adopt a config the sessions service already persisted: project collections
216
243
  * and contexts into the open store, swap the in-memory context, and refresh
217
- * the watcher, egress policy and mutation generations (same sequence as the
218
- * config-sync route helpers).
244
+ * the watcher, egress policy and mutation generations.
219
245
  */
220
- async function adoptConfig(
246
+ function adoptConfig(
221
247
  ctxHolder: ContextHolder,
222
248
  store: SqliteAdapter,
223
249
  config: Config
224
250
  ): Promise<void> {
225
- const collections = await store.syncCollections(config.collections);
226
- if (!collections.ok) {
227
- throw new Error(
228
- `Config saved but collection sync failed: ${collections.error.message}`
229
- );
230
- }
231
- const contexts = await store.syncContexts(config.contexts ?? []);
232
- if (!contexts.ok) {
233
- throw new Error(
234
- `Config saved but context sync failed: ${contexts.error.message}`
235
- );
236
- }
237
- ctxHolder.config = config;
238
- ctxHolder.current = { ...ctxHolder.current, config };
239
- ctxHolder.watchService?.updateCollections(
240
- watchedCollections(config),
241
- withContentTypeRules({}, config)
242
- );
243
- await ctxHolder.invalidateEgressPolicy?.();
244
- ctxHolder.markContentMutation?.();
245
- ctxHolder.markIndexMutation?.();
251
+ return adoptServedConfig(servedConfig(ctxHolder, store), config);
252
+ }
253
+
254
+ /** Read this instance's config file, binding checked (see readInstanceConfig). */
255
+ function readConfigFile(ctxHolder: ContextHolder): Promise<Config> {
256
+ return readInstanceConfig(instanceIdentity(ctxHolder));
246
257
  }
247
258
 
248
259
  // ─────────────────────────────────────────────────────────────────────────────
249
260
  // Handlers
250
261
  // ─────────────────────────────────────────────────────────────────────────────
251
262
 
263
+ /**
264
+ * Adopt the config file when it changed underneath the running server, for
265
+ * example after `gno sessions source add/remove` on the same pair. Runs
266
+ * before the status read is admitted: adopting new collections moves the
267
+ * authorization epoch, which would void a read already in flight. Returns an
268
+ * error response, or null when the served config is current.
269
+ */
270
+ export async function refreshSessionsConfig(
271
+ ctxHolder: ContextHolder,
272
+ store: SqliteAdapter
273
+ ): Promise<Response | null> {
274
+ try {
275
+ await refreshServedConfig(servedConfig(ctxHolder, store));
276
+ return null;
277
+ } catch (error) {
278
+ return sessionsErrorResponse(error);
279
+ }
280
+ }
281
+
252
282
  /** GET /api/sessions/status */
253
283
  export async function handleSessionsStatus(
254
284
  ctxHolder: ContextHolder
@@ -427,8 +457,22 @@ export async function handleSessionsRemoveSource(
427
457
  try {
428
458
  const { configPath } = instanceIdentity(ctxHolder);
429
459
  await assertInstanceBinding(ctxHolder);
430
- const config = await removeSessionSource({ configPath, id });
431
- await adoptConfig(ctxHolder, store, config);
460
+ const config = await removeSessionSource({ configPath, id }).catch(
461
+ (error: unknown) => {
462
+ // Already unregistered (for example from the CLI): the goal holds.
463
+ if (
464
+ error instanceof SessionsError &&
465
+ error.code === "SESSIONS_UNKNOWN_SOURCE"
466
+ ) {
467
+ return readConfigFile(ctxHolder);
468
+ }
469
+ throw error;
470
+ }
471
+ );
472
+ // A no-op remove leaves the served config current; adopting it anyway
473
+ // would reset the egress policy under concurrent reads.
474
+ if (!Bun.deepEquals(config, ctxHolder.config))
475
+ await adoptConfig(ctxHolder, store, config);
432
476
  } catch (error) {
433
477
  return sessionsErrorResponse(error);
434
478
  }
@@ -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 {