@gmickel/gno 2.9.2 → 2.10.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 (38) hide show
  1. package/README.md +2 -2
  2. package/assets/spa-production.json.gz +0 -0
  3. package/browser-extension/artifacts/{gno-browser-clipper-v2.9.2.zip → gno-browser-clipper-v2.10.1.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v2.10.1.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/bunfig.toml +5 -1
  7. package/package.json +3 -2
  8. package/spec/cli.md +102 -12
  9. package/spec/mcp.md +1 -1
  10. package/spec/output-schemas/mcp-remove-result.schema.json +2 -2
  11. package/src/app/bun-runtime.ts +41 -0
  12. package/src/cli/commands/cleanup.ts +9 -1
  13. package/src/cli/commands/collection/remove.ts +50 -31
  14. package/src/cli/commands/collection/rename.ts +36 -8
  15. package/src/cli/commands/daemon.ts +3 -0
  16. package/src/cli/commands/doctor.ts +61 -0
  17. package/src/cli/commands/mcp/config-editors.ts +56 -8
  18. package/src/cli/commands/mcp/install.ts +159 -24
  19. package/src/cli/commands/mcp/launch-path.ts +87 -0
  20. package/src/cli/commands/mcp/paths.ts +30 -6
  21. package/src/cli/commands/mcp/repair-entry.ts +160 -0
  22. package/src/cli/commands/mcp/status.ts +55 -15
  23. package/src/cli/commands/mcp.ts +4 -0
  24. package/src/cli/commands/serve.ts +3 -0
  25. package/src/cli/commands/status.ts +24 -0
  26. package/src/cli/detach.ts +10 -0
  27. package/src/cli/program.ts +64 -19
  28. package/src/cli/runtime-health.ts +328 -0
  29. package/src/core/network-boundary-inventory.ts +16 -0
  30. package/src/ingestion/file-processor.ts +118 -17
  31. package/src/ingestion/prepare-worker.ts +20 -2
  32. package/src/mcp/tools/remove-collection.ts +2 -1
  33. package/src/serve/public/components/BootstrapStatus.tsx +3 -3
  34. package/src/serve/public/globals.built.css +1 -1
  35. package/src/serve/routes/api.ts +3 -4
  36. package/src/serve/status.ts +2 -1
  37. package/src/store/sqlite/adapter.ts +17 -4
  38. package/browser-extension/artifacts/gno-browser-clipper-v2.9.2.zip.sha256 +0 -1
@@ -19,6 +19,14 @@
19
19
  * If no backend can start, the file fails closed with
20
20
  * ISOLATION_UNAVAILABLE; it is never prepared unbounded.
21
21
  *
22
+ * A message that cannot be deserialized on the way to or from the processor
23
+ * (fn-213) fails the file with PROCESSOR_MESSAGE_FAILED and replaces the
24
+ * processor, and one stderr line names the direction, backend, file and
25
+ * step. Bun 1.4.0 and later report it as a `messageerror` event on the
26
+ * receiving side of a worker; over the child's IPC channel Bun closes the
27
+ * channel on both ends instead, so the child backend cannot tell which
28
+ * direction failed.
29
+ *
22
30
  * One processor is reused across files (starting one costs about a third of
23
31
  * a second of module loading). It is replaced after an overrun, after a
24
32
  * file that leaves memory above half the budget, and after a short idle
@@ -78,8 +86,26 @@ export interface ReadyMessage {
78
86
  type: "ready";
79
87
  }
80
88
 
89
+ /** Posted by the worker when a job it was sent could not be deserialized. */
90
+ export interface UndeliverableJobMessage {
91
+ type: "undeliverable-job";
92
+ }
93
+
94
+ /** Which message could not be delivered; the child backend cannot tell. */
95
+ export type DeliveryDirection = "job" | "result" | "unknown";
96
+
97
+ /** Why a processor stopped working. */
98
+ interface ProcessorFailure {
99
+ reason: string;
100
+ /** Set when a message could not be delivered. */
101
+ delivery?: DeliveryDirection;
102
+ }
103
+
81
104
  export type FileProcessorBackendKind = "worker" | "child";
82
105
 
106
+ /** Error code of a file whose message to or from the processor was lost. */
107
+ export const PROCESSOR_MESSAGE_FAILED = "PROCESSOR_MESSAGE_FAILED";
108
+
83
109
  /** Error codes of a file stopped at its budget. */
84
110
  export const BUDGET_ERROR_CODES: ReadonlySet<string> = new Set([
85
111
  "TIMEOUT",
@@ -149,7 +175,7 @@ interface Processor {
149
175
  /** Settles once the processor has loaded and can take a file. */
150
176
  readonly ready: Promise<void>;
151
177
  /** Settles, with a reason, if the processor dies or errors. */
152
- readonly failed: Promise<string>;
178
+ readonly failed: Promise<ProcessorFailure>;
153
179
  /** Send one file; its messages go to `onMessage`. */
154
180
  run(
155
181
  request: PrepareFileRequest,
@@ -164,6 +190,8 @@ interface Processor {
164
190
  /** Child process id (child backend), for shutdown and tests. */
165
191
  readonly pid: number | null;
166
192
  stop(): void;
193
+ /** Tests only: raise the event Bun raises for an undeliverable message. */
194
+ simulateUndeliverable?(direction: "job" | "result"): void;
167
195
  }
168
196
 
169
197
  const phaseName = (index: number): string => PREPARE_PHASES[index] ?? "startup";
@@ -176,25 +204,38 @@ function startWorker(): Processor {
176
204
  const phaseSlot = new Int32Array(new SharedArrayBuffer(4));
177
205
  let onMessage: ((message: FileWorkerMessage) => void) | null = null;
178
206
  let markReady: () => void = () => undefined;
179
- let markFailed: (reason: string) => void = () => undefined;
207
+ let markFailed: (failure: ProcessorFailure) => void = () => undefined;
180
208
  let dead = false;
181
209
  const ready = new Promise<void>((resolve) => {
182
210
  markReady = resolve;
183
211
  });
184
- const failed = new Promise<string>((resolve) => {
185
- markFailed = (reason) => {
212
+ const failed = new Promise<ProcessorFailure>((resolve) => {
213
+ markFailed = (failure) => {
186
214
  dead = true;
187
- resolve(reason);
215
+ resolve(failure);
188
216
  };
189
217
  });
190
218
  worker.onmessage = (
191
- event: MessageEvent<FileWorkerMessage | ReadyMessage>
219
+ event: MessageEvent<
220
+ FileWorkerMessage | ReadyMessage | UndeliverableJobMessage
221
+ >
192
222
  ) => {
193
223
  if (event.data.type === "ready") markReady();
194
- else onMessage?.(event.data);
224
+ else if (event.data.type === "undeliverable-job") {
225
+ markFailed({
226
+ reason: "the file worker could not deserialize its job",
227
+ delivery: "job",
228
+ });
229
+ } else onMessage?.(event.data);
230
+ };
231
+ worker.onmessageerror = () => {
232
+ markFailed({
233
+ reason: "a message from the file worker could not be deserialized",
234
+ delivery: "result",
235
+ });
195
236
  };
196
237
  worker.onerror = (event: ErrorEvent) => {
197
- markFailed(event.message || "file worker failed");
238
+ markFailed({ reason: event.message || "file worker failed" });
198
239
  };
199
240
  return {
200
241
  kind: "worker",
@@ -212,7 +253,12 @@ function startWorker(): Processor {
212
253
  pid: null,
213
254
  stop() {
214
255
  worker.terminate();
215
- markFailed("file processor was stopped");
256
+ markFailed({ reason: "file processor was stopped" });
257
+ },
258
+ simulateUndeliverable(direction) {
259
+ if (direction === "result") {
260
+ worker.dispatchEvent(new MessageEvent("messageerror"));
261
+ } else worker.postMessage({ simulateUndeliverable: true });
216
262
  },
217
263
  };
218
264
  }
@@ -267,12 +313,21 @@ function startChild(): Processor {
267
313
  child.unref();
268
314
  liveChildren.add(child);
269
315
  ensureShutdownHooks();
270
- const failed = child.exited.then((code) => {
316
+ const failed = child.exited.then((code): ProcessorFailure => {
271
317
  exited = true;
272
318
  liveChildren.delete(child);
273
- return stopping
274
- ? "file processor was stopped"
275
- : `file processor child exited (${code})`;
319
+ if (stopping) return { reason: "file processor was stopped" };
320
+ // Bun closes the channel on both ends when either side receives a
321
+ // message it cannot deserialize. The child exits cleanly only once its
322
+ // channel is gone (file-child.ts), and a crash never exits cleanly; the
323
+ // exit can be reported before Bun's own disconnect callback.
324
+ if (code === 0) {
325
+ return {
326
+ reason: "the IPC channel to the file processor child closed",
327
+ delivery: "unknown",
328
+ };
329
+ }
330
+ return { reason: `file processor child exited (${code})` };
276
331
  });
277
332
  return {
278
333
  kind: "child",
@@ -367,6 +422,19 @@ export function activeFileProcessorPid(): number | null {
367
422
  return processor?.pid ?? null;
368
423
  }
369
424
 
425
+ /**
426
+ * Tests only: make the running worker processor raise the event Bun raises
427
+ * when a message cannot be deserialized (a real payload cannot be made to
428
+ * fail from JavaScript). False when no worker processor is running.
429
+ */
430
+ export function simulateUndeliverableMessage(
431
+ direction: "job" | "result"
432
+ ): boolean {
433
+ if (!processor?.simulateUndeliverable) return false;
434
+ processor.simulateUndeliverable(direction);
435
+ return true;
436
+ }
437
+
370
438
  // ─────────────────────────────────────────────────────────────────────────────
371
439
  // Processor lifecycle
372
440
  // ─────────────────────────────────────────────────────────────────────────────
@@ -429,11 +497,30 @@ const awaitReady = (active: Processor): Promise<string | null> => {
429
497
  });
430
498
  return Promise.race([
431
499
  active.ready.then(() => null),
432
- active.failed,
500
+ active.failed.then((failure) => failure.reason),
433
501
  timedOut,
434
502
  ]).finally(() => clearTimeout(timer));
435
503
  };
436
504
 
505
+ /** Failure of a file whose message to or from the processor was lost. */
506
+ function undeliverableFailure(
507
+ request: PrepareFileRequest,
508
+ kind: FileProcessorBackendKind,
509
+ phase: string,
510
+ failure: ProcessorFailure
511
+ ): PrepareFailure {
512
+ const direction = failure.delivery ?? "unknown";
513
+ const file = `${request.input.collection}/${request.input.relativePath}`;
514
+ console.error(
515
+ `gno: file processor message lost (direction ${direction}, backend ${kind}) for ${file} during ${phase}: ${failure.reason}; the processor was replaced`
516
+ );
517
+ return {
518
+ code: PROCESSOR_MESSAGE_FAILED,
519
+ message: `The file processor could not deliver a message during ${phase} (${failure.reason}); the processor was replaced`,
520
+ details: { direction, backend: kind, phase },
521
+ };
522
+ }
523
+
437
524
  const runOne = async (
438
525
  request: PrepareFileRequest,
439
526
  budget: FileBudgetLimits,
@@ -511,9 +598,23 @@ const runOne = async (
511
598
  const sampler = setInterval(() => {
512
599
  void checkMemory();
513
600
  }, MEMORY_SAMPLE_INTERVAL_MS);
514
- void active.failed.then((reason) =>
515
- finish({ ok: false, error: { code: "INTERNAL", message: reason } }, false)
516
- );
601
+ void active.failed.then((failure) => {
602
+ if (settled) return;
603
+ finish(
604
+ {
605
+ ok: false,
606
+ error: failure.delivery
607
+ ? undeliverableFailure(
608
+ request,
609
+ active.kind,
610
+ active.phase(),
611
+ failure
612
+ )
613
+ : { code: "INTERNAL", message: failure.reason },
614
+ },
615
+ false
616
+ );
617
+ });
517
618
  // A process already over the budget does not start another file.
518
619
  const rss = process.memoryUsage.rss();
519
620
  if (rss > budget.maxMemoryBytes) {
@@ -8,6 +8,7 @@ import type {
8
8
  FileWorkerMessage,
9
9
  FileWorkerRequest,
10
10
  ReadyMessage,
11
+ UndeliverableJobMessage,
11
12
  } from "./file-processor";
12
13
 
13
14
  import { defaultChunker } from "./chunker";
@@ -16,14 +17,31 @@ import { PREPARE_PHASES, prepareFile } from "./prepare-file";
16
17
 
17
18
  declare const self: Worker;
18
19
 
19
- const post = (message: FileWorkerMessage | ReadyMessage): void => {
20
+ const post = (
21
+ message: FileWorkerMessage | ReadyMessage | UndeliverableJobMessage
22
+ ): void => {
20
23
  self.postMessage(message);
21
24
  };
22
25
 
23
- self.onmessage = async (event: MessageEvent<FileWorkerRequest>) => {
26
+ // A job that cannot be deserialized never reaches onmessage (Bun 1.4.0+
27
+ // raises messageerror instead); tell the parent so the file fails at once.
28
+ // A listener, not `onmessageerror`: Bun's worker scope never calls that
29
+ // property.
30
+ self.addEventListener("messageerror", () => {
31
+ post({ type: "undeliverable-job" });
32
+ });
33
+
34
+ self.onmessage = async (
35
+ event: MessageEvent<FileWorkerRequest | { simulateUndeliverable: true }>
36
+ ) => {
24
37
  // Only the parent that created this worker may drive it; a Bun Worker sees
25
38
  // its parent's messages with an empty origin.
26
39
  if (event.origin !== "") return;
40
+ if ("simulateUndeliverable" in event.data) {
41
+ // Tests only (simulateUndeliverableMessage): raise the real event.
42
+ self.dispatchEvent(new MessageEvent("messageerror"));
43
+ return;
44
+ }
27
45
  const { request, phaseSlot } = event.data;
28
46
  const outcome = await prepareFile(request, {
29
47
  convert: convertForPreparation,
@@ -21,6 +21,7 @@ interface RemoveCollectionResult {
21
21
  removed: true;
22
22
  collection: string;
23
23
  configUpdated: true;
24
+ /** Documents are deleted; content, chunks and vectors stay until cleanup. */
24
25
  indexedDataRetained: true;
25
26
  note: string;
26
27
  }
@@ -101,7 +102,7 @@ export function handleRemoveCollection(
101
102
  collection: name,
102
103
  configUpdated: true,
103
104
  indexedDataRetained: true,
104
- note: "Documents remain in index until next full reindex or manual cleanup",
105
+ note: "Collection removed from config and its documents deleted from the index. Content, chunks and vectors stay until gno cleanup.",
105
106
  };
106
107
 
107
108
  return result;
@@ -65,8 +65,8 @@ export function BootstrapStatus({
65
65
 
66
66
  return (
67
67
  <section className="space-y-4">
68
- <div className="flex items-start justify-between gap-4">
69
- <div>
68
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
69
+ <div className="min-w-0">
70
70
  <div className="mb-2 flex items-center gap-2">
71
71
  <PackageIcon className="size-4 text-primary" />
72
72
  <h2 className="font-semibold text-2xl">Bootstrap & Storage</h2>
@@ -77,7 +77,7 @@ export function BootstrapStatus({
77
77
  </p>
78
78
  </div>
79
79
  {missingModels > 0 && !bootstrap.models.downloading && (
80
- <Button onClick={onDownloadModels} size="sm">
80
+ <Button className="self-start" onClick={onDownloadModels} size="sm">
81
81
  <DownloadIcon className="mr-2 size-4" />
82
82
  Download missing models
83
83
  </Button>