@gmickel/gno 1.37.0 → 1.38.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 (40) hide show
  1. package/assets/spa-production.json.gz +0 -0
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.37.0.zip → gno-browser-clipper-v1.38.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.38.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +1 -1
  6. package/spec/cli.md +35 -15
  7. package/src/cli/commands/cleanup.ts +8 -2
  8. package/src/cli/commands/collection/clear-embeddings.ts +6 -1
  9. package/src/cli/commands/doctor-activation.ts +5 -1
  10. package/src/cli/commands/doctor.ts +72 -2
  11. package/src/cli/commands/embed.ts +227 -194
  12. package/src/cli/commands/index-cmd.ts +74 -50
  13. package/src/cli/commands/init.ts +5 -1
  14. package/src/cli/commands/profile-apply.ts +5 -1
  15. package/src/cli/commands/setup-activation.ts +2 -1
  16. package/src/cli/commands/setup.ts +2 -1
  17. package/src/cli/commands/shared.ts +5 -1
  18. package/src/cli/commands/status.ts +5 -1
  19. package/src/cli/commands/tags.ts +18 -3
  20. package/src/cli/commands/update.ts +34 -27
  21. package/src/cli/commands/vec.ts +13 -4
  22. package/src/cli/errors.ts +3 -2
  23. package/src/cli/program.ts +345 -194
  24. package/src/config/defaults.ts +2 -0
  25. package/src/config/index.ts +3 -0
  26. package/src/config/types.ts +32 -1
  27. package/src/core/file-lock.ts +16 -4
  28. package/src/core/write-lease.ts +354 -0
  29. package/src/embed/backlog.ts +9 -1
  30. package/src/embed/retry.ts +116 -3
  31. package/src/sdk/client.ts +3 -1
  32. package/src/sdk/embed.ts +8 -3
  33. package/src/sdk/types.ts +2 -0
  34. package/src/serve/embed-scheduler.ts +8 -0
  35. package/src/serve/resident-runtime.ts +5 -1
  36. package/src/serve/spa-production-build.ts +53 -7
  37. package/src/store/sqlite/adapter.ts +28 -4
  38. package/src/store/sqlite/scoped-index.ts +5 -1
  39. package/src/store/vector/sqlite-vec.ts +2 -1
  40. package/browser-extension/artifacts/gno-browser-clipper-v1.37.0.zip.sha256 +0 -1
@@ -17,6 +17,11 @@ import {
17
17
  isInitialized,
18
18
  loadConfig,
19
19
  } from "../../config";
20
+ import {
21
+ type CliWriteLeaseOptions,
22
+ type WriteLeaseContention,
23
+ withCliWriteLease,
24
+ } from "../../core/write-lease";
20
25
  import { getEmbeddingFingerprint } from "../../embed/fingerprint";
