@gmickel/gno 2.12.2 → 2.13.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/README.md +1 -1
- package/assets/skill/SKILL.md +1 -1
- package/assets/skill/cli-reference.md +23 -10
- package/assets/skill/mcp-reference.md +2 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.12.2.zip → gno-browser-clipper-v2.13.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.13.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +66 -5
- package/spec/mcp.md +96 -1
- package/spec/output-schemas/convert.schema.json +97 -0
- package/spec/output-schemas/mcp-job-status.schema.json +17 -1
- package/spec/output-schemas/mcp-sync-result.schema.json +9 -2
- package/spec/output-schemas/search-results.schema.json +7 -0
- package/src/cli/commands/index-cmd.ts +50 -12
- package/src/cli/commands/query.ts +21 -8
- package/src/cli/commands/search.ts +11 -0
- package/src/cli/commands/vsearch.ts +17 -4
- package/src/cli/options.ts +19 -0
- package/src/cli/program.ts +79 -10
- package/src/core/collection-scope.ts +105 -0
- package/src/core/errors.ts +5 -0
- package/src/core/job-manager.ts +238 -19
- package/src/core/reindex-paths.ts +167 -0
- package/src/core/retrieval-trace-request.ts +1 -0
- package/src/ingestion/convert-file.ts +181 -0
- package/src/llm/registry.ts +20 -0
- package/src/mcp/http-egress.ts +21 -2
- package/src/mcp/tools/collection-scope.ts +25 -0
- package/src/mcp/tools/index-cmd.ts +66 -48
- package/src/mcp/tools/index.ts +74 -10
- package/src/mcp/tools/job-status.ts +7 -0
- package/src/mcp/tools/query.ts +28 -7
- package/src/mcp/tools/search.ts +4 -0
- package/src/mcp/tools/sync.ts +68 -60
- package/src/mcp/tools/vsearch.ts +9 -3
- package/src/pipeline/filters.ts +18 -3
- package/src/pipeline/graph-retrieval.ts +1 -0
- package/src/pipeline/hybrid.ts +15 -2
- package/src/pipeline/hydration.ts +7 -2
- package/src/pipeline/owner-fusion.ts +1 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +7 -0
- package/src/pipeline/vsearch.ts +10 -5
- package/src/sdk/client.ts +39 -11
- package/src/sdk/convert.ts +27 -0
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +1 -1
- package/src/serve/jobs.ts +4 -1
- package/src/serve/public/components/IndexingProgress.tsx +25 -3
- package/src/serve/routes/api.ts +164 -24
- package/src/store/sqlite/adapter.ts +97 -27
- package/src/store/sqlite/eligibility.ts +4 -0
- package/src/store/types.ts +7 -0
- package/src/store/vector/sqlite-vec.ts +67 -15
- package/src/store/vector/variant-search.ts +41 -4
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.2.zip.sha256 +0 -1
|
@@ -164,7 +164,7 @@ import {
|
|
|
164
164
|
import { getSchemaVersion, migrations, runMigrations } from "../migrations";
|
|
165
165
|
import { err, ok } from "../types";
|
|
166
166
|
import { getStoredEmbeddingFingerprint } from "../vector/freshness";
|
|
167
|
-
import { modelTableName } from "../vector/sqlite-vec";
|
|
167
|
+
import { bindReadConnection, modelTableName } from "../vector/sqlite-vec";
|
|
168
168
|
import {
|
|
169
169
|
getVariantStatus,
|
|
170
170
|
listVectorPartitions,
|
|
@@ -622,7 +622,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
622
622
|
private activeTransaction?: {
|
|
623
623
|
token: { revoked: boolean };
|
|
624
624
|
release: () => void;
|
|
625
|
+
db: Database;
|
|
625
626
|
};
|
|
627
|
+
/** Set while a read-only caller runs; its reads use `readDb`. */
|
|
628
|
+
private readonly readScope = new AsyncLocalStorage<true>();
|
|
629
|
+
/** Second connection for read-only callers, opened on first use. */
|
|
630
|
+
private readDb: Database | null = null;
|
|
626
631
|
private shutdownFenced = false;
|
|
627
632
|
private shutdownDeadline?: number;
|
|
628
633
|
private contextGeneration = 0;
|
|
@@ -741,6 +746,8 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
741
746
|
|
|
742
747
|
async close(): Promise<void> {
|
|
743
748
|
this.fenceForShutdown();
|
|
749
|
+
this.readDb?.close(false);
|
|
750
|
+
this.readDb = null;
|
|
744
751
|
if (this.db) {
|
|
745
752
|
try {
|
|
746
753
|
// Current Bun finalizes all statements and releases Windows handles now.
|
|
@@ -791,7 +798,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
791
798
|
// JS cannot interleave this synchronous rollback with a running callback.
|
|
792
799
|
// A callback suspended at await must never commit or use the store again.
|
|
793
800
|
transaction.token.revoked = true;
|
|
794
|
-
|
|
801
|
+
transaction.db.exec("ROLLBACK");
|
|
795
802
|
transaction.release();
|
|
796
803
|
this.activeTransaction = undefined;
|
|
797
804
|
}
|
|
@@ -800,6 +807,47 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
800
807
|
return this.db !== null;
|
|
801
808
|
}
|
|
802
809
|
|
|
810
|
+
/**
|
|
811
|
+
* Run a read-only caller on a second connection to the same index. All
|
|
812
|
+
* writes stay on the shared connection, so a write transaction there (a
|
|
813
|
+
* sync batch held open across file conversions) is never visible to these
|
|
814
|
+
* reads: WAL gives this connection the last committed state. An in-memory
|
|
815
|
+
* index has no second connection and reads on the shared one.
|
|
816
|
+
*/
|
|
817
|
+
withReadConnection<T>(fn: () => Promise<T>): Promise<T> {
|
|
818
|
+
if (this.dbPath === "" || this.dbPath === ":memory:") return fn();
|
|
819
|
+
return this.readScope.run(true, fn);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private openReadDb(main: Database): Database {
|
|
823
|
+
if (this.readDb) return this.readDb;
|
|
824
|
+
const db = new Database(this.dbPath);
|
|
825
|
+
try {
|
|
826
|
+
// A write here would wait out the busy timeout while the shared
|
|
827
|
+
// connection's batch, on the same event loop, cannot commit: fail fast.
|
|
828
|
+
db.exec("PRAGMA query_only = ON");
|
|
829
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
830
|
+
const busyTimeout =
|
|
831
|
+
main.query<{ timeout: number }, []>("PRAGMA busy_timeout").get()
|
|
832
|
+
?.timeout ?? DEFAULT_BUSY_TIMEOUT_MS;
|
|
833
|
+
db.exec(`PRAGMA busy_timeout = ${busyTimeout}`);
|
|
834
|
+
if (this.ftsTokenizer.startsWith("snowball")) {
|
|
835
|
+
const snowball = loadFts5Snowball(db);
|
|
836
|
+
if (!snowball.loaded)
|
|
837
|
+
throw new Error(`Failed to load fts5-snowball: ${snowball.error}`);
|
|
838
|
+
}
|
|
839
|
+
} catch (cause) {
|
|
840
|
+
db.close(false);
|
|
841
|
+
throw cause;
|
|
842
|
+
}
|
|
843
|
+
bindReadConnection(db, main, async () => {
|
|
844
|
+
const release = await this.acquireTransactionWriter();
|
|
845
|
+
release();
|
|
846
|
+
});
|
|
847
|
+
this.readDb = db;
|
|
848
|
+
return db;
|
|
849
|
+
}
|
|
850
|
+
|
|
803
851
|
/**
|
|
804
852
|
* Run an async function within a single SQLite transaction.
|
|
805
853
|
* Uses SAVEPOINT for nesting safety.
|
|
@@ -808,6 +856,9 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
808
856
|
* explicit BEGIN/COMMIT to support async callbacks.
|
|
809
857
|
*/
|
|
810
858
|
async withTransaction<T>(fn: () => Promise<T>): Promise<StoreResult<T>> {
|
|
859
|
+
// Writes always run on the shared connection, even from a read scope.
|
|
860
|
+
if (this.readScope.getStore())
|
|
861
|
+
return this.readScope.exit(() => this.withTransaction(fn));
|
|
811
862
|
const parent = this.txContext.getStore();
|
|
812
863
|
const connection = this.ensureOpen();
|
|
813
864
|
const isOuter = parent === undefined;
|
|
@@ -826,7 +877,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
826
877
|
if (isOuter) {
|
|
827
878
|
// IMMEDIATE reduces lock churn for bulk writes
|
|
828
879
|
db.exec("BEGIN IMMEDIATE");
|
|
829
|
-
this.activeTransaction = { token, release: releaseWriter
|
|
880
|
+
this.activeTransaction = { token, release: releaseWriter!, db };
|
|
830
881
|
} else {
|
|
831
882
|
db.exec(`SAVEPOINT ${savepoint}`);
|
|
832
883
|
}
|
|
@@ -872,6 +923,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
872
923
|
}
|
|
873
924
|
}
|
|
874
925
|
|
|
926
|
+
/** Write outside a transaction, on the shared connection even from a read scope. */
|
|
927
|
+
private async admitWrite<T>(write: (db: Database) => T): Promise<T> {
|
|
928
|
+
return this.readScope.exit(() => write(this.ensureOpen()));
|
|
929
|
+
}
|
|
930
|
+
|
|
875
931
|
private async acquireTransactionWriter(): Promise<() => void> {
|
|
876
932
|
const previous = this.txTail;
|
|
877
933
|
let release!: () => void;
|
|
@@ -913,8 +969,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
913
969
|
if (!this.db) {
|
|
914
970
|
throw new Error("Database not open");
|
|
915
971
|
}
|
|
916
|
-
|
|
917
|
-
|
|
972
|
+
const db =
|
|
973
|
+
this.readScope.getStore() && !this.txContext.getStore()
|
|
974
|
+
? this.openReadDb(this.db)
|
|
975
|
+
: this.db;
|
|
976
|
+
this.capShutdownBusyWait(db);
|
|
977
|
+
return db;
|
|
918
978
|
}
|
|
919
979
|
|
|
920
980
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1507,14 +1567,16 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1507
1567
|
async createRetrievalTrace(
|
|
1508
1568
|
input: RetrievalTraceInput
|
|
1509
1569
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1510
|
-
return
|
|
1570
|
+
return this.admitWrite((db) => createStoredTrace(db, input));
|
|
1511
1571
|
}
|
|
1512
1572
|
|
|
1513
1573
|
async mergeRetrievalTraceEgressLineage(
|
|
1514
1574
|
traceId: string,
|
|
1515
1575
|
lineage: EgressLineage
|
|
1516
1576
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1517
|
-
return
|
|
1577
|
+
return this.admitWrite((db) =>
|
|
1578
|
+
mergeStoredTraceEgressLineage(db, traceId, lineage)
|
|
1579
|
+
);
|
|
1518
1580
|
}
|
|
1519
1581
|
|
|
1520
1582
|
async getRetrievalTrace(
|
|
@@ -1542,37 +1604,39 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1542
1604
|
status: RetrievalTraceTerminalStatus,
|
|
1543
1605
|
updatedAtMs: number
|
|
1544
1606
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1545
|
-
return
|
|
1607
|
+
return this.admitWrite((db) =>
|
|
1608
|
+
finalizeStoredTrace(db, traceId, status, updatedAtMs)
|
|
1609
|
+
);
|
|
1546
1610
|
}
|
|
1547
1611
|
|
|
1548
1612
|
async appendRetrievalTraceRun(
|
|
1549
1613
|
input: RetrievalTraceRunInput
|
|
1550
1614
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1551
|
-
return
|
|
1615
|
+
return this.admitWrite((db) => appendStoredTraceRun(db, input));
|
|
1552
1616
|
}
|
|
1553
1617
|
|
|
1554
1618
|
async appendRetrievalTraceEvent(
|
|
1555
1619
|
input: RetrievalTraceEventInput
|
|
1556
1620
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1557
|
-
return
|
|
1621
|
+
return this.admitWrite((db) => appendStoredTraceEvent(db, input));
|
|
1558
1622
|
}
|
|
1559
1623
|
|
|
1560
1624
|
async appendRetrievalTraceJudgment(
|
|
1561
1625
|
input: RetrievalTraceJudgmentInput
|
|
1562
1626
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1563
|
-
return
|
|
1627
|
+
return this.admitWrite((db) => appendStoredTraceJudgment(db, input));
|
|
1564
1628
|
}
|
|
1565
1629
|
|
|
1566
1630
|
async appendRetrievalTraceExport(
|
|
1567
1631
|
input: RetrievalTraceExportInput
|
|
1568
1632
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1569
|
-
return
|
|
1633
|
+
return this.admitWrite((db) => appendStoredTraceExport(db, input));
|
|
1570
1634
|
}
|
|
1571
1635
|
|
|
1572
1636
|
async appendRetrievalTraceExportManifest(
|
|
1573
1637
|
input: RetrievalTraceExportManifestInput
|
|
1574
1638
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1575
|
-
return
|
|
1639
|
+
return this.admitWrite((db) => appendStoredTraceExportManifest(db, input));
|
|
1576
1640
|
}
|
|
1577
1641
|
|
|
1578
1642
|
async getRetrievalTraceExportManifest(
|
|
@@ -1590,32 +1654,34 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1590
1654
|
async getOrCreateRetrievalTraceRedactionSecret(): Promise<
|
|
1591
1655
|
StoreResult<string>
|
|
1592
1656
|
> {
|
|
1593
|
-
return
|
|
1657
|
+
return this.admitWrite((db) => getOrCreateStoredTraceRedactionSecret(db));
|
|
1594
1658
|
}
|
|
1595
1659
|
|
|
1596
1660
|
async deleteRetrievalTrace(
|
|
1597
1661
|
traceId: string
|
|
1598
1662
|
): Promise<StoreResult<RetrievalTraceDeleteCounts>> {
|
|
1599
|
-
return
|
|
1663
|
+
return this.admitWrite((db) => deleteStoredTrace(db, traceId));
|
|
1600
1664
|
}
|
|
1601
1665
|
|
|
1602
1666
|
async purgeRetrievalTraces(): Promise<
|
|
1603
1667
|
StoreResult<RetrievalTracePurgeResult>
|
|
1604
1668
|
> {
|
|
1605
|
-
return
|
|
1669
|
+
return this.admitWrite((db) => purgeStoredTraces(db));
|
|
1606
1670
|
}
|
|
1607
1671
|
|
|
1608
1672
|
async enforceRetrievalTraceRetention(
|
|
1609
1673
|
policy: RetrievalTraceRetentionPolicy,
|
|
1610
1674
|
nowMs: number
|
|
1611
1675
|
): Promise<StoreResult<RetrievalTraceRetentionResult>> {
|
|
1612
|
-
return
|
|
1676
|
+
return this.admitWrite((db) =>
|
|
1677
|
+
enforceStoredTraceRetention(db, policy, nowMs)
|
|
1678
|
+
);
|
|
1613
1679
|
}
|
|
1614
1680
|
|
|
1615
1681
|
async appendEgressAuditReceipt(
|
|
1616
1682
|
receipt: EgressAuditReceiptInput
|
|
1617
1683
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1618
|
-
return
|
|
1684
|
+
return this.admitWrite((db) => appendStoredEgressAuditReceipt(db, receipt));
|
|
1619
1685
|
}
|
|
1620
1686
|
|
|
1621
1687
|
async appendEgressAuditReceiptWithRetention(
|
|
@@ -1623,11 +1689,8 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1623
1689
|
policy: EgressAuditRetentionPolicy,
|
|
1624
1690
|
nowMs: number
|
|
1625
1691
|
): Promise<StoreResult<RetrievalTraceAppendResult>> {
|
|
1626
|
-
return
|
|
1627
|
-
|
|
1628
|
-
receipt,
|
|
1629
|
-
policy,
|
|
1630
|
-
nowMs
|
|
1692
|
+
return this.admitWrite((db) =>
|
|
1693
|
+
appendStoredEgressAuditReceiptWithRetention(db, receipt, policy, nowMs)
|
|
1631
1694
|
);
|
|
1632
1695
|
}
|
|
1633
1696
|
|
|
@@ -1647,7 +1710,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1647
1710
|
async deleteEgressAuditReceipt(
|
|
1648
1711
|
auditId: string
|
|
1649
1712
|
): Promise<StoreResult<EgressAuditDeleteResult>> {
|
|
1650
|
-
return
|
|
1713
|
+
return this.admitWrite((db) => deleteStoredEgressAuditReceipt(db, auditId));
|
|
1651
1714
|
}
|
|
1652
1715
|
|
|
1653
1716
|
async getEgressAuditStatus(): Promise<StoreResult<EgressAuditStatusResult>> {
|
|
@@ -1658,13 +1721,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1658
1721
|
policy: EgressAuditRetentionPolicy,
|
|
1659
1722
|
nowMs: number
|
|
1660
1723
|
): Promise<StoreResult<EgressAuditRetentionResult>> {
|
|
1661
|
-
return
|
|
1724
|
+
return this.admitWrite((db) =>
|
|
1725
|
+
enforceStoredEgressAuditRetention(db, policy, nowMs)
|
|
1726
|
+
);
|
|
1662
1727
|
}
|
|
1663
1728
|
|
|
1664
1729
|
async purgeEgressAuditReceipts(): Promise<
|
|
1665
1730
|
StoreResult<EgressAuditPurgeResult>
|
|
1666
1731
|
> {
|
|
1667
|
-
return
|
|
1732
|
+
return this.admitWrite((db) => purgeStoredEgressAuditReceipts(db));
|
|
1668
1733
|
}
|
|
1669
1734
|
|
|
1670
1735
|
getContextGeneration(): number {
|
|
@@ -2330,6 +2395,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2330
2395
|
mirrorHashes: string[],
|
|
2331
2396
|
options: {
|
|
2332
2397
|
collection?: string;
|
|
2398
|
+
collections?: string[];
|
|
2333
2399
|
activeOnly?: boolean;
|
|
2334
2400
|
} = {}
|
|
2335
2401
|
): Promise<StoreResult<DocumentRow[]>> {
|
|
@@ -2350,7 +2416,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2350
2416
|
|
|
2351
2417
|
// SQLite SQLITE_LIMIT_VARIABLE_NUMBER defaults to 999.
|
|
2352
2418
|
// Reserve headroom for optional non-IN parameters.
|
|
2353
|
-
const SQL_PARAM_LIMIT =
|
|
2419
|
+
const SQL_PARAM_LIMIT = 898;
|
|
2354
2420
|
|
|
2355
2421
|
for (let i = 0; i < uniqueHashes.length; i += SQL_PARAM_LIMIT) {
|
|
2356
2422
|
const batch = uniqueHashes.slice(i, i + SQL_PARAM_LIMIT);
|
|
@@ -2365,6 +2431,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2365
2431
|
clauses.push("collection = ?");
|
|
2366
2432
|
params.push(options.collection);
|
|
2367
2433
|
}
|
|
2434
|
+
if (options.collections !== undefined) {
|
|
2435
|
+
clauses.push("collection IN (SELECT value FROM json_each(?))");
|
|
2436
|
+
params.push(JSON.stringify(options.collections));
|
|
2437
|
+
}
|
|
2368
2438
|
|
|
2369
2439
|
const sql = `SELECT * FROM documents INDEXED BY idx_documents_mirror_hash WHERE ${clauses.join(" AND ")} ORDER BY id`;
|
|
2370
2440
|
rows.push(...db.query<DbDocumentRow, string[]>(sql).all(...params));
|
|
@@ -90,6 +90,10 @@ export function buildEligibleDocumentQuery(
|
|
|
90
90
|
conditions.push("d.collection = ?");
|
|
91
91
|
params.push(options.collection);
|
|
92
92
|
}
|
|
93
|
+
if (options.collections !== undefined) {
|
|
94
|
+
conditions.push("d.collection IN (SELECT value FROM json_each(?))");
|
|
95
|
+
params.push(JSON.stringify(options.collections));
|
|
96
|
+
}
|
|
93
97
|
if (options.relPathPrefix !== undefined) {
|
|
94
98
|
conditions.push(
|
|
95
99
|
"(COALESCE(NULLIF(d.record_source_path, ''), d.rel_path) = ? OR substr(COALESCE(NULLIF(d.record_source_path, ''), d.rel_path), 1, length(?) + 1) = ? || '/')"
|
package/src/store/types.ts
CHANGED
|
@@ -718,6 +718,11 @@ export interface DocumentEligibilityOptions {
|
|
|
718
718
|
exclude?: string[];
|
|
719
719
|
/** Filter by collection */
|
|
720
720
|
collection?: string;
|
|
721
|
+
/**
|
|
722
|
+
* Filter to documents in ANY of these collections, before ranking and LIMIT.
|
|
723
|
+
* Undefined is unrestricted; an empty array denies all.
|
|
724
|
+
*/
|
|
725
|
+
collections?: string[];
|
|
721
726
|
/** Internal exact relative-path boundary applied before ranking and LIMIT. */
|
|
722
727
|
relPathPrefix?: string;
|
|
723
728
|
/** Filter to docs with ANY of these tags */
|
|
@@ -1945,6 +1950,8 @@ export interface StorePort {
|
|
|
1945
1950
|
mirrorHashes: string[],
|
|
1946
1951
|
options?: {
|
|
1947
1952
|
collection?: string;
|
|
1953
|
+
/** Keep only documents in ANY of these collections; empty denies all. */
|
|
1954
|
+
collections?: string[];
|
|
1948
1955
|
activeOnly?: boolean;
|
|
1949
1956
|
}
|
|
1950
1957
|
): Promise<StoreResult<DocumentRow[]>>;
|
|
@@ -68,6 +68,37 @@ export function modelTableName(modelUri: string): string {
|
|
|
68
68
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
69
|
|
|
70
70
|
const indexDatabases = new WeakMap<VectorIndexPort, Database>();
|
|
71
|
+
/** Query-only read connections, mapped to the shared connection that writes. */
|
|
72
|
+
const writerDatabases = new WeakMap<
|
|
73
|
+
Database,
|
|
74
|
+
{ writer: Database; idle: () => Promise<void> }
|
|
75
|
+
>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Register `reader` as a query-only connection whose writes go to `writer`;
|
|
79
|
+
* `idle` resolves once no write transaction is open on `writer`.
|
|
80
|
+
*/
|
|
81
|
+
export function bindReadConnection(
|
|
82
|
+
reader: Database,
|
|
83
|
+
writer: Database,
|
|
84
|
+
idle: () => Promise<void>
|
|
85
|
+
): void {
|
|
86
|
+
writerDatabases.set(reader, { writer, idle });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The connection to write on for `db`: itself, or for a query-only read
|
|
91
|
+
* connection, the shared connection behind it. Partition resolution records
|
|
92
|
+
* runtime callers and migrates legacy partitions, so it runs there.
|
|
93
|
+
*/
|
|
94
|
+
export function writerConnection(db: Database): Database {
|
|
95
|
+
return writerDatabases.get(db)?.writer ?? db;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Wait until the shared connection behind `db` has committed its open batch. */
|
|
99
|
+
export function writerIdle(db: Database): Promise<void> {
|
|
100
|
+
return writerDatabases.get(db)?.idle() ?? Promise.resolve();
|
|
101
|
+
}
|
|
71
102
|
|
|
72
103
|
/** Partition resolution needs the database behind a vector index port. */
|
|
73
104
|
export function getVectorIndexDatabase(
|
|
@@ -131,9 +162,22 @@ export async function createVectorIndexPort(
|
|
|
131
162
|
loadError = e instanceof Error ? e.message : String(e);
|
|
132
163
|
}
|
|
133
164
|
|
|
165
|
+
// A query-only (read) connection cannot create the table. A missing table
|
|
166
|
+
// there means no vectors were ever stored for this model: search is empty.
|
|
167
|
+
const tableExists =
|
|
168
|
+
db
|
|
169
|
+
.query<{ found: number }, [string]>(
|
|
170
|
+
"SELECT 1 AS found FROM sqlite_master WHERE name = ?"
|
|
171
|
+
)
|
|
172
|
+
.get(tableName) !== null;
|
|
173
|
+
const queryOnly =
|
|
174
|
+
db.query<{ query_only: number }, []>("PRAGMA query_only").get()
|
|
175
|
+
?.query_only === 1;
|
|
176
|
+
const noVectorsYet = searchAvailable && !tableExists && queryOnly;
|
|
177
|
+
|
|
134
178
|
// Create per-model vec0 table if extension available
|
|
135
179
|
// Graceful degradation: if table creation fails, storage still works
|
|
136
|
-
if (searchAvailable) {
|
|
180
|
+
if (searchAvailable && !tableExists && !queryOnly) {
|
|
137
181
|
try {
|
|
138
182
|
db.exec(`
|
|
139
183
|
CREATE VIRTUAL TABLE IF NOT EXISTS ${tableName} USING vec0(
|
|
@@ -161,26 +205,32 @@ export async function createVectorIndexPort(
|
|
|
161
205
|
`);
|
|
162
206
|
|
|
163
207
|
// Prepared statements for vec0 table (if available)
|
|
164
|
-
const deleteVecChunkStmt =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
208
|
+
const deleteVecChunkStmt =
|
|
209
|
+
searchAvailable && !noVectorsYet
|
|
210
|
+
? db.prepare(`DELETE FROM ${tableName} WHERE chunk_id = ?`)
|
|
211
|
+
: null;
|
|
212
|
+
|
|
213
|
+
const insertVecStmt =
|
|
214
|
+
searchAvailable && !noVectorsYet
|
|
215
|
+
? db.prepare(
|
|
216
|
+
`INSERT INTO ${tableName} (chunk_id, embedding) VALUES (?, ?)`
|
|
217
|
+
)
|
|
218
|
+
: null;
|
|
171
219
|
|
|
172
|
-
const searchStmt =
|
|
173
|
-
|
|
220
|
+
const searchStmt =
|
|
221
|
+
searchAvailable && !noVectorsYet
|
|
222
|
+
? db.prepare(`
|
|
174
223
|
SELECT chunk_id, distance
|
|
175
224
|
FROM ${tableName}
|
|
176
225
|
WHERE embedding MATCH ?
|
|
177
226
|
AND k = ?
|
|
178
227
|
`)
|
|
179
|
-
|
|
228
|
+
: null;
|
|
180
229
|
|
|
181
|
-
const deleteVecStmt =
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
const deleteVecStmt =
|
|
231
|
+
searchAvailable && !noVectorsYet
|
|
232
|
+
? db.prepare(`DELETE FROM ${tableName} WHERE chunk_id LIKE ? || ':%'`)
|
|
233
|
+
: null;
|
|
184
234
|
|
|
185
235
|
// Track if vec0 writes failed this session (needs sync)
|
|
186
236
|
let vecDirty = false;
|
|
@@ -291,7 +341,7 @@ export async function createVectorIndexPort(
|
|
|
291
341
|
k: number,
|
|
292
342
|
searchOptions?: VectorSearchOptions
|
|
293
343
|
): Promise<StoreResult<VectorSearchResult[]>> {
|
|
294
|
-
if (!
|
|
344
|
+
if (!searchAvailable || !(searchStmt || noVectorsYet)) {
|
|
295
345
|
return Promise.resolve(
|
|
296
346
|
err("VEC_SEARCH_UNAVAILABLE", formatUnavailableMessage(loadError))
|
|
297
347
|
);
|
|
@@ -397,6 +447,8 @@ export async function createVectorIndexPort(
|
|
|
397
447
|
)
|
|
398
448
|
);
|
|
399
449
|
}
|
|
450
|
+
// No per-model table on a read connection: nothing was ever stored.
|
|
451
|
+
if (!searchStmt) return Promise.resolve(ok([]));
|
|
400
452
|
const results = searchStmt.all(encodeEmbedding(embedding), k) as {
|
|
401
453
|
chunk_id: string;
|
|
402
454
|
distance: number;
|
|
@@ -13,10 +13,16 @@ import { buildEligibleDocumentQuery } from "../sqlite/eligibility";
|
|
|
13
13
|
import {
|
|
14
14
|
embeddingPartitionIdentity,
|
|
15
15
|
identityPartitionId,
|
|
16
|
+
isPartitionActivated,
|
|
16
17
|
resolveRuntimePartition,
|
|
17
18
|
retrievalUse,
|
|
18
19
|
} from "./runtime-compat";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
encodeEmbedding,
|
|
22
|
+
getVectorIndexDatabase,
|
|
23
|
+
writerConnection,
|
|
24
|
+
writerIdle,
|
|
25
|
+
} from "./sqlite-vec";
|
|
20
26
|
import { embeddingInputHash } from "./variants";
|
|
21
27
|
|
|
22
28
|
/** Fallback/warning code: this runtime may not read the activated vectors. */
|
|
@@ -35,13 +41,22 @@ export type SearchPartition =
|
|
|
35
41
|
* an activated partition gets a reason instead of an identity; vector spaces
|
|
36
42
|
* are never mixed.
|
|
37
43
|
*/
|
|
38
|
-
export
|
|
44
|
+
export function resolveVectorSearchIdentity(
|
|
39
45
|
port: EmbeddingPort,
|
|
40
46
|
vectorIndex: VectorIndexPort
|
|
47
|
+
): Promise<SearchPartition> {
|
|
48
|
+
return resolveSearchPartition(port, vectorIndex, false);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function resolveSearchPartition(
|
|
52
|
+
port: EmbeddingPort,
|
|
53
|
+
vectorIndex: VectorIndexPort,
|
|
54
|
+
waited: boolean
|
|
41
55
|
): Promise<SearchPartition> {
|
|
42
56
|
const primary = embeddingPartitionIdentity(port);
|
|
43
|
-
const
|
|
44
|
-
if (!primary || !
|
|
57
|
+
const indexDb = getVectorIndexDatabase(vectorIndex);
|
|
58
|
+
if (!primary || !indexDb) return { identity: primary };
|
|
59
|
+
const db = writerConnection(indexDb);
|
|
45
60
|
let resolved;
|
|
46
61
|
try {
|
|
47
62
|
resolved = await resolveRuntimePartition(db, port, primary);
|
|
@@ -61,6 +76,28 @@ export async function resolveVectorSearchIdentity(
|
|
|
61
76
|
);
|
|
62
77
|
if (use.kind === "unavailable")
|
|
63
78
|
return { unavailable: { reason: use.reason, building: use.building } };
|
|
79
|
+
// A first query on an upgraded index can migrate its legacy partition inside
|
|
80
|
+
// a sync batch still open on the shared connection; the read connection sees
|
|
81
|
+
// it only once that batch commits. Wait for that one batch, then resolve
|
|
82
|
+
// again (the migration commits with it, or reruns if the batch rolled back).
|
|
83
|
+
const partitionId = identityPartitionId(resolved.identity);
|
|
84
|
+
if (
|
|
85
|
+
indexDb !== db &&
|
|
86
|
+
isPartitionActivated(db, partitionId) &&
|
|
87
|
+
!isPartitionActivated(indexDb, partitionId)
|
|
88
|
+
) {
|
|
89
|
+
if (!waited) {
|
|
90
|
+
await writerIdle(indexDb);
|
|
91
|
+
return resolveSearchPartition(port, vectorIndex, true);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
unavailable: {
|
|
95
|
+
reason:
|
|
96
|
+
"its vector partition is being migrated while the index syncs; retry shortly",
|
|
97
|
+
building: false,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
64
101
|
return { identity: resolved.identity };
|
|
65
102
|
}
|
|
66
103
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
34953d40a06904ec1c0fc7c6df54aaa14cb24acb05b38c3f15d572a285f7b3fc gno-browser-clipper-v2.12.2.zip
|