@gmickel/gno 2.12.1 → 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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +1 -1
  3. package/assets/skill/cli-reference.md +23 -10
  4. package/assets/skill/mcp-reference.md +2 -0
  5. package/assets/spa-production.json.gz +0 -0
  6. package/browser-extension/artifacts/{gno-browser-clipper-v2.12.1.zip → gno-browser-clipper-v2.13.0.zip} +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.13.0.zip.sha256 +1 -0
  8. package/browser-extension/dist/manifest.json +1 -1
  9. package/package.json +1 -1
  10. package/spec/cli.md +66 -5
  11. package/spec/mcp.md +97 -2
  12. package/spec/output-schemas/convert.schema.json +97 -0
  13. package/spec/output-schemas/mcp-job-status.schema.json +17 -1
  14. package/spec/output-schemas/mcp-sync-result.schema.json +9 -2
  15. package/spec/output-schemas/search-results.schema.json +7 -0
  16. package/src/cli/commands/index-cmd.ts +50 -12
  17. package/src/cli/commands/query.ts +21 -8
  18. package/src/cli/commands/search.ts +11 -0
  19. package/src/cli/commands/vsearch.ts +17 -4
  20. package/src/cli/options.ts +19 -0
  21. package/src/cli/program.ts +79 -10
  22. package/src/core/collection-scope.ts +105 -0
  23. package/src/core/errors.ts +5 -0
  24. package/src/core/job-manager.ts +264 -47
  25. package/src/core/reindex-paths.ts +167 -0
  26. package/src/core/retrieval-trace-request.ts +1 -0
  27. package/src/ingestion/convert-file.ts +181 -0
  28. package/src/llm/registry.ts +20 -0
  29. package/src/mcp/http-egress.ts +21 -2
  30. package/src/mcp/tools/collection-scope.ts +25 -0
  31. package/src/mcp/tools/index-cmd.ts +66 -48
  32. package/src/mcp/tools/index.ts +74 -10
  33. package/src/mcp/tools/job-status.ts +7 -0
  34. package/src/mcp/tools/query.ts +28 -7
  35. package/src/mcp/tools/search.ts +4 -0
  36. package/src/mcp/tools/sync.ts +68 -60
  37. package/src/mcp/tools/vsearch.ts +9 -3
  38. package/src/pipeline/filters.ts +18 -3
  39. package/src/pipeline/graph-retrieval.ts +1 -0
  40. package/src/pipeline/hybrid.ts +15 -2
  41. package/src/pipeline/hydration.ts +7 -2
  42. package/src/pipeline/owner-fusion.ts +1 -0
  43. package/src/pipeline/search.ts +2 -0
  44. package/src/pipeline/types.ts +7 -0
  45. package/src/pipeline/vsearch.ts +10 -5
  46. package/src/sdk/client.ts +39 -11
  47. package/src/sdk/convert.ts +27 -0
  48. package/src/sdk/index.ts +2 -0
  49. package/src/sdk/types.ts +1 -1
  50. package/src/serve/jobs.ts +4 -1
  51. package/src/serve/public/components/IndexingProgress.tsx +25 -3
  52. package/src/serve/routes/api.ts +164 -24
  53. package/src/store/sqlite/adapter.ts +97 -27
  54. package/src/store/sqlite/eligibility.ts +4 -0
  55. package/src/store/types.ts +7 -0
  56. package/src/store/vector/sqlite-vec.ts +67 -15
  57. package/src/store/vector/variant-search.ts +41 -4
  58. package/browser-extension/artifacts/gno-browser-clipper-v2.12.1.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
- this.db?.exec("ROLLBACK");
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
- this.capShutdownBusyWait(this.db);
917
- return this.db;
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 createStoredTrace(this.ensureOpen(), input);
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 mergeStoredTraceEgressLineage(this.ensureOpen(), traceId, lineage);
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 finalizeStoredTrace(this.ensureOpen(), traceId, status, updatedAtMs);
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 appendStoredTraceRun(this.ensureOpen(), input);
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 appendStoredTraceEvent(this.ensureOpen(), input);
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 appendStoredTraceJudgment(this.ensureOpen(), input);
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 appendStoredTraceExport(this.ensureOpen(), input);
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 appendStoredTraceExportManifest(this.ensureOpen(), input);
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 getOrCreateStoredTraceRedactionSecret(this.ensureOpen());
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 deleteStoredTrace(this.ensureOpen(), traceId);
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 purgeStoredTraces(this.ensureOpen());
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 enforceStoredTraceRetention(this.ensureOpen(), policy, nowMs);
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 appendStoredEgressAuditReceipt(this.ensureOpen(), receipt);
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 appendStoredEgressAuditReceiptWithRetention(
1627
- this.ensureOpen(),
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 deleteStoredEgressAuditReceipt(this.ensureOpen(), auditId);
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 enforceStoredEgressAuditRetention(this.ensureOpen(), policy, nowMs);
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 purgeStoredEgressAuditReceipts(this.ensureOpen());
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 = options.collection ? 899 : 900;
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) = ? || '/')"
@@ -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 = searchAvailable
165
- ? db.prepare(`DELETE FROM ${tableName} WHERE chunk_id = ?`)
166
- : null;
167
-
168
- const insertVecStmt = searchAvailable
169
- ? db.prepare(`INSERT INTO ${tableName} (chunk_id, embedding) VALUES (?, ?)`)
170
- : null;
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 = searchAvailable
173
- ? db.prepare(`
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
- : null;
228
+ : null;
180
229
 
181
- const deleteVecStmt = searchAvailable
182
- ? db.prepare(`DELETE FROM ${tableName} WHERE chunk_id LIKE ? || ':%'`)
183
- : null;
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 (!(searchAvailable && searchStmt)) {
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 { encodeEmbedding, getVectorIndexDatabase } from "./sqlite-vec";
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 async function resolveVectorSearchIdentity(
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 db = getVectorIndexDatabase(vectorIndex);
44
- if (!primary || !db) return { identity: 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
- 0b2bb11d4bf3e827e847426464808224e803bc7a84a4e065541a7d057f5cb651 gno-browser-clipper-v2.12.1.zip