@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.
- package/browser-extension/artifacts/{gno-browser-clipper-v1.43.0.zip → gno-browser-clipper-v1.44.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.44.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +89 -8
- package/spec/mcp.md +12 -0
- package/spec/output-schemas/changes-follow-event.schema.json +35 -0
- package/spec/output-schemas/index-receipt.schema.json +135 -0
- package/spec/output-schemas/process-status.schema.json +76 -0
- package/src/cli/commands/changes-follow.ts +167 -0
- package/src/cli/commands/changes.ts +63 -0
- package/src/cli/commands/daemon.ts +35 -0
- package/src/cli/commands/doctor.ts +71 -0
- package/src/cli/commands/embed.ts +236 -178
- package/src/cli/commands/index-cmd.ts +238 -57
- package/src/cli/program.ts +94 -4
- package/src/config/types.ts +48 -0
- package/src/core/capture-sync.ts +144 -0
- package/src/core/capture.ts +10 -0
- package/src/core/findings-records.ts +381 -0
- package/src/core/findings-run-state.ts +282 -0
- package/src/embed/stage-state.ts +199 -0
- package/src/mcp/tools/capture.ts +91 -136
- package/src/serve/capture-service.ts +227 -53
- package/src/serve/findings-pass.ts +335 -0
- package/src/serve/resident-runtime.ts +42 -0
- package/src/serve/routes/api.ts +14 -14
- package/browser-extension/artifacts/gno-browser-clipper-v1.43.0.zip.sha256 +0 -1
|
@@ -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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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 (!
|
|
520
|
-
return {
|
|
578
|
+
if (!embedResult.ok) {
|
|
579
|
+
return { success: false, error: embedResult.error.message };
|
|
521
580
|
}
|
|
522
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
564
|
-
const
|
|
565
|
-
modelUri,
|
|
595
|
+
// Create vector index port
|
|
596
|
+
const vectorResult = await createVectorIndexPort(db, {
|
|
597
|
+
model: modelUri,
|
|
566
598
|
dimensions,
|
|
567
599
|
});
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
654
|
+
if (!result.ok) {
|
|
655
|
+
return { success: false, error: result.error };
|
|
656
|
+
}
|
|
615
657
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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]
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
await
|
|
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
|
});
|