@deepagents/text2sql 2.3.0 → 3.0.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 CHANGED
@@ -67,7 +67,6 @@ const adapter = new Postgres({
67
67
  });
68
68
 
69
69
  const text2sql = new Text2Sql({
70
- version: 'v1',
71
70
  model: groq('openai/gpt-oss-20b'),
72
71
  adapters: { main: adapter },
73
72
  });
@@ -194,8 +193,12 @@ sandbox was not prepared correctly.
194
193
  - `--out-dir <path>`: writes artifacts under that path (default:
195
194
  `$TEXT2SQL_OUT_DIR` or `./sql`).
196
195
 
197
- Set `TEXT2SQL_INDEX_VERSION` to manage cache invalidation across runs. Cache
198
- keys are `index-<version>-<adapter>`, so bump the version when schema changes.
196
+ The `sql` CLI caches introspected schema only when you opt in via env: set
197
+ `TEXT2SQL_INDEX_CACHE_DIR` (where cache files live) and/or
198
+ `TEXT2SQL_INDEX_VERSION` (an invalidation token — bump it when the schema
199
+ changes). With neither set, every `sql index` introspects fresh. See
200
+ [Schema index caching & coordination](#schema-index-caching--coordination) for
201
+ the underlying injectable primitives.
199
202
 
200
203
  For in-process or virtual-sandbox usage (without installing the package CLI),
201
204
  wrap an existing `Text2Sql` instance as a just-bash custom command:
@@ -328,6 +331,69 @@ for await (const chunk of followUp) {
328
331
  }
329
332
  ```
330
333
 
334
+ ## Schema index caching & coordination
335
+
336
+ Schema introspection is the expensive part of indexing. `Text2Sql` and
337
+ `AdapterIndexer` own no storage and no locking — you inject both as optional
338
+ primitives, keyed per adapter:
339
+
340
+ ```typescript
341
+ export interface IndexCache {
342
+ read(key: string): Promise<ContextFragment[] | null>;
343
+ write(key: string, fragments: ContextFragment[]): Promise<void>;
344
+ }
345
+
346
+ export interface IndexLock {
347
+ // Acquire (waiting if held elsewhere), run fn, release — even on throw.
348
+ // Reject if the lock can't be acquired (callers fail closed).
349
+ run<T>(key: string, fn: () => Promise<T>): Promise<T>;
350
+ }
351
+ ```
352
+
353
+ - **No `cache`** → every `index()` introspects fresh (no cache events).
354
+ - **`cache` only** → warm reads skip introspection within whatever the cache
355
+ spans.
356
+ - **`cache` + `lock`** → introspection is single-flight: concurrent callers for
357
+ the same adapter wait on the lock, then read the warm cache.
358
+
359
+ A file-backed `IndexCache` ships as a composable primitive:
360
+
361
+ ```typescript
362
+ import { FileIndexCache, Text2Sql } from '@deepagents/text2sql';
363
+
364
+ const text2sql = new Text2Sql({
365
+ model,
366
+ adapters: { main: adapter },
367
+ // Point `dir` at a shared volume to share one cache across processes.
368
+ cache: new FileIndexCache({ dir: '/var/cache/text2sql', namespace: 'v1' }),
369
+ });
370
+ ```
371
+
372
+ `FileIndexCache` writes atomically (temp + rename) and treats an unparseable
373
+ file as a miss, so a torn read self-heals into a re-introspect.
374
+
375
+ ### Horizontally-scaled deployments
376
+
377
+ Running many processes/containers against one database (e.g. a fleet of
378
+ daemons)? Two coordination tiers:
379
+
380
+ - The **lock alone** serializes introspection — never two concurrent
381
+ introspections of the same database (caps peak DB load). Back it with a
382
+ distributed lock you already operate (a Redis lock, a Postgres
383
+ `pg_advisory_lock`, etc.).
384
+ - The **lock plus a shared cache directory** gives fleet-wide single-flight:
385
+ the lock holder writes the cache on the shared volume, and every waiter reads
386
+ it instead of re-introspecting.
387
+
388
+ > On object-storage-backed volumes (GCS/S3 FUSE) `rename` is not atomic and
389
+ > file locks are unreliable — that is exactly why the lock and cache are
390
+ > injectable. Use a real distributed lock; the `FileIndexCache` parse-as-miss
391
+ > behavior plus the post-lock recheck tolerate a non-atomic write.
392
+
393
+ See `demo/text2sql-daemon` for a daemon that injects a `FileIndexCache`
394
+ (`TEXT2SQL_INDEX_CACHE_DIR` / `TEXT2SQL_INDEX_VERSION`) and a `pg_advisory_lock`
395
+ backed `IndexLock`.
396
+
331
397
  ## Streaming Index Progress
332
398
 
333
399
  `AdapterIndexer#index({ onProgress })` emits progress events while it warms or reads
@@ -343,7 +409,7 @@ import {
343
409
  type Text2SqlIndexProgressEvent,
344
410
  } from '@deepagents/text2sql';
345
411
 
346
- const indexer = new AdapterIndexer({ adapters, version: 'v1' });
412
+ const indexer = new AdapterIndexer({ adapters });
347
413
 
348
414
  await context.continue(user('Show me top 10 customers'));
349
415
 
package/dist/bin/sql.js CHANGED
@@ -3,6 +3,71 @@
3
3
  // packages/text2sql/src/bin/sql.ts
4
4
  import { cac } from "cac";
5
5
 
6
+ // packages/text2sql/src/lib/file-cache.ts
7
+ import { createHash, randomUUID } from "node:crypto";
8
+ import { existsSync } from "node:fs";
9
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
10
+ import { tmpdir } from "node:os";
11
+ import path from "node:path";
12
+ var FileCache = class {
13
+ path;
14
+ constructor(watermark, extension = ".txt", baseDir = tmpdir()) {
15
+ const hash = createHash("md5").update(watermark).digest("hex");
16
+ this.path = path.join(baseDir, `text2sql-${hash}${extension}`);
17
+ }
18
+ async get() {
19
+ if (existsSync(this.path)) {
20
+ return readFile(this.path, "utf-8");
21
+ }
22
+ return null;
23
+ }
24
+ async set(content) {
25
+ await mkdir(path.dirname(this.path), { recursive: true });
26
+ const tempPath = `${this.path}.${randomUUID()}.tmp`;
27
+ await writeFile(tempPath, content, "utf-8");
28
+ await rename(tempPath, this.path);
29
+ }
30
+ };
31
+ var JsonCache = class extends FileCache {
32
+ constructor(watermark, baseDir) {
33
+ super(watermark, ".json", baseDir);
34
+ }
35
+ async read() {
36
+ const content = await this.get();
37
+ if (!content) {
38
+ return null;
39
+ }
40
+ try {
41
+ return JSON.parse(content);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+ write(data) {
47
+ return this.set(JSON.stringify(data));
48
+ }
49
+ };
50
+
51
+ // packages/text2sql/src/lib/index-cache.ts
52
+ var FileIndexCache = class {
53
+ #dir;
54
+ #namespace;
55
+ constructor(options = {}) {
56
+ this.#dir = options.dir;
57
+ this.#namespace = options.namespace;
58
+ }
59
+ read(key) {
60
+ return this.#cacheFor(key).read();
61
+ }
62
+ write(key, fragments) {
63
+ return this.#cacheFor(key).write(fragments);
64
+ }
65
+ #cacheFor(key) {
66
+ const watermark = this.#namespace ? `${this.#namespace}-${key}` : key;
67
+ return new JsonCache(watermark, this.#dir);
68
+ }
69
+ };
70
+
6
71
  // packages/text2sql/src/lib/sql.ts
7
72
  import "@deepagents/agent";
8
73
 
@@ -120,44 +185,6 @@ function buildScopeParseErrorPayload(sql, dialect, error) {
120
185
  };
121
186
  }
122
187
 
123
- // packages/text2sql/src/lib/file-cache.ts
124
- import { createHash } from "node:crypto";
125
- import { existsSync } from "node:fs";
126
- import { readFile, writeFile } from "node:fs/promises";
127
- import { tmpdir } from "node:os";
128
- import path from "node:path";
129
- var FileCache = class {
130
- path;
131
- constructor(watermark, extension = ".txt") {
132
- const hash = createHash("md5").update(watermark).digest("hex");
133
- this.path = path.join(tmpdir(), `text2sql-${hash}${extension}`);
134
- }
135
- async get() {
136
- if (existsSync(this.path)) {
137
- return readFile(this.path, "utf-8");
138
- }
139
- return null;
140
- }
141
- set(content) {
142
- return writeFile(this.path, content, "utf-8");
143
- }
144
- };
145
- var JsonCache = class extends FileCache {
146
- constructor(watermark) {
147
- super(watermark, ".json");
148
- }
149
- async read() {
150
- const content = await this.get();
151
- if (content) {
152
- return JSON.parse(content);
153
- }
154
- return null;
155
- }
156
- write(data) {
157
- return this.set(JSON.stringify(data));
158
- }
159
- };
160
-
161
188
  // packages/text2sql/src/lib/adapter-index.ts
162
189
  var noopProgressHandler = () => {
163
190
  };
@@ -172,8 +199,8 @@ function timestampProgressHandler(onProgress) {
172
199
  }
173
200
  var AdapterIndexer = class {
174
201
  #adapters;
175
- #version;
176
- #cacheKey;
202
+ #cache;
203
+ #lock;
177
204
  constructor(options) {
178
205
  const adapterNames = Object.keys(options.adapters);
179
206
  if (adapterNames.length === 0) {
@@ -181,8 +208,8 @@ var AdapterIndexer = class {
181
208
  }
182
209
  validateAdapterNames(adapterNames);
183
210
  this.#adapters = options.adapters;
184
- this.#version = options.version;
185
- this.#cacheKey = options.cacheKey;
211
+ this.#cache = options.cache;
212
+ this.#lock = options.lock;
186
213
  }
187
214
  async index(options = {}) {
188
215
  const progress = timestampProgressHandler(options.onProgress);
@@ -239,17 +266,42 @@ var AdapterIndexer = class {
239
266
  adapter: name,
240
267
  message: `Indexing adapter "${name}"...`
241
268
  });
242
- const cacheKey = this.#adapterCacheKey(name);
243
- const cache = cacheKey ? new JsonCache(cacheKey) : void 0;
269
+ const cache = this.#cache;
270
+ const key = name;
271
+ const emitCacheHit = () => progress({
272
+ type: "adapter:cache-hit",
273
+ adapter: name,
274
+ message: `Using cached index for adapter "${name}".`,
275
+ cached: true
276
+ });
277
+ const emitCacheMiss = () => progress({
278
+ type: "adapter:cache-miss",
279
+ adapter: name,
280
+ message: `No cached index for adapter "${name}".`,
281
+ cached: false
282
+ });
283
+ const introspect = async () => {
284
+ const ctx = createGroundingContext({
285
+ onProgress: (event) => progress(adapterProgressEvent(name, event))
286
+ });
287
+ const fragments = await adapter.introspect(ctx);
288
+ await cache?.write(key, fragments);
289
+ return fragments;
290
+ };
291
+ let servedFromCache = false;
292
+ const populate = async () => {
293
+ const recheck = await cache?.read(key);
294
+ if (recheck) {
295
+ servedFromCache = true;
296
+ return recheck;
297
+ }
298
+ if (cache) emitCacheMiss();
299
+ return introspect();
300
+ };
244
301
  try {
245
- const cached = await cache?.read();
302
+ const cached = await cache?.read(key);
246
303
  if (cached) {
247
- progress({
248
- type: "adapter:cache-hit",
249
- adapter: name,
250
- message: `Using cached index for adapter "${name}".`,
251
- cached: true
252
- });
304
+ emitCacheHit();
253
305
  progress({
254
306
  type: "adapter:end",
255
307
  adapter: name,
@@ -258,24 +310,13 @@ var AdapterIndexer = class {
258
310
  });
259
311
  return cached;
260
312
  }
261
- if (cache) {
262
- progress({
263
- type: "adapter:cache-miss",
264
- adapter: name,
265
- message: `No cached index for adapter "${name}".`,
266
- cached: false
267
- });
268
- }
269
- const ctx = createGroundingContext({
270
- onProgress: (event) => progress(adapterProgressEvent(name, event))
271
- });
272
- const fragments = await adapter.introspect(ctx);
273
- await cache?.write(fragments);
313
+ const fragments = this.#lock ? await this.#lock.run(key, populate) : await populate();
314
+ if (servedFromCache) emitCacheHit();
274
315
  progress({
275
316
  type: "adapter:end",
276
317
  adapter: name,
277
318
  message: `Finished indexing adapter "${name}".`,
278
- cached: false
319
+ cached: servedFromCache
279
320
  });
280
321
  return fragments;
281
322
  } catch (error) {
@@ -302,16 +343,6 @@ var AdapterIndexer = class {
302
343
  return [name, adapter];
303
344
  });
304
345
  }
305
- #adapterCacheKey(name) {
306
- const configured = this.#cacheKey?.(name);
307
- if (configured !== void 0) {
308
- return configured;
309
- }
310
- if (this.#version) {
311
- return `index-${this.#version}-${name}`;
312
- }
313
- return void 0;
314
- }
315
346
  };
316
347
  function adapterProgressEvent(adapter, progress) {
317
348
  return {
@@ -824,7 +855,8 @@ var Text2Sql = class {
824
855
  this.#config = config;
825
856
  this.#indexer = new AdapterIndexer({
826
857
  adapters: config.adapters,
827
- version: config.version
858
+ cache: config.cache,
859
+ lock: config.lock
828
860
  });
829
861
  }
830
862
  #requireAdapter(name) {
@@ -959,7 +991,7 @@ var SqlQueryCommand = class extends SqlCommand {
959
991
  // packages/text2sql/src/bin/commands/index.ts
960
992
  import { constants, createWriteStream } from "node:fs";
961
993
  import {
962
- mkdir,
994
+ mkdir as mkdir2,
963
995
  open,
964
996
  stat,
965
997
  writeFile as writeFile2
@@ -1012,9 +1044,9 @@ var IndexCommand = class extends SqlCommand {
1012
1044
  const explicitEventsPath = resolveExplicitEventsPath(ctx);
1013
1045
  const eventsPath = explicitEventsPath ?? path3.join(outputDir, `index-${id}.events.ndjson`);
1014
1046
  try {
1015
- await mkdir(outputDir, { recursive: true });
1047
+ await mkdir2(outputDir, { recursive: true });
1016
1048
  if (explicitEventsPath) {
1017
- await mkdir(path3.dirname(explicitEventsPath), { recursive: true });
1049
+ await mkdir2(path3.dirname(explicitEventsPath), { recursive: true });
1018
1050
  }
1019
1051
  const eventsStream = await openEventsWriter(
1020
1052
  eventsPath,
@@ -1145,7 +1177,7 @@ function dedupe(values) {
1145
1177
  }
1146
1178
 
1147
1179
  // packages/text2sql/src/bin/commands/run.ts
1148
- import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
1180
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
1149
1181
  import * as path4 from "node:path";
1150
1182
  import { v7 as v72 } from "uuid";
1151
1183
  var RunCommand = class extends SqlQueryCommand {
@@ -1159,7 +1191,7 @@ var RunCommand = class extends SqlQueryCommand {
1159
1191
  const { rows, columns } = await ctx.text2Sql.run(name, sql);
1160
1192
  const outPath = path4.join(outputDir, `${v72()}.json`);
1161
1193
  try {
1162
- await mkdir2(outputDir, { recursive: true });
1194
+ await mkdir3(outputDir, { recursive: true });
1163
1195
  await writeFile3(outPath, JSON.stringify(rows, null, 2));
1164
1196
  } catch (error) {
1165
1197
  this.fail(errorMessage2(error));
@@ -1296,12 +1328,20 @@ function registerCommand(cli2, command) {
1296
1328
  return runCommand(command, positional, options);
1297
1329
  });
1298
1330
  }
1331
+ function resolveIndexCache() {
1332
+ const dir = process.env.TEXT2SQL_INDEX_CACHE_DIR;
1333
+ const namespace = process.env.TEXT2SQL_INDEX_VERSION;
1334
+ if (!dir && !namespace) {
1335
+ return void 0;
1336
+ }
1337
+ return new FileIndexCache({ dir, namespace });
1338
+ }
1299
1339
  async function runCommand(command, positional, options) {
1300
1340
  let ctx;
1301
1341
  try {
1302
1342
  const text2Sql = new Text2Sql({
1303
1343
  adapters: await loadAdapters(),
1304
- version: process.env.TEXT2SQL_INDEX_VERSION
1344
+ cache: resolveIndexCache()
1305
1345
  });
1306
1346
  ctx = {
1307
1347
  text2Sql,
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export * from './lib/agents/suggestions.agents.ts';
6
6
  export * from './lib/adapter-index.ts';
7
7
  export * from './lib/checkpoint.ts';
8
8
  export * from './lib/file-cache.ts';
9
+ export * from './lib/index-cache.ts';
10
+ export * from './lib/index-lock.ts';
9
11
  export * from './lib/fragments/schema.ts';
10
12
  export * from './lib/fs/index.ts';
11
13
  export * from './lib/instructions.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oCAAoC,CAAC;AACnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1301,16 +1301,16 @@ var suggestionsAgent = agent({
1301
1301
  import { fragment as fragment2 } from "@deepagents/context";
1302
1302
 
1303
1303
  // packages/text2sql/src/lib/file-cache.ts
1304
- import { createHash } from "node:crypto";
1304
+ import { createHash, randomUUID } from "node:crypto";
1305
1305
  import { existsSync } from "node:fs";
1306
- import { readFile, writeFile } from "node:fs/promises";
1306
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1307
1307
  import { tmpdir } from "node:os";
1308
1308
  import path from "node:path";
1309
1309
  var FileCache = class {
1310
1310
  path;
1311
- constructor(watermark, extension = ".txt") {
1311
+ constructor(watermark, extension = ".txt", baseDir = tmpdir()) {
1312
1312
  const hash2 = createHash("md5").update(watermark).digest("hex");
1313
- this.path = path.join(tmpdir(), `text2sql-${hash2}${extension}`);
1313
+ this.path = path.join(baseDir, `text2sql-${hash2}${extension}`);
1314
1314
  }
1315
1315
  async get() {
1316
1316
  if (existsSync(this.path)) {
@@ -1318,26 +1318,53 @@ var FileCache = class {
1318
1318
  }
1319
1319
  return null;
1320
1320
  }
1321
- set(content) {
1322
- return writeFile(this.path, content, "utf-8");
1321
+ async set(content) {
1322
+ await mkdir(path.dirname(this.path), { recursive: true });
1323
+ const tempPath = `${this.path}.${randomUUID()}.tmp`;
1324
+ await writeFile(tempPath, content, "utf-8");
1325
+ await rename(tempPath, this.path);
1323
1326
  }
1324
1327
  };
1325
1328
  var JsonCache = class extends FileCache {
1326
- constructor(watermark) {
1327
- super(watermark, ".json");
1329
+ constructor(watermark, baseDir) {
1330
+ super(watermark, ".json", baseDir);
1328
1331
  }
1329
1332
  async read() {
1330
1333
  const content = await this.get();
1331
- if (content) {
1334
+ if (!content) {
1335
+ return null;
1336
+ }
1337
+ try {
1332
1338
  return JSON.parse(content);
1339
+ } catch {
1340
+ return null;
1333
1341
  }
1334
- return null;
1335
1342
  }
1336
1343
  write(data) {
1337
1344
  return this.set(JSON.stringify(data));
1338
1345
  }
1339
1346
  };
1340
1347
 
1348
+ // packages/text2sql/src/lib/index-cache.ts
1349
+ var FileIndexCache = class {
1350
+ #dir;
1351
+ #namespace;
1352
+ constructor(options = {}) {
1353
+ this.#dir = options.dir;
1354
+ this.#namespace = options.namespace;
1355
+ }
1356
+ read(key) {
1357
+ return this.#cacheFor(key).read();
1358
+ }
1359
+ write(key, fragments) {
1360
+ return this.#cacheFor(key).write(fragments);
1361
+ }
1362
+ #cacheFor(key) {
1363
+ const watermark = this.#namespace ? `${this.#namespace}-${key}` : key;
1364
+ return new JsonCache(watermark, this.#dir);
1365
+ }
1366
+ };
1367
+
1341
1368
  // packages/text2sql/src/lib/adapter-index.ts
1342
1369
  var TEXT2SQL_INDEX_PROGRESS_CHUNK = "data-text2sql-index-progress";
1343
1370
  var noopProgressHandler = () => {
@@ -1353,8 +1380,8 @@ function timestampProgressHandler(onProgress) {
1353
1380
  }
1354
1381
  var AdapterIndexer = class {
1355
1382
  #adapters;
1356
- #version;
1357
- #cacheKey;
1383
+ #cache;
1384
+ #lock;
1358
1385
  constructor(options) {
1359
1386
  const adapterNames = Object.keys(options.adapters);
1360
1387
  if (adapterNames.length === 0) {
@@ -1362,8 +1389,8 @@ var AdapterIndexer = class {
1362
1389
  }
1363
1390
  validateAdapterNames(adapterNames);
1364
1391
  this.#adapters = options.adapters;
1365
- this.#version = options.version;
1366
- this.#cacheKey = options.cacheKey;
1392
+ this.#cache = options.cache;
1393
+ this.#lock = options.lock;
1367
1394
  }
1368
1395
  async index(options = {}) {
1369
1396
  const progress = timestampProgressHandler(options.onProgress);
@@ -1420,17 +1447,42 @@ var AdapterIndexer = class {
1420
1447
  adapter: name,
1421
1448
  message: `Indexing adapter "${name}"...`
1422
1449
  });
1423
- const cacheKey = this.#adapterCacheKey(name);
1424
- const cache = cacheKey ? new JsonCache(cacheKey) : void 0;
1450
+ const cache = this.#cache;
1451
+ const key = name;
1452
+ const emitCacheHit = () => progress({
1453
+ type: "adapter:cache-hit",
1454
+ adapter: name,
1455
+ message: `Using cached index for adapter "${name}".`,
1456
+ cached: true
1457
+ });
1458
+ const emitCacheMiss = () => progress({
1459
+ type: "adapter:cache-miss",
1460
+ adapter: name,
1461
+ message: `No cached index for adapter "${name}".`,
1462
+ cached: false
1463
+ });
1464
+ const introspect = async () => {
1465
+ const ctx = createGroundingContext({
1466
+ onProgress: (event) => progress(adapterProgressEvent(name, event))
1467
+ });
1468
+ const fragments = await adapter.introspect(ctx);
1469
+ await cache?.write(key, fragments);
1470
+ return fragments;
1471
+ };
1472
+ let servedFromCache = false;
1473
+ const populate = async () => {
1474
+ const recheck = await cache?.read(key);
1475
+ if (recheck) {
1476
+ servedFromCache = true;
1477
+ return recheck;
1478
+ }
1479
+ if (cache) emitCacheMiss();
1480
+ return introspect();
1481
+ };
1425
1482
  try {
1426
- const cached = await cache?.read();
1483
+ const cached = await cache?.read(key);
1427
1484
  if (cached) {
1428
- progress({
1429
- type: "adapter:cache-hit",
1430
- adapter: name,
1431
- message: `Using cached index for adapter "${name}".`,
1432
- cached: true
1433
- });
1485
+ emitCacheHit();
1434
1486
  progress({
1435
1487
  type: "adapter:end",
1436
1488
  adapter: name,
@@ -1439,24 +1491,13 @@ var AdapterIndexer = class {
1439
1491
  });
1440
1492
  return cached;
1441
1493
  }
1442
- if (cache) {
1443
- progress({
1444
- type: "adapter:cache-miss",
1445
- adapter: name,
1446
- message: `No cached index for adapter "${name}".`,
1447
- cached: false
1448
- });
1449
- }
1450
- const ctx = createGroundingContext({
1451
- onProgress: (event) => progress(adapterProgressEvent(name, event))
1452
- });
1453
- const fragments = await adapter.introspect(ctx);
1454
- await cache?.write(fragments);
1494
+ const fragments = this.#lock ? await this.#lock.run(key, populate) : await populate();
1495
+ if (servedFromCache) emitCacheHit();
1455
1496
  progress({
1456
1497
  type: "adapter:end",
1457
1498
  adapter: name,
1458
1499
  message: `Finished indexing adapter "${name}".`,
1459
- cached: false
1500
+ cached: servedFromCache
1460
1501
  });
1461
1502
  return fragments;
1462
1503
  } catch (error) {
@@ -1483,16 +1524,6 @@ var AdapterIndexer = class {
1483
1524
  return [name, adapter];
1484
1525
  });
1485
1526
  }
1486
- #adapterCacheKey(name) {
1487
- const configured = this.#cacheKey?.(name);
1488
- if (configured !== void 0) {
1489
- return configured;
1490
- }
1491
- if (this.#version) {
1492
- return `index-${this.#version}-${name}`;
1493
- }
1494
- return void 0;
1495
- }
1496
1527
  };
1497
1528
  function adapterProgressEvent(adapter, progress) {
1498
1529
  return {
@@ -5580,7 +5611,8 @@ var Text2Sql = class {
5580
5611
  this.#config = config;
5581
5612
  this.#indexer = new AdapterIndexer({
5582
5613
  adapters: config.adapters,
5583
- version: config.version
5614
+ cache: config.cache,
5615
+ lock: config.lock
5584
5616
  });
5585
5617
  }
5586
5618
  #requireAdapter(name) {
@@ -5790,6 +5822,7 @@ export {
5790
5822
  AdapterIndexer,
5791
5823
  Checkpoint,
5792
5824
  FileCache,
5825
+ FileIndexCache,
5793
5826
  JsonCache,
5794
5827
  MssqlFs,
5795
5828
  Point,