@gmickel/gno 1.43.0 → 1.44.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.
@@ -30,6 +30,14 @@ import {
30
30
  MAX_EMBED_CHUNK_ATTEMPTS,
31
31
  type EmbedStoreBatchResult,
32
32
  } from "../../embed/retry";
33
+ import {
34
+ findInterruptedStage,
35
+ formatInterruptedStage,
36
+ type InterruptedStage,
37
+ markIndexStageFinished,
38
+ markIndexStageRunning,
39
+ readIndexStageState,
40
+ } from "../../embed/stage-state";
33
41
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
34
42
  import { resolveDownloadPolicy } from "../../llm/policy";
35
43
  import { resolveModelUri } from "../../llm/registry";
@@ -75,9 +83,15 @@ export interface EmbedOptions extends CliWriteLeaseOptions {
75
83
  verbose?: boolean;
76
84
  /** Use cached models only (also used by the standalone setup worker). */
77
85
  offline?: boolean;
86
+ /**
87
+ * Resume preamble already handled by the caller (`gno index`). When set
88
+ * (including `null`), embed() neither re-detects an interrupted stage nor
89
+ * prints its own preamble; the value is echoed in the result.
90
+ */
91
+ resumedFrom?: InterruptedStage | null;
78
92
  }
79
93
 
80
- export type EmbedResult =
94
+ export type EmbedResult = (
81
95
  | {
82
96
  success: true;
83
97
  embedded: number;
@@ -91,7 +105,22 @@ export type EmbedResult =
91
105
  suggestion?: string;
92
106
  syncError?: string;
93
107
  }
94
- | { success: false; error: string; contention?: WriteLeaseContention };
108
+ | { success: false; error: string; contention?: WriteLeaseContention }
109
+ ) & {
110
+ /** Stage a previous run left `running` (fn-132 R4); null when none. */
111
+ resumedFrom?: InterruptedStage | null;
112
+ };
113
+
114
+ /**
115
+ * Stage outcome for the persisted marker and the `gno index` receipt: the
116
+ * embed stage completed only when every chunk it attempted was stored.
117
+ */
118
+ export function embedStageOutcome(result: EmbedResult): "completed" | "failed" {
119
+ if (!result.success) {
120
+ return "failed";
121
+ }
122
+ return result.errors === 0 && !result.syncError ? "completed" : "failed";
123
+ }
95
124
 
96
125
  // ─────────────────────────────────────────────────────────────────────────────
97
126
  // Helpers
@@ -458,214 +487,243 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
458
487
 
459
488
  // Get raw DB for vector ops (SqliteAdapter always implements SqliteDbProvider)
460
489
  const db = store.getRawDb();