21
26
  import {
22
27
  addUniqueSamples,
@@ -47,7 +52,7 @@ import {
47
52
  // Types
48
53
  // ─────────────────────────────────────────────────────────────────────────────
49
54
 
50
- export interface EmbedOptions {
55
+ export interface EmbedOptions extends CliWriteLeaseOptions {
51
56
  /** Override config path */
52
57
  configPath?: string;
53
58
  /** Index name */
@@ -77,6 +82,8 @@ export type EmbedResult =
77
82
  success: true;
78
83
  embedded: number;
79
84
  errors: number;
85
+ /** Chunks deferred by SQLITE_BUSY/LOCKED after retries (fn-127 R6). */
86
+ contentionErrors: number;
80
87
  duration: number;
81
88
  model: string;
82
89
  searchAvailable: boolean;
@@ -84,7 +91,7 @@ export type EmbedResult =
84
91
  suggestion?: string;
85
92
  syncError?: string;
86
93
  }
87
- | { success: false; error: string };
94
+ | { success: false; error: string; contention?: WriteLeaseContention };
88
95
 
89
96
  // ─────────────────────────────────────────────────────────────────────────────
90
97
  // Helpers
@@ -137,6 +144,7 @@ type BatchResult =
137
144
  ok: true;
138
145
  embedded: number;
139
146
  errors: number;
147
+ contentionErrors: number;
140
148
  duration: number;
141
149
  errorSamples: string[];
142
150
  suggestion?: string;
@@ -152,6 +160,7 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
152
160
  const startTime = Date.now();
153
161
  let embedded = 0;
154
162
  let errors = 0;
163
+ let contentionErrors = 0;
155
164
  const errorSamples: string[] = [];
156
165
  let suggestion: string | undefined;
157
166
  let cursor: Cursor | undefined;
@@ -279,6 +288,7 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
279
288
  suggestion ||= retryResult.suggestion;
280
289
  embedded += retryResult.embedded;
281
290
  errors += retryResult.errors;
291
+ contentionErrors += retryResult.contentionErrors;
282
292
  retryEmbedded += retryResult.embedded;
283
293
 
284
294
  const retryByKey = new Set(
@@ -331,6 +341,7 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
331
341
  suggestion ||= batchStoreResult.suggestion;
332
342
  embedded += batchStoreResult.embedded;
333
343
  errors += batchStoreResult.errors;
344
+ contentionErrors += batchStoreResult.contentionErrors;
334
345
  enqueueRetryItems(batchStoreResult.retryItems, 1);
335
346
 
336
347
  if (embedded > beforeEmbedded) {
@@ -358,6 +369,7 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
358
369
  ok: true,
359
370
  embedded,
360
371
  errors,
372
+ contentionErrors,
361
373
  duration: (Date.now() - startTime) / 1000,
362
374
  errorSamples,
363
375
  suggestion,
@@ -407,7 +419,11 @@ async function initEmbedContext(
407
419
  const paths = getConfigPaths();
408
420
  store.setConfigPath(configPath ?? paths.configFile);
409
421
 
410
- const openResult = await store.open(dbPath, config.ftsTokenizer);
422
+ const openResult = await store.open(
423
+ dbPath,
424
+ config.ftsTokenizer,
425
+ config.busyTimeoutMs
426
+ );
411
427
  if (!openResult.ok) {
412
428
  return { ok: false, error: openResult.error.message };
413
429
  }
@@ -427,226 +443,232 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
427
443
  const force = options.force ?? false;
428
444
  const dryRun = options.dryRun ?? false;
429
445
 
430
- // Initialize config and store
431
- const initResult = await initEmbedContext(
432
- options.configPath,
433
- options.indexName,
434
- options.collection,
435
- options.model
436
- );
437
- if (!initResult.ok) {
438
- return { success: false, error: initResult.error };
439
- }
440
- const { config, modelUri, store } = initResult;
441
-
442
- // Get raw DB for vector ops (SqliteAdapter always implements SqliteDbProvider)
443
- const db = store.getRawDb();
444
- let embedPort: EmbeddingPort | null = null;
445
- let vectorIndex: VectorIndexPort | null = null;
446
-
447
- try {
448
- // Create stats port for backlog detection
449
- const stats: VectorStatsPort = createVectorStatsPort(db);
450
-
451
- let totalToEmbed = 0;
452
- if (force) {
453
- const forceCount = await getActiveChunkCount(db, options.collection);
454
- if (!forceCount.ok) {
455
- return { success: false, error: forceCount.error.message };
456
- }
457
- totalToEmbed = forceCount.value;
458
-
459
- if (totalToEmbed === 0 || dryRun) {
460
- const vecAvailable = await checkVecAvailable(db);
461
- return {
462
- success: true,
463
- embedded: totalToEmbed,
464
- errors: 0,
465
- duration: 0,
466
- model: modelUri,
467
- searchAvailable: vecAvailable,
468
- errorSamples: [],
469
- };
470
- }
446
+ return await withCliWriteLease(options, async () => {
447
+ // Initialize config and store
448
+ const initResult = await initEmbedContext(
449
+ options.configPath,
450
+ options.indexName,
451
+ options.collection,
452
+ options.model
453
+ );
454
+ if (!initResult.ok) {
455
+ return { success: false, error: initResult.error };
471
456
  }
457
+ const { config, modelUri, store } = initResult;
458
+
459
+ // Get raw DB for vector ops (SqliteAdapter always implements SqliteDbProvider)
460
+ const db = store.getRawDb();
461
+ let embedPort: EmbeddingPort | null = null;
462
+ let vectorIndex: VectorIndexPort | null = null;
463
+
464
+ try {
465
+ // Create stats port for backlog detection
466
+ const stats: VectorStatsPort = createVectorStatsPort(db);
467
+
468
+ let totalToEmbed = 0;
469
+ if (force) {
470
+ const forceCount = await getActiveChunkCount(db, options.collection);
471
+ if (!forceCount.ok) {
472
+ return { success: false, error: forceCount.error.message };
473
+ }
474
+ totalToEmbed = forceCount.value;
475
+
476
+ if (totalToEmbed === 0 || dryRun) {
477
+ const vecAvailable = await checkVecAvailable(db);
478
+ return {
479
+ success: true,
480
+ embedded: totalToEmbed,
481
+ errors: 0,
482
+ contentionErrors: 0,
483
+ duration: 0,
484
+ model: modelUri,
485
+ searchAvailable: vecAvailable,
486
+ errorSamples: [],
487
+ };
488
+ }
489
+ }
472
490
 
473
- // Create LLM adapter and embedding port with auto-download
474
- let offline = options.offline;
475
- if (offline === undefined) {
476
- offline = getGlobals().offline;
477
- }
478
- const policy = resolveDownloadPolicy(process.env, {
479
- offline,
480
- });
491
+ // Create LLM adapter and embedding port with auto-download
492
+ let offline = options.offline;
493
+ if (offline === undefined) {
494
+ offline = getGlobals().offline;
495
+ }
496
+ const policy = resolveDownloadPolicy(process.env, {
497
+ offline,
498
+ });
481
499
 
482
- // Create progress renderer for model download (throttled to avoid spam)
483
- const showDownloadProgress = !options.json && process.stderr.isTTY;
484
- const downloadProgress = showDownloadProgress
485
- ? createThrottledProgressRenderer(createProgressRenderer())
486
- : undefined;
500
+ // Create progress renderer for model download (throttled to avoid spam)
501
+ const showDownloadProgress = !options.json && process.stderr.isTTY;
502
+ const downloadProgress = showDownloadProgress
503
+ ? createThrottledProgressRenderer(createProgressRenderer())
504
+ : undefined;
487
505
 
488
- const llm = new LlmAdapter(config);
489
- const recreateEmbedPort = async () => {
490
- if (embedPort) {
491
- await embedPort.dispose();
492
- }
493
- await llm.getManager().dispose(modelUri);
494
- const recreated = await llm.createEmbeddingPort(modelUri, {
506
+ const llm = new LlmAdapter(config);
507
+ const recreateEmbedPort = async () => {
508
+ if (embedPort) {
509
+ await embedPort.dispose();
510
+ }
511
+ await llm.getManager().dispose(modelUri);
512
+ const recreated = await llm.createEmbeddingPort(modelUri, {
513
+ egressCollections: options.collection ? [options.collection] : "all",
514
+ policy,
515
+ onProgress: downloadProgress
516
+ ? (progress) => downloadProgress("embed", progress)
517
+ : undefined,
518
+ });
519
+ if (!recreated.ok) {
520
+ return { ok: false as const, error: recreated.error.message };
521
+ }
522
+ const initResult = await recreated.value.init();
523
+ if (!initResult.ok) {
524
+ await recreated.value.dispose();
525
+ return { ok: false as const, error: initResult.error.message };
526
+ }
527
+ return { ok: true as const, value: recreated.value };
528
+ };
529
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
495
530
  egressCollections: options.collection ? [options.collection] : "all",
496
531
  policy,
497
532
  onProgress: downloadProgress
498
533
  ? (progress) => downloadProgress("embed", progress)
499
534
  : undefined,
500
535
  });
501
- if (!recreated.ok) {
502
- return { ok: false as const, error: recreated.error.message };
503
- }
504
- const initResult = await recreated.value.init();
505
- if (!initResult.ok) {
506
- await recreated.value.dispose();
507
- return { ok: false as const, error: initResult.error.message };
536
+ if (!embedResult.ok) {
537
+ return { success: false, error: embedResult.error.message };
508
538
  }
509
- return { ok: true as const, value: recreated.value };
510
- };
511
- const embedResult = await llm.createEmbeddingPort(modelUri, {
512
- egressCollections: options.collection ? [options.collection] : "all",
513
- policy,
514
- onProgress: downloadProgress
515
- ? (progress) => downloadProgress("embed", progress)
516
- : undefined,
517
- });
518
- if (!embedResult.ok) {
519
- return { success: false, error: embedResult.error.message };
520
- }
521
- embedPort = embedResult.value;
539
+ embedPort = embedResult.value;
522
540
 
523
- // Clear download progress line if shown
524
- if (showDownloadProgress) {
525
- process.stderr.write("\n");
526
- }
527
-
528
- // Discover dimensions via probe embedding
529
- const probeResult = await embedPort.embed("dimension probe");
530
- if (!probeResult.ok) {
531
- return { success: false, error: probeResult.error.message };
532
- }
533
- const dimensions = probeResult.value.length;
541
+ // Clear download progress line if shown
542
+ if (showDownloadProgress) {
543
+ process.stderr.write("\n");
544
+ }
534
545
 
535
- // Create vector index port
536
- const vectorResult = await createVectorIndexPort(db, {
537
- model: modelUri,
538
- dimensions,
539
- });
540
- if (!vectorResult.ok) {
541
- return { success: false, error: vectorResult.error.message };
542
- }
543
- vectorIndex = vectorResult.value;
546
+ // Discover dimensions via probe embedding
547
+ const probeResult = await embedPort.embed("dimension probe");
548
+ if (!probeResult.ok) {
549
+ return { success: false, error: probeResult.error.message };
550
+ }
551
+ const dimensions = probeResult.value.length;
544
552
 
545
- if (!force) {
546
- const embedFingerprint = getEmbeddingFingerprint({
547
- modelUri,
553
+ // Create vector index port
554
+ const vectorResult = await createVectorIndexPort(db, {
555
+ model: modelUri,
548
556
  dimensions,
549
557
  });
550
- const backlogResult = await stats.countBacklog(
551
- modelUri,
552
- embedFingerprint,
553
- {
554
- collection: options.collection,
555
- }
556
- );
557
-
558
- if (!backlogResult.ok) {
559
- return { success: false, error: backlogResult.error.message };
558
+ if (!vectorResult.ok) {
559
+ return { success: false, error: vectorResult.error.message };
560
560
  }
561
+ vectorIndex = vectorResult.value;
562
+
563
+ if (!force) {
564
+ const embedFingerprint = getEmbeddingFingerprint({
565
+ modelUri,
566
+ dimensions,
567
+ });
568
+ const backlogResult = await stats.countBacklog(
569
+ modelUri,
570
+ embedFingerprint,
571
+ {
572
+ collection: options.collection,
573
+ }
574
+ );
575
+
576
+ if (!backlogResult.ok) {
577
+ return { success: false, error: backlogResult.error.message };
578
+ }
561
579
 
562
- totalToEmbed = backlogResult.value;
563
-
564
- if (totalToEmbed === 0 || dryRun) {
565
- return {
566
- success: true,
567
- embedded: totalToEmbed,
568
- errors: 0,
569
- duration: 0,
570
- model: modelUri,
571
- searchAvailable: vectorIndex.searchAvailable,
572
- errorSamples: [],
573
- };
580
+ totalToEmbed = backlogResult.value;
581
+
582
+ if (totalToEmbed === 0 || dryRun) {
583
+ return {
584
+ success: true,
585
+ embedded: totalToEmbed,
586
+ errors: 0,
587
+ contentionErrors: 0,
588
+ duration: 0,
589
+ model: modelUri,
590
+ searchAvailable: vectorIndex.searchAvailable,
591
+ errorSamples: [],
592
+ };
593
+ }
574
594
  }
575
- }
576
595
 
577
- // Process batches
578
- const result = await processBatches({
579
- db,
580
- stats,
581
- embedPort,
582
- vectorIndex,
583
- modelUri,
584
- collection: options.collection,
585
- batchSize,
586
- force,
587
- showProgress: !options.json,
588
- totalToEmbed,
589
- verbose: options.verbose ?? false,
590
- recreateEmbedPort,
591
- });
596
+ // Process batches
597
+ const result = await processBatches({
598
+ db,
599
+ stats,
600
+ embedPort,
601
+ vectorIndex,
602
+ modelUri,
603
+ collection: options.collection,
604
+ batchSize,
605
+ force,
606
+ showProgress: !options.json,
607
+ totalToEmbed,
608
+ verbose: options.verbose ?? false,
609
+ recreateEmbedPort,
610
+ });
592
611
 
593
- if (!result.ok) {
594
- return { success: false, error: result.error };
595
- }
612
+ if (!result.ok) {
613
+ return { success: false, error: result.error };
614
+ }
596
615
 
597
- // Sync vec index if any vec0 writes failed (matches embedBacklog behavior)
598
- if (vectorIndex.vecDirty) {
599
- const syncResult = await vectorIndex.syncVecIndex();
600
- if (syncResult.ok) {
601
- const { added, removed } = syncResult.value;
602
- if (added > 0 || removed > 0) {
616
+ // Sync vec index if any vec0 writes failed (matches embedBacklog behavior)
617
+ if (vectorIndex.vecDirty) {
618
+ const syncResult = await vectorIndex.syncVecIndex();
619
+ if (syncResult.ok) {
620
+ const { added, removed } = syncResult.value;
621
+ if (added > 0 || removed > 0) {
622
+ if (!options.json) {
623
+ process.stdout.write(
624
+ `\n[vec] Synced index: +${added} -${removed}\n`
625
+ );
626
+ }
627
+ }
628
+ vectorIndex.vecDirty = false;
629
+ } else {
603
630
  if (!options.json) {
604
631
  process.stdout.write(
605
- `\n[vec] Synced index: +${added} -${removed}\n`
632
+ `\n[vec] Sync failed: ${syncResult.error.message}\n`
606
633
  );
607
634
  }
635
+ return {
636
+ success: true,
637
+ embedded: result.embedded,
638
+ errors: result.errors,
639
+ contentionErrors: result.contentionErrors,
640
+ duration: result.duration,
641
+ model: modelUri,
642
+ searchAvailable: vectorIndex.searchAvailable,
643
+ errorSamples: [
644
+ ...result.errorSamples,
645
+ syncResult.error.message,
646
+ ].slice(0, 5),
647
+ suggestion:
648
+ "Vector index sync failed after embedding. Rerun `gno embed` once more. If it repeats, run `gno vec sync`.",
649
+ syncError: syncResult.error.message,
650
+ };
608
651
  }
609
- vectorIndex.vecDirty = false;
610
- } else {
611
- if (!options.json) {
612
- process.stdout.write(
613
- `\n[vec] Sync failed: ${syncResult.error.message}\n`
614
- );
615
- }
616
- return {
617
- success: true,
618
- embedded: result.embedded,
619
- errors: result.errors,
620
- duration: result.duration,
621
- model: modelUri,
622
- searchAvailable: vectorIndex.searchAvailable,
623
- errorSamples: [
624
- ...result.errorSamples,
625
- syncResult.error.message,
626
- ].slice(0, 5),
627
- suggestion:
628
- "Vector index sync failed after embedding. Rerun `gno embed` once more. If it repeats, run `gno vec sync`.",
629
- syncError: syncResult.error.message,
630
- };
631
652
  }
632
- }
633
653
 
634
- return {
635
- success: true,
636
- embedded: result.embedded,
637
- errors: result.errors,
638
- duration: result.duration,
639
- model: modelUri,
640
- searchAvailable: vectorIndex.searchAvailable,
641
- errorSamples: result.errorSamples,
642
- suggestion: result.suggestion,
643
- };
644
- } finally {
645
- if (embedPort) {
646
- await embedPort.dispose();
654
+ return {
655
+ success: true,
656
+ embedded: result.embedded,
657
+ errors: result.errors,
658
+ contentionErrors: result.contentionErrors,
659
+ duration: result.duration,
660
+ model: modelUri,
661
+ searchAvailable: vectorIndex.searchAvailable,
662
+ errorSamples: result.errorSamples,
663
+ suggestion: result.suggestion,
664
+ };
665
+ } finally {
666
+ if (embedPort) {
667
+ await embedPort.dispose();
668
+ }
669
+ await store.close();
647
670
  }
648
- await store.close();
649
- }
671
+ });
650
672
  }
651
673
 
652
674
  // ─────────────────────────────────────────────────────────────────────────────
@@ -761,6 +783,7 @@ export function formatEmbed(
761
783
  {
762
784
  embedded: result.embedded,
763
785
  errors: result.errors,
786
+ contentionErrors: result.contentionErrors,
764
787
  duration: result.duration,
765
788
  model: result.model,
766
789
  searchAvailable: result.searchAvailable,
@@ -777,7 +800,11 @@ export function formatEmbed(
777
800
  return `Dry run: would embed ${result.embedded.toLocaleString()} chunks with model ${result.model}`;
778
801
  }
779
802
 
780
- if (result.embedded === 0 && result.errors === 0) {
803
+ if (
804
+ result.embedded === 0 &&
805
+ result.errors === 0 &&
806
+ result.contentionErrors === 0
807
+ ) {
781
808
  return "No chunks need embedding. All up to date.";
782
809
  }
783
810
 
@@ -798,6 +825,12 @@ export function formatEmbed(
798
825
  }
799
826
  }
800
827
 
828
+ if (result.contentionErrors > 0) {
829
+ lines.push(
830
+ `${result.contentionErrors} chunks deferred by index contention (SQLITE_BUSY) — not embedding failures. Rerun \`gno embed\` when the other writer finishes.`
831
+ );
832
+ }
833
+
801
834
  if (!result.searchAvailable) {
802
835
  lines.push(
803
836
  "Warning: sqlite-vec not available. Embeddings stored but KNN search disabled."