461
- let embedPort: EmbeddingPort | null = null;
462
- let vectorIndex: VectorIndexPort | null = null;
463
490
 
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
- };
491
+ // Resume preamble (fn-132 R4): a stage left `running` by a dead process.
492
+ // `gno index` detects it once for both stages and hands it in.
493
+ const resumedFrom =
494
+ options.resumedFrom === undefined
495
+ ? findInterruptedStage(readIndexStageState(db))
496
+ : options.resumedFrom;
497
+ if (options.resumedFrom === undefined && resumedFrom && !options.json) {
498
+ process.stderr.write(`${formatInterruptedStage(resumedFrom)}\n`);
499
+ }
500
+
501
+ const runEmbedPass = async (): Promise<EmbedResult> => {
502
+ let embedPort: EmbeddingPort | null = null;
503
+ let vectorIndex: VectorIndexPort | null = null;
504
+ const runEmbedBody = async (): Promise<EmbedResult> => {
505
+ // Create stats port for backlog detection
506
+ const stats: VectorStatsPort = createVectorStatsPort(db);
507
+
508
+ let totalToEmbed = 0;
509
+ if (force) {
510
+ const forceCount = await getActiveChunkCount(db, options.collection);
511
+ if (!forceCount.ok) {
512
+ return { success: false, error: forceCount.error.message };
513
+ }
514
+ totalToEmbed = forceCount.value;
515
+
516
+ if (totalToEmbed === 0 || dryRun) {
517
+ const vecAvailable = await checkVecAvailable(db);
518
+ return {
519
+ success: true,
520
+ embedded: totalToEmbed,
521
+ errors: 0,
522
+ contentionErrors: 0,
523
+ duration: 0,
524
+ model: modelUri,
525
+ searchAvailable: vecAvailable,
526
+ errorSamples: [],
527
+ };
528
+ }
488
529
  }
489
- }
490
530
 
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
- });
531
+ // Create LLM adapter and embedding port with auto-download
532
+ let offline = options.offline;
533
+ if (offline === undefined) {
534
+ offline = getGlobals().offline;
535
+ }
536
+ const policy = resolveDownloadPolicy(process.env, {
537
+ offline,
538
+ });
499
539
 
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;
540
+ // Create progress renderer for model download (throttled to avoid spam)
541
+ const showDownloadProgress = !options.json && process.stderr.isTTY;
542
+ const downloadProgress = showDownloadProgress
543
+ ? createThrottledProgressRenderer(createProgressRenderer())
544
+ : undefined;
505
545
 
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, {
546
+ const llm = new LlmAdapter(config);
547
+ const recreateEmbedPort = async () => {
548
+ if (embedPort) {
549
+ await embedPort.dispose();
550
+ }
551
+ await llm.getManager().dispose(modelUri);
552
+ const recreated = await llm.createEmbeddingPort(modelUri, {
553
+ egressCollections: options.collection
554
+ ? [options.collection]
555
+ : "all",
556
+ policy,
557
+ onProgress: downloadProgress
558
+ ? (progress) => downloadProgress("embed", progress)
559
+ : undefined,
560
+ });
561
+ if (!recreated.ok) {
562
+ return { ok: false as const, error: recreated.error.message };
563
+ }
564
+ const initResult = await recreated.value.init();
565
+ if (!initResult.ok) {
566
+ await recreated.value.dispose();
567
+ return { ok: false as const, error: initResult.error.message };
568
+ }
569
+ return { ok: true as const, value: recreated.value };
570
+ };
571
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
513
572
  egressCollections: options.collection ? [options.collection] : "all",
514
573
  policy,
515
574
  onProgress: downloadProgress
516
575
  ? (progress) => downloadProgress("embed", progress)
517
576
  : undefined,
518
577
  });
519
- if (!recreated.ok) {
520
- return { ok: false as const, error: recreated.error.message };
578
+ if (!embedResult.ok) {
579
+ return { success: false, error: embedResult.error.message };
521
580
  }
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, {
530
- egressCollections: options.collection ? [options.collection] : "all",
531
- policy,
532
- onProgress: downloadProgress
533
- ? (progress) => downloadProgress("embed", progress)
534
- : undefined,
535
- });
536
- if (!embedResult.ok) {
537
- return { success: false, error: embedResult.error.message };
538
- }
539
- embedPort = embedResult.value;
540
-
541
- // Clear download progress line if shown
542
- if (showDownloadProgress) {
543
- process.stderr.write("\n");
544
- }
581
+ embedPort = embedResult.value;
545
582
 
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;
583
+ // Clear download progress line if shown
584
+ if (showDownloadProgress) {
585
+ process.stderr.write("\n");
586
+ }
552
587
 
553
- // Create vector index port
554
- const vectorResult = await createVectorIndexPort(db, {
555
- model: modelUri,
556
- dimensions,
557
- });
558
- if (!vectorResult.ok) {
559
- return { success: false, error: vectorResult.error.message };
560
- }
561
- vectorIndex = vectorResult.value;
588
+ // Discover dimensions via probe embedding
589
+ const probeResult = await embedPort.embed("dimension probe");
590
+ if (!probeResult.ok) {
591
+ return { success: false, error: probeResult.error.message };
592
+ }
593
+ const dimensions = probeResult.value.length;
562
594
 
563
- if (!force) {
564
- const embedFingerprint = getEmbeddingFingerprint({
565
- modelUri,
595
+ // Create vector index port
596
+ const vectorResult = await createVectorIndexPort(db, {
597
+ model: modelUri,
566
598
  dimensions,
567
599
  });
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 };
600
+ if (!vectorResult.ok) {
601
+ return { success: false, error: vectorResult.error.message };
578
602
  }
603
+ vectorIndex = vectorResult.value;
604
+
605
+ if (!force) {
606
+ const embedFingerprint = getEmbeddingFingerprint({
607
+ modelUri,
608
+ dimensions,
609
+ });
610
+ const backlogResult = await stats.countBacklog(
611
+ modelUri,
612
+ embedFingerprint,
613
+ {
614
+ collection: options.collection,
615
+ }
616
+ );
579
617
 
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
- };
618
+ if (!backlogResult.ok) {
619
+ return { success: false, error: backlogResult.error.message };
620
+ }
621
+
622
+ totalToEmbed = backlogResult.value;
623
+
624
+ if (totalToEmbed === 0 || dryRun) {
625
+ return {
626
+ success: true,
627
+ embedded: totalToEmbed,
628
+ errors: 0,
629
+ contentionErrors: 0,
630
+ duration: 0,
631
+ model: modelUri,
632
+ searchAvailable: vectorIndex.searchAvailable,
633
+ errorSamples: [],
634
+ };
635
+ }
593
636
  }
594
- }
595
637
 
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
- });
638
+ // Process batches
639
+ const result = await processBatches({
640
+ db,
641
+ stats,
642
+ embedPort,
643
+ vectorIndex,
644
+ modelUri,
645
+ collection: options.collection,
646
+ batchSize,
647
+ force,
648
+ showProgress: !options.json,
649
+ totalToEmbed,
650
+ verbose: options.verbose ?? false,
651
+ recreateEmbedPort,
652
+ });
611
653
 
612
- if (!result.ok) {
613
- return { success: false, error: result.error };
614
- }
654
+ if (!result.ok) {
655
+ return { success: false, error: result.error };
656
+ }
615
657
 
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) {
658
+ // Sync vec index if any vec0 writes failed (matches embedBacklog behavior)
659
+ if (vectorIndex.vecDirty) {
660
+ const syncResult = await vectorIndex.syncVecIndex();
661
+ if (syncResult.ok) {
662
+ const { added, removed } = syncResult.value;
663
+ if (added > 0 || removed > 0) {
664
+ if (!options.json) {
665
+ process.stdout.write(
666
+ `\n[vec] Synced index: +${added} -${removed}\n`
667
+ );
668
+ }
669
+ }
670
+ vectorIndex.vecDirty = false;
671
+ } else {
622
672
  if (!options.json) {
623
673
  process.stdout.write(
624
- `\n[vec] Synced index: +${added} -${removed}\n`
674
+ `\n[vec] Sync failed: ${syncResult.error.message}\n`
625
675
  );
626
676
  }
677
+ return {
678
+ success: true,
679
+ embedded: result.embedded,
680
+ errors: result.errors,
681
+ contentionErrors: result.contentionErrors,
682
+ duration: result.duration,
683
+ model: modelUri,
684
+ searchAvailable: vectorIndex.searchAvailable,
685
+ errorSamples: [
686
+ ...result.errorSamples,
687
+ syncResult.error.message,
688
+ ].slice(0, 5),
689
+ suggestion:
690
+ "Vector index sync failed after embedding. Rerun `gno embed` once more. If it repeats, run `gno vec sync`.",
691
+ syncError: syncResult.error.message,
692
+ };
627
693
  }
628
- vectorIndex.vecDirty = false;
629
- } else {
630
- if (!options.json) {
631
- process.stdout.write(
632
- `\n[vec] Sync failed: ${syncResult.error.message}\n`
633
- );
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
- };
651
694
  }
652
- }
653
695
 
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,
696
+ return {
697
+ success: true,
698
+ embedded: result.embedded,
699
+ errors: result.errors,
700
+ contentionErrors: result.contentionErrors,
701
+ duration: result.duration,
702
+ model: modelUri,
703
+ searchAvailable: vectorIndex.searchAvailable,
704
+ errorSamples: result.errorSamples,
705
+ suggestion: result.suggestion,
706
+ };
664
707
  };
665
- } finally {
666
- if (embedPort) {
667
- await embedPort.dispose();
708
+
709
+ try {
710
+ return await runEmbedBody();
711
+ } finally {
712
+ // Assigned inside runEmbedBody; the cast defeats TS's null narrowing.
713
+ await (embedPort as EmbeddingPort | null)?.dispose();
668
714
  }
715
+ };
716
+
717
+ // Persisted stage marker: `running` until the pass reports, so a process
718
+ // that dies here is surfaced by the next run's resume preamble.
719
+ markIndexStageRunning(db, "embed", { collection: options.collection });
720
+ let outcome: "completed" | "failed" = "failed";
721
+ try {
722
+ const result = await runEmbedPass();
723
+ outcome = embedStageOutcome(result);
724
+ return { ...result, resumedFrom };
725
+ } finally {
726
+ markIndexStageFinished(db, "embed", outcome);
669
727
  await store.close();
670
728
  }
671
729
  });