@ls-stack/agent-eval 0.55.1 → 0.55.2

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.
@@ -275,12 +275,15 @@ const traceCacheRefSchema = z.object({
275
275
  z.object({
276
276
  key: z.string(),
277
277
  namespace: z.string(),
278
- operationType: cacheOperationTypeSchema,
279
- operationName: z.string(),
280
- spanName: z.string().optional(),
281
- spanKind: traceSpanKindSchema.optional(),
282
278
  storedAt: z.string(),
283
- sizeBytes: z.number()
279
+ lastAccessedAt: z.string()
280
+ });
281
+ z.object({
282
+ removedCacheFiles: z.number(),
283
+ removedDebugFiles: z.number(),
284
+ removedBlobFiles: z.number(),
285
+ removedIndexRows: z.number(),
286
+ rewrittenIndexes: z.number()
284
287
  });
285
288
  /** Zod schema for `SerializedCacheSpan`, defined lazily for recursion. */
286
289
  const serializedCacheSpanSchema = z.object({
@@ -1377,6 +1380,7 @@ const agentEvalsConfigSchema = z.object({
1377
1380
  dir: z.string().optional(),
1378
1381
  maxEntriesPerNamespace: z.preprocess((value) => typeof value === "number" && Number.isFinite(value) ? value : void 0, z.number().optional()),
1379
1382
  maxEntriesByNamespace: z.record(z.string(), z.number()).optional(),
1383
+ pruneIdleDelayMs: z.preprocess((value) => typeof value === "number" && Number.isFinite(value) ? value : void 0, z.number().optional()),
1380
1384
  maxEntriesPerEval: z.preprocess((value) => typeof value === "number" && Number.isFinite(value) ? value : void 0, z.number().optional())
1381
1385
  }).optional()
1382
1386
  });
@@ -4926,8 +4930,10 @@ const cacheSerializationMarker = "__aecs";
4926
4930
  const supportedCacheSerializationPrefix = "v1:";
4927
4931
  const externalJsonCacheSerializationMarker = "v1:ExternalJson";
4928
4932
  const externalJsonBlobExtension = ".json.br";
4933
+ const externalJsonBlobDirName = "cache-blobs";
4929
4934
  const cacheEntryExtension = ".json.br";
4930
4935
  const debugEntryExtension = ".json";
4936
+ const cacheIndexFilePrefix = ".index-";
4931
4937
  async function commitPendingCacheWrites(params) {
4932
4938
  for (const pendingWrite of params.pendingWrites) await params.backingStore.write(pendingWrite.entry, pendingWrite.debugKey);
4933
4939
  }
@@ -4941,8 +4947,14 @@ async function commitPendingCacheWrites(params) {
4941
4947
  function createFsCacheStore(options) {
4942
4948
  const cacheDir = resolve(options.workspaceRoot, options.dir ?? ".agent-evals/cache");
4943
4949
  const debugDir = resolve(options.workspaceRoot, options.debugDir ?? ".agent-evals/cache-debug");
4944
- const blobDir = resolve(options.workspaceRoot, options.blobDir ?? ".agent-evals/cache-blobs");
4945
- const externalJsonStore = createExternalJsonBlobStore(blobDir);
4950
+ const blobDir = options.blobDir === void 0 ? join(cacheDir, externalJsonBlobDirName) : resolve(options.workspaceRoot, options.blobDir);
4951
+ const legacyBlobDir = resolve(options.workspaceRoot, ".agent-evals/cache-blobs");
4952
+ const fallbackBlobDirs = options.blobDir === void 0 && legacyBlobDir !== blobDir ? [legacyBlobDir] : [];
4953
+ const blobDirs = [blobDir, ...fallbackBlobDirs];
4954
+ const externalJsonStore = createExternalJsonBlobStore({
4955
+ fallbackDirs: fallbackBlobDirs,
4956
+ primaryDir: blobDir
4957
+ });
4946
4958
  const defaultMaxEntries = normalizeMaxEntries(options.maxEntriesPerNamespace);
4947
4959
  return {
4948
4960
  externalJsonStore,
@@ -4956,11 +4968,22 @@ function createFsCacheStore(options) {
4956
4968
  return blobDir;
4957
4969
  },
4958
4970
  async lookup(namespace, keyHash) {
4959
- const entry = await readCacheEntry(cacheDir, namespace, keyHash);
4960
- return entry === null ? null : await materializeExternalJsonCacheEntryOrNull(entry, externalJsonStore);
4971
+ const entry = await readIndexedCacheEntry({
4972
+ cacheDir,
4973
+ key: keyHash,
4974
+ namespace
4975
+ });
4976
+ if (entry === null) return null;
4977
+ const materialized = await materializeExternalJsonCacheEntryOrNull(entry, externalJsonStore);
4978
+ if (materialized !== null) await updateCacheIndexLastAccessedAt(cacheDir, namespace, keyHash);
4979
+ return materialized;
4961
4980
  },
4962
4981
  async lookupWithDebug(namespace, keyHash) {
4963
- const rawEntry = await readCacheEntry(cacheDir, namespace, keyHash);
4982
+ const rawEntry = await readIndexedCacheEntry({
4983
+ cacheDir,
4984
+ key: keyHash,
4985
+ namespace
4986
+ });
4964
4987
  if (rawEntry === null) return null;
4965
4988
  const entry = await materializeExternalJsonCacheEntryOrNull(rawEntry, externalJsonStore);
4966
4989
  if (entry === null) return null;
@@ -4975,8 +4998,17 @@ function createFsCacheStore(options) {
4975
4998
  };
4976
4999
  },
4977
5000
  async write(entry, debugKey) {
4978
- const maxEntries = maxEntriesForNamespace(entry.namespace, defaultMaxEntries, options.maxEntriesByNamespace);
4979
- await withCacheFileLock(namespaceLockPath(cacheDir, entry.namespace), () => writeCompressedCacheEntry(cacheDir, entry));
5001
+ await withCacheFileLock(namespaceLockPath(cacheDir, entry.namespace), async () => {
5002
+ await writeCompressedCacheEntry(cacheDir, entry);
5003
+ if (!usesSupportedCacheSerialization(entry.recording)) return;
5004
+ const index = await readNamespaceIndex(cacheDir, entry.namespace);
5005
+ index.entries[entry.key] = {
5006
+ storedAt: entry.storedAt,
5007
+ lastAccessedAt: entry.storedAt,
5008
+ blobRefs: await collectExternalJsonBlobRefs(entry, blobDirs)
5009
+ };
5010
+ await writeNamespaceIndex(cacheDir, index);
5011
+ });
4980
5012
  if (debugKey !== void 0) {
4981
5013
  if ((await resultify(() => writeDebugKeyEntry({
4982
5014
  debugDir,
@@ -4987,36 +5019,11 @@ function createFsCacheStore(options) {
4987
5019
  key: entry.key
4988
5020
  }));
4989
5021
  }
4990
- await pruneEntriesForNamespace({
4991
- cacheDir,
4992
- debugDir,
4993
- namespace: entry.namespace,
4994
- maxEntries,
4995
- protectedKey: entry.key
4996
- });
4997
- await pruneExternalJsonBlobs(cacheDir, blobDir);
4998
5022
  },
4999
5023
  async list() {
5000
- const files = await listCacheEntryFiles(cacheDir);
5001
5024
  const items = [];
5002
- for (const filePath of files) {
5003
- const fileEntry = await readCacheEntryFilePath(filePath);
5004
- if (fileEntry === null || !entryMatchesPath(filePath, fileEntry.entry)) continue;
5005
- const entry = fileEntry.entry;
5006
- const operationType = entry.operationType ?? "span";
5007
- const operationName = entry.operationName ?? entry.spanName ?? entry.namespace;
5008
- items.push({
5009
- key: entry.key,
5010
- namespace: entry.namespace,
5011
- operationType,
5012
- operationName,
5013
- spanName: entry.spanName,
5014
- spanKind: entry.spanKind,
5015
- storedAt: entry.storedAt,
5016
- sizeBytes: fileEntry.sizeBytes
5017
- });
5018
- }
5019
- items.sort((a, b) => a.storedAt < b.storedAt ? 1 : -1);
5025
+ for (const index of await listCacheIndexes(cacheDir)) for (const [key, entry] of Object.entries(index.entries)) items.push(toCacheListItem(index.namespace, key, entry));
5026
+ items.sort((a, b) => a.lastAccessedAt < b.lastAccessedAt ? 1 : -1);
5020
5027
  return items;
5021
5028
  },
5022
5029
  async clear(filter) {
@@ -5029,21 +5036,46 @@ function createFsCacheStore(options) {
5029
5036
  recursive: true,
5030
5037
  force: true
5031
5038
  });
5032
- await rm(blobDir, {
5039
+ await Promise.all(blobDirs.map((dir) => rm(dir, {
5033
5040
  recursive: true,
5034
5041
  force: true
5035
- });
5042
+ })));
5036
5043
  return;
5037
5044
  }
5038
5045
  if (filter.namespace !== void 0) {
5039
5046
  await clearCacheEntries(cacheDir, filter);
5040
5047
  await clearDebugEntries(debugDir, filter);
5041
- await pruneExternalJsonBlobs(cacheDir, blobDir);
5048
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5042
5049
  return;
5043
5050
  }
5044
5051
  await clearCacheEntries(cacheDir, filter);
5045
5052
  await clearDebugEntries(debugDir, filter);
5046
- await pruneExternalJsonBlobs(cacheDir, blobDir);
5053
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5054
+ },
5055
+ async pruneExternalJsonBlobs() {
5056
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5057
+ },
5058
+ async pruneRetention() {
5059
+ for (const index_ of await listCacheIndexes(cacheDir)) {
5060
+ const namespace = index_.namespace;
5061
+ const maxEntries = maxEntriesForNamespace(namespace, defaultMaxEntries, options.maxEntriesByNamespace);
5062
+ const keptKeys = await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5063
+ return pruneCacheEntriesForNamespace({
5064
+ cacheDir,
5065
+ index: await readNamespaceIndex(cacheDir, namespace),
5066
+ maxEntries
5067
+ });
5068
+ });
5069
+ await withCacheFileLock(namespaceLockPath(debugDir, namespace), () => pruneDebugEntriesForNamespace(debugDir, namespace, keptKeys));
5070
+ }
5071
+ await pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs);
5072
+ },
5073
+ async repair() {
5074
+ return repairIndexedCache({
5075
+ blobDirs,
5076
+ cacheDir,
5077
+ debugDir
5078
+ });
5047
5079
  }
5048
5080
  };
5049
5081
  }
@@ -5126,11 +5158,31 @@ function entryPath(params) {
5126
5158
  if (filePath !== namespaceDir && !filePath.startsWith(`${namespaceDir}${sep}`)) throw new Error(`Cache entry key escapes namespace directory: ${params.key}`);
5127
5159
  return filePath;
5128
5160
  }
5129
- async function readCacheEntry(cacheDir, namespace, key) {
5130
- return (await readCacheEntryFilePath(cacheEntryPath(cacheDir, namespace, key), {
5131
- namespace,
5132
- key
5133
- }))?.entry ?? null;
5161
+ function cacheIndexPath(cacheDir, namespace) {
5162
+ return join(namespaceDirPath(cacheDir, namespace), `${cacheIndexFilePrefix}${hashNamespace(namespace)}${debugEntryExtension}`);
5163
+ }
5164
+ async function readIndexedCacheEntry(params) {
5165
+ return withCacheFileLock(namespaceLockPath(params.cacheDir, params.namespace), async () => {
5166
+ if ((await readNamespaceIndex(params.cacheDir, params.namespace)).entries[params.key] === void 0) return null;
5167
+ const fileEntry = await readCacheEntryFilePath(cacheEntryPath(params.cacheDir, params.namespace, params.key), {
5168
+ namespace: params.namespace,
5169
+ key: params.key
5170
+ });
5171
+ if (fileEntry === null) return null;
5172
+ return fileEntry.entry;
5173
+ });
5174
+ }
5175
+ async function updateCacheIndexLastAccessedAt(cacheDir, namespace, key) {
5176
+ await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5177
+ const index = await readNamespaceIndex(cacheDir, namespace);
5178
+ const entry = index.entries[key];
5179
+ if (entry === void 0) return;
5180
+ index.entries[key] = {
5181
+ ...entry,
5182
+ lastAccessedAt: new Date(getRealDateNowMs()).toISOString()
5183
+ };
5184
+ await writeNamespaceIndex(cacheDir, index);
5185
+ });
5134
5186
  }
5135
5187
  async function readCacheEntryFilePath(filePath, expected) {
5136
5188
  if (!existsSync(filePath)) return null;
@@ -5145,10 +5197,7 @@ async function readCacheEntryFilePath(filePath, expected) {
5145
5197
  const entry = parsed.data;
5146
5198
  if (!usesSupportedCacheSerialization(entry.recording)) return null;
5147
5199
  if (expected !== void 0 && (entry.namespace !== expected.namespace || entry.key !== expected.key)) return null;
5148
- return {
5149
- entry,
5150
- sizeBytes: compressedResult.value.byteLength
5151
- };
5200
+ return { entry };
5152
5201
  }
5153
5202
  async function writeCompressedCacheEntry(cacheDir, entry) {
5154
5203
  const filePath = cacheEntryPath(cacheDir, entry.namespace, entry.key);
@@ -5197,23 +5246,132 @@ async function writeAtomicFile(filePath, contents) {
5197
5246
  await writeFile(tmpPath, contents);
5198
5247
  await rename(tmpPath, filePath);
5199
5248
  }
5249
+ const emptyCacheIndex = (namespace) => ({
5250
+ version: 1,
5251
+ namespace,
5252
+ entries: {}
5253
+ });
5254
+ async function readNamespaceIndex(cacheDir, namespace) {
5255
+ const indexPath = cacheIndexPath(cacheDir, namespace);
5256
+ if (!existsSync(indexPath)) return emptyCacheIndex(namespace);
5257
+ const rawResult = await resultify(() => readFile(indexPath, "utf8"));
5258
+ if (rawResult.error) return emptyCacheIndex(namespace);
5259
+ return parseCacheIndexFile(safeJsonParse(rawResult.value), namespace) ?? emptyCacheIndex(namespace);
5260
+ }
5261
+ async function writeNamespaceIndex(cacheDir, index) {
5262
+ const entries = Object.entries(index.entries);
5263
+ if (entries.length === 0) {
5264
+ await rm(cacheIndexPath(cacheDir, index.namespace), { force: true });
5265
+ await removeDirIfEmpty(namespaceDirPath(cacheDir, index.namespace));
5266
+ return;
5267
+ }
5268
+ const sortedEntries = entries.toSorted(([a], [b]) => a < b ? -1 : 1);
5269
+ const normalizedEntries = Object.fromEntries(sortedEntries.map(([key, entry]) => [key, entry]));
5270
+ await writeAtomicFile(cacheIndexPath(cacheDir, index.namespace), JSON.stringify({
5271
+ ...index,
5272
+ entries: normalizedEntries
5273
+ }, null, 2));
5274
+ }
5275
+ async function listCacheIndexes(cacheDir) {
5276
+ if (!existsSync(cacheDir)) return [];
5277
+ const entriesResult = await resultify(() => readdir(cacheDir, { withFileTypes: true }));
5278
+ if (entriesResult.error) return [];
5279
+ const records = [];
5280
+ for (const entry of entriesResult.value) {
5281
+ if (!entry.isDirectory()) continue;
5282
+ const namespaceDir = join(cacheDir, entry.name);
5283
+ for (const indexFilePath of await listCacheIndexFiles(namespaceDir)) {
5284
+ const rawResult = await resultify(() => readFile(indexFilePath, "utf8"));
5285
+ if (rawResult.error) continue;
5286
+ const parsed = parseCacheIndexFile(safeJsonParse(rawResult.value));
5287
+ if (parsed === null) continue;
5288
+ records.push(parsed);
5289
+ }
5290
+ }
5291
+ return records;
5292
+ }
5293
+ function hashNamespace(namespace) {
5294
+ return createHash("sha256").update(namespace).digest("hex");
5295
+ }
5296
+ function parseCacheIndexFile(value, expectedNamespace) {
5297
+ if (!isRecordLike(value)) return null;
5298
+ if (value.version !== 1 || typeof value.namespace !== "string") return null;
5299
+ if (expectedNamespace !== void 0 && value.namespace !== expectedNamespace) return null;
5300
+ if (!isRecordLike(value.entries)) return null;
5301
+ const entries = {};
5302
+ for (const [key, entryValue] of Object.entries(value.entries)) {
5303
+ const entry = parseCacheIndexEntry(entryValue);
5304
+ if (entry === null) return null;
5305
+ entries[key] = entry;
5306
+ }
5307
+ return {
5308
+ version: 1,
5309
+ namespace: value.namespace,
5310
+ entries
5311
+ };
5312
+ }
5313
+ function parseCacheIndexEntry(value) {
5314
+ if (!isRecordLike(value)) return null;
5315
+ if (typeof value.storedAt !== "string" || typeof value.lastAccessedAt !== "string") return null;
5316
+ if (!Array.isArray(value.blobRefs)) return null;
5317
+ const blobRefs = [];
5318
+ for (const blobRef of value.blobRefs) {
5319
+ if (typeof blobRef !== "string") return null;
5320
+ blobRefs.push(blobRef);
5321
+ }
5322
+ return {
5323
+ storedAt: value.storedAt,
5324
+ lastAccessedAt: value.lastAccessedAt,
5325
+ blobRefs
5326
+ };
5327
+ }
5328
+ function toCacheListItem(namespace, key, entry) {
5329
+ return {
5330
+ key,
5331
+ namespace,
5332
+ storedAt: entry.storedAt,
5333
+ lastAccessedAt: entry.lastAccessedAt
5334
+ };
5335
+ }
5336
+ function keyFromEntryFilePath(filePath, extension) {
5337
+ const name = basename(filePath);
5338
+ if (!name.endsWith(extension)) return null;
5339
+ return name.slice(0, -extension.length);
5340
+ }
5341
+ function debugNamespaceFromPath(debugDir, filePath) {
5342
+ return basename(dirname(resolve(debugDir, relative(debugDir, filePath))));
5343
+ }
5200
5344
  async function clearCacheEntries(cacheDir, filter) {
5201
- const files = filter.namespace === void 0 ? await listCacheEntryFiles(cacheDir) : await listCacheEntryFiles(namespaceDirPath(cacheDir, filter.namespace));
5202
- for (const filePath of files) {
5203
- const fileEntry = await readCacheEntryFilePath(filePath);
5204
- if (fileEntry === null) continue;
5205
- const entry = fileEntry.entry;
5206
- if (!entryMatchesFilter(entry, filter)) continue;
5207
- await withCacheFileLock(namespaceLockPath(cacheDir, entry.namespace), () => rm(filePath, { force: true }));
5345
+ const indexes = filter.namespace === void 0 ? await listCacheIndexes(cacheDir) : [await readNamespaceIndex(cacheDir, filter.namespace)];
5346
+ for (const record of indexes) {
5347
+ const namespace = record.namespace;
5348
+ await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5349
+ const index = await readNamespaceIndex(cacheDir, namespace);
5350
+ const matchingKeys = Object.keys(index.entries).filter((key) => {
5351
+ return index.entries[key] !== void 0 && entryMatchesFilter({
5352
+ namespace,
5353
+ key
5354
+ }, filter);
5355
+ });
5356
+ for (const key of matchingKeys) {
5357
+ await rm(cacheEntryPath(cacheDir, namespace, key), { force: true });
5358
+ delete index.entries[key];
5359
+ }
5360
+ await writeNamespaceIndex(cacheDir, index);
5361
+ });
5208
5362
  }
5209
- if (filter.namespace !== void 0) await removeDirIfEmpty(namespaceDirPath(cacheDir, filter.namespace));
5210
5363
  }
5211
5364
  async function clearDebugEntries(debugDir, filter) {
5212
- const files = filter.namespace === void 0 ? await listDebugEntryFiles(debugDir) : await listDebugEntryFiles(namespaceDirPath(debugDir, filter.namespace));
5365
+ const files = await listDebugEntryFiles(filter.namespace === void 0 ? debugDir : namespaceDirPath(debugDir, filter.namespace));
5213
5366
  for (const filePath of files) {
5214
- const entry = await readDebugEntryFilePath(filePath);
5215
- if (entry === null || !entryMatchesFilter(entry, filter)) continue;
5216
- await withCacheFileLock(namespaceLockPath(debugDir, entry.namespace), () => rm(filePath, { force: true }));
5367
+ const namespace = filter.namespace === void 0 ? debugNamespaceFromPath(debugDir, filePath) : filter.namespace;
5368
+ const key = keyFromEntryFilePath(filePath, debugEntryExtension);
5369
+ if (key === null) continue;
5370
+ if (!entryMatchesFilter({
5371
+ namespace,
5372
+ key
5373
+ }, filter)) continue;
5374
+ await withCacheFileLock(namespaceLockPath(debugDir, namespace), () => rm(filePath, { force: true }));
5217
5375
  }
5218
5376
  if (filter.namespace !== void 0) await removeDirIfEmpty(namespaceDirPath(debugDir, filter.namespace));
5219
5377
  }
@@ -5221,49 +5379,86 @@ function entryMatchesFilter(entry, filter) {
5221
5379
  if (filter.namespace !== void 0 && entry.namespace !== filter.namespace) return false;
5222
5380
  return filter.key === void 0 || entry.key === filter.key;
5223
5381
  }
5224
- async function pruneEntriesForNamespace(params) {
5225
- const { cacheDir, debugDir, namespace, maxEntries, protectedKey } = params;
5226
- await withCacheFileLock(namespaceLockPath(cacheDir, namespace), async () => {
5227
- const keptKeys = await pruneCacheEntriesForNamespace(cacheDir, namespace, maxEntries, protectedKey);
5228
- await withCacheFileLock(namespaceLockPath(debugDir, namespace), () => pruneDebugEntriesForNamespace(debugDir, namespace, keptKeys));
5229
- });
5230
- }
5231
- async function pruneCacheEntriesForNamespace(cacheDir, namespace, maxEntries, protectedKey) {
5232
- const entries = await listCacheEntriesForNamespace(cacheDir, namespace);
5233
- const sorted = entries.toSorted((a, b) => a.entry.storedAt < b.entry.storedAt ? 1 : -1);
5382
+ async function pruneCacheEntriesForNamespace(params) {
5383
+ const { cacheDir, index, maxEntries } = params;
5384
+ const entries = Object.entries(index.entries);
5385
+ const sorted = entries.toSorted(([, a], [, b]) => a.lastAccessedAt < b.lastAccessedAt ? 1 : -1);
5234
5386
  const keptKeys = /* @__PURE__ */ new Set();
5235
- const protectedEntry = entries.find((item) => item.entry.key === protectedKey);
5236
- if (protectedEntry !== void 0) keptKeys.add(protectedEntry.entry.key);
5237
- for (const item of sorted) {
5387
+ for (const [key] of sorted) {
5238
5388
  if (keptKeys.size >= maxEntries) break;
5239
- keptKeys.add(item.entry.key);
5389
+ keptKeys.add(key);
5240
5390
  }
5241
- for (const item of entries) if (!keptKeys.has(item.entry.key)) await rm(item.filePath, { force: true });
5242
- await removeDirIfEmpty(namespaceDirPath(cacheDir, namespace));
5391
+ for (const [key] of entries) if (!keptKeys.has(key)) {
5392
+ await rm(cacheEntryPath(cacheDir, index.namespace, key), { force: true });
5393
+ delete index.entries[key];
5394
+ }
5395
+ await writeNamespaceIndex(cacheDir, index);
5243
5396
  return keptKeys;
5244
5397
  }
5245
5398
  async function pruneDebugEntriesForNamespace(debugDir, namespace, keptKeys) {
5246
5399
  const files = await listDebugEntryFiles(namespaceDirPath(debugDir, namespace));
5247
5400
  for (const filePath of files) {
5248
- const entry = await readDebugEntryFilePath(filePath);
5249
- if (entry !== null && entry.namespace === namespace && !keptKeys.has(entry.key)) await rm(filePath, { force: true });
5401
+ const key = keyFromEntryFilePath(filePath, debugEntryExtension);
5402
+ if (key !== null && !keptKeys.has(key)) await rm(filePath, { force: true });
5250
5403
  }
5251
5404
  await removeDirIfEmpty(namespaceDirPath(debugDir, namespace));
5252
5405
  }
5253
- async function listCacheEntriesForNamespace(cacheDir, namespace) {
5254
- const files = await listCacheEntryFiles(namespaceDirPath(cacheDir, namespace));
5255
- const entries = [];
5256
- for (const filePath of files) {
5257
- const fileEntry = await readCacheEntryFilePath(filePath);
5258
- if (fileEntry !== null && fileEntry.entry.namespace === namespace && entryMatchesPath(filePath, fileEntry.entry)) entries.push({
5259
- filePath,
5260
- entry: fileEntry.entry
5406
+ async function repairIndexedCache(params) {
5407
+ const summary = {
5408
+ removedCacheFiles: 0,
5409
+ removedDebugFiles: 0,
5410
+ removedBlobFiles: 0,
5411
+ removedIndexRows: 0,
5412
+ rewrittenIndexes: 0
5413
+ };
5414
+ for (const index_ of await listCacheIndexes(params.cacheDir)) {
5415
+ const result = await withCacheFileLock(namespaceLockPath(params.cacheDir, index_.namespace), async () => {
5416
+ const index = await readNamespaceIndex(params.cacheDir, index_.namespace);
5417
+ let removedRows = 0;
5418
+ for (const key of Object.keys(index.entries)) if (!existsSync(cacheEntryPath(params.cacheDir, index.namespace, key))) {
5419
+ delete index.entries[key];
5420
+ removedRows++;
5421
+ }
5422
+ if (removedRows === 0) return {
5423
+ removedRows,
5424
+ rewritten: false
5425
+ };
5426
+ await writeNamespaceIndex(params.cacheDir, index);
5427
+ return {
5428
+ removedRows,
5429
+ rewritten: true
5430
+ };
5261
5431
  });
5432
+ summary.removedIndexRows += result.removedRows;
5433
+ if (result.rewritten) summary.rewrittenIndexes++;
5262
5434
  }
5263
- return entries;
5264
- }
5265
- function entryMatchesPath(filePath, entry) {
5266
- return basename(filePath) === `${entry.key}${cacheEntryExtension}` && basename(dirname(filePath)) === sanitizeSegment$1(entry.namespace);
5435
+ const indexes = await listCacheIndexes(params.cacheDir);
5436
+ const indexedCacheFiles = /* @__PURE__ */ new Set();
5437
+ const indexedDebugFiles = /* @__PURE__ */ new Set();
5438
+ const indexedBlobRefs = /* @__PURE__ */ new Set();
5439
+ for (const index_ of indexes) for (const [key, entry] of Object.entries(index_.entries)) {
5440
+ indexedCacheFiles.add(cacheEntryPath(params.cacheDir, index_.namespace, key));
5441
+ indexedDebugFiles.add(debugEntryPath(params.debugDir, index_.namespace, key));
5442
+ for (const blobRef of entry.blobRefs) indexedBlobRefs.add(blobRef);
5443
+ }
5444
+ for (const filePath of await listCacheEntryFiles(params.cacheDir, "allNamespaces")) if (!indexedCacheFiles.has(filePath)) {
5445
+ await rm(filePath, { force: true });
5446
+ summary.removedCacheFiles++;
5447
+ await removeDirIfEmpty(dirname(filePath));
5448
+ }
5449
+ for (const filePath of await listDebugEntryFiles(params.debugDir)) if (!indexedDebugFiles.has(filePath)) {
5450
+ await rm(filePath, { force: true });
5451
+ summary.removedDebugFiles++;
5452
+ await removeDirIfEmpty(dirname(filePath));
5453
+ }
5454
+ for (const blobDir of params.blobDirs) {
5455
+ if (!existsSync(blobDir)) continue;
5456
+ for (const blobRef of await listExternalJsonBlobPaths(blobDir)) if (!indexedBlobRefs.has(blobRef)) {
5457
+ await rm(resolveStorePath(blobDir, blobRef), { force: true });
5458
+ summary.removedBlobFiles++;
5459
+ }
5460
+ }
5461
+ return summary;
5267
5462
  }
5268
5463
  function usesSupportedCacheSerialization(value) {
5269
5464
  if (Array.isArray(value)) return value.every(usesSupportedCacheSerialization);
@@ -5271,14 +5466,14 @@ function usesSupportedCacheSerialization(value) {
5271
5466
  if (Object.hasOwn(value, cacheSerializationMarker) && (typeof value[cacheSerializationMarker] !== "string" || !value[cacheSerializationMarker].startsWith(supportedCacheSerializationPrefix))) return false;
5272
5467
  return Object.values(value).every(usesSupportedCacheSerialization);
5273
5468
  }
5274
- function createExternalJsonBlobStore(blobDir) {
5469
+ function createExternalJsonBlobStore(params) {
5275
5470
  return {
5276
5471
  async write(rawJson) {
5277
5472
  const rawBytes = Buffer.from(rawJson, "utf8");
5278
5473
  const hash = hashExternalJson(rawBytes);
5279
5474
  const path = externalJsonBlobPath(hash);
5280
5475
  const compressed = brotliCompressSync(rawBytes);
5281
- const filePath = resolveStorePath(blobDir, path);
5476
+ const filePath = resolveStorePath(params.primaryDir, path);
5282
5477
  if (!existsSync(filePath)) await writeAtomicFile(filePath, compressed);
5283
5478
  return {
5284
5479
  compressedLength: compressed.byteLength,
@@ -5288,10 +5483,15 @@ function createExternalJsonBlobStore(blobDir) {
5288
5483
  };
5289
5484
  },
5290
5485
  async read(ref) {
5291
- const rawBytes = brotliDecompressSync(await readFile(resolveStorePath(blobDir, ref.path)));
5292
- const rawJson = rawBytes.toString("utf8");
5293
- if (rawBytes.byteLength !== ref.length || hashExternalJson(rawBytes) !== ref.hash) throw new Error(`External cache blob failed integrity check: ${ref.hash}`);
5294
- return rawJson;
5486
+ for (const dir of [params.primaryDir, ...params.fallbackDirs]) {
5487
+ const compressedResult = await resultify(() => readFile(resolveStorePath(dir, ref.path)));
5488
+ if (compressedResult.error) continue;
5489
+ const rawBytesResult = resultify(() => brotliDecompressSync(compressedResult.value));
5490
+ if (rawBytesResult.error) continue;
5491
+ const rawBytes = rawBytesResult.value;
5492
+ if (rawBytes.byteLength === ref.length && hashExternalJson(rawBytes) === ref.hash) return rawBytes.toString("utf8");
5493
+ }
5494
+ throw new Error(`External cache blob failed integrity check: ${ref.hash}`);
5295
5495
  }
5296
5496
  };
5297
5497
  }
@@ -5317,28 +5517,55 @@ async function materializeExternalJsonCacheEntryOrNull(entry, store) {
5317
5517
  const result = await resultify(() => materializeExternalJsonCacheEntry(entry, store));
5318
5518
  return result.error ? null : result.value;
5319
5519
  }
5320
- async function pruneExternalJsonBlobs(cacheDir, blobDir) {
5321
- if (!existsSync(blobDir)) return;
5520
+ async function pruneUnreferencedExternalJsonBlobs(cacheDir, blobDirs) {
5322
5521
  const referenced = await collectReferencedExternalJsonBlobPaths(cacheDir);
5323
- for (const path of await listExternalJsonBlobPaths(blobDir)) if (!referenced.has(path)) await rm(resolveStorePath(blobDir, path), { force: true });
5522
+ for (const blobDir of blobDirs) {
5523
+ if (!existsSync(blobDir)) continue;
5524
+ for (const path of await listExternalJsonBlobPaths(blobDir)) if (!referenced.has(path)) await rm(resolveStorePath(blobDir, path), { force: true });
5525
+ }
5324
5526
  }
5325
5527
  async function collectReferencedExternalJsonBlobPaths(cacheDir) {
5326
5528
  const paths = /* @__PURE__ */ new Set();
5327
- for (const filePath of await listCacheEntryFiles(cacheDir)) {
5328
- const fileEntry = await readCacheEntryFilePath(filePath);
5329
- if (fileEntry === null || !entryMatchesPath(filePath, fileEntry.entry)) continue;
5330
- collectExternalJsonBlobPaths(fileEntry.entry, paths);
5331
- }
5529
+ for (const index_ of await listCacheIndexes(cacheDir)) for (const entry of Object.values(index_.entries)) for (const blobRef of entry.blobRefs) paths.add(blobRef);
5332
5530
  return paths;
5333
5531
  }
5334
- function collectExternalJsonBlobPaths(value, paths) {
5532
+ async function collectExternalJsonBlobRefs(value, blobDirs) {
5533
+ const paths = /* @__PURE__ */ new Set();
5534
+ const pendingBlobPaths = [];
5535
+ collectExternalJsonBlobPaths(value, paths, pendingBlobPaths);
5536
+ while (pendingBlobPaths.length > 0) {
5537
+ const blobPath = pendingBlobPaths.pop();
5538
+ if (blobPath === void 0) continue;
5539
+ const rawJson = await readExternalJsonBlobByPath(blobDirs, blobPath);
5540
+ if (rawJson === null) continue;
5541
+ const json = safeJsonParse(rawJson);
5542
+ if (json === null) continue;
5543
+ collectExternalJsonBlobPaths(json, paths, pendingBlobPaths);
5544
+ }
5545
+ return [...paths].sort();
5546
+ }
5547
+ function collectExternalJsonBlobPaths(value, paths, pendingBlobPaths) {
5335
5548
  if (Array.isArray(value)) {
5336
- for (const item of value) collectExternalJsonBlobPaths(item, paths);
5549
+ for (const item of value) collectExternalJsonBlobPaths(item, paths, pendingBlobPaths);
5337
5550
  return;
5338
5551
  }
5339
5552
  if (!isRecordLike(value)) return;
5340
- if (value[cacheSerializationMarker] === externalJsonCacheSerializationMarker && typeof value.path === "string") paths.add(value.path);
5341
- for (const entryValue of Object.values(value)) collectExternalJsonBlobPaths(entryValue, paths);
5553
+ if (value[cacheSerializationMarker] === externalJsonCacheSerializationMarker && typeof value.path === "string") {
5554
+ if (!paths.has(value.path)) {
5555
+ paths.add(value.path);
5556
+ pendingBlobPaths.push(value.path);
5557
+ }
5558
+ }
5559
+ for (const entryValue of Object.values(value)) collectExternalJsonBlobPaths(entryValue, paths, pendingBlobPaths);
5560
+ }
5561
+ async function readExternalJsonBlobByPath(blobDirs, path) {
5562
+ for (const blobDir of blobDirs) {
5563
+ const compressedResult = await resultify(() => readFile(resolveStorePath(blobDir, path)));
5564
+ if (compressedResult.error) continue;
5565
+ const rawResult = resultify(() => brotliDecompressSync(compressedResult.value).toString("utf8"));
5566
+ if (!rawResult.error) return rawResult.value;
5567
+ }
5568
+ return null;
5342
5569
  }
5343
5570
  async function listExternalJsonBlobPaths(blobDir) {
5344
5571
  const paths = [];
@@ -5357,12 +5584,33 @@ async function collectExternalJsonBlobFilePaths(root, dir, paths) {
5357
5584
  if (entry.isFile() && entry.name.endsWith(externalJsonBlobExtension)) paths.push(relative(root, path));
5358
5585
  }
5359
5586
  }
5360
- async function listCacheEntryFiles(rootDir) {
5361
- return listFilesWithExtension(rootDir, cacheEntryExtension);
5587
+ async function listCacheEntryFiles(rootDir, scope) {
5588
+ if (scope === "namespace") return listDirectFilesWithExtension(rootDir, cacheEntryExtension);
5589
+ if (!existsSync(rootDir)) return [];
5590
+ const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
5591
+ if (entriesResult.error) return [];
5592
+ const files = [];
5593
+ for (const entry of entriesResult.value) {
5594
+ if (!entry.isDirectory()) continue;
5595
+ files.push(...await listDirectFilesWithExtension(join(rootDir, entry.name), cacheEntryExtension));
5596
+ }
5597
+ return files;
5362
5598
  }
5363
5599
  async function listDebugEntryFiles(rootDir) {
5364
5600
  return listFilesWithExtension(rootDir, debugEntryExtension);
5365
5601
  }
5602
+ async function listCacheIndexFiles(rootDir) {
5603
+ if (!existsSync(rootDir)) return [];
5604
+ const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
5605
+ if (entriesResult.error) return [];
5606
+ return entriesResult.value.filter((entry) => entry.isFile() && entry.name.startsWith(cacheIndexFilePrefix) && entry.name.endsWith(debugEntryExtension)).map((entry) => join(rootDir, entry.name));
5607
+ }
5608
+ async function listDirectFilesWithExtension(rootDir, extension) {
5609
+ if (!existsSync(rootDir)) return [];
5610
+ const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
5611
+ if (entriesResult.error) return [];
5612
+ return entriesResult.value.filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => join(rootDir, entry.name));
5613
+ }
5366
5614
  async function listFilesWithExtension(rootDir, extension) {
5367
5615
  if (!existsSync(rootDir)) return [];
5368
5616
  const entriesResult = await resultify(() => readdir(rootDir, { withFileTypes: true }));
@@ -5396,6 +5644,7 @@ async function withCacheFileLock(filePath, fn) {
5396
5644
  force: true
5397
5645
  });
5398
5646
  if (result.error) throw result.error;
5647
+ return result.value;
5399
5648
  }
5400
5649
  async function acquireLock(lockPath) {
5401
5650
  const startedAt = Date.now();
@@ -1,4 +1,4 @@
1
- import { Et as caseRowSchema, Pt as runWithEvalRegistry, Tt as caseDetailSchema, X as runWithEvalClock, _t as validateEvalTagName, bt as runSummarySchema, d as loadEvalModule, f as resolveEvalDefaultConfig, ft as deriveScopedSummaryFromCases, g as commitPendingCacheWrites, gt as matchesTagsFilter, ht as dedupeEvalTags, i as isCaseChildMessage, m as buildDeclaredColumnDefs, mt as deriveStatusFromChildStatuses, n as resolveRunnableEvalCases, o as stripTerminalControlCodes, pt as deriveStatusFromCaseRows, q as runInEvalRuntimeScope, t as filterEvalCases, u as runWithModuleIsolation, vt as validateTagsFilterExpression, wt as getCaseRowCaseKey, yt as runManifestSchema } from "./runExecution-Sw38bCaq.mjs";
1
+ import { Et as caseRowSchema, Pt as runWithEvalRegistry, Tt as caseDetailSchema, X as runWithEvalClock, _t as validateEvalTagName, bt as runSummarySchema, d as loadEvalModule, f as resolveEvalDefaultConfig, ft as deriveScopedSummaryFromCases, g as commitPendingCacheWrites, gt as matchesTagsFilter, ht as dedupeEvalTags, i as isCaseChildMessage, m as buildDeclaredColumnDefs, mt as deriveStatusFromChildStatuses, n as resolveRunnableEvalCases, o as stripTerminalControlCodes, pt as deriveStatusFromCaseRows, q as runInEvalRuntimeScope, t as filterEvalCases, u as runWithModuleIsolation, vt as validateTagsFilterExpression, wt as getCaseRowCaseKey, yt as runManifestSchema } from "./runExecution-C31dpemR.mjs";
2
2
  import { readFile, readdir, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { existsSync } from "node:fs";
@@ -1,5 +1,5 @@
1
- import { n as createRunner } from "./cli-BR3wMZMx.mjs";
2
- import "./src-hBGtzWuA.mjs";
1
+ import { n as createRunner } from "./cli-Bu9347r1.mjs";
2
+ import "./src-FR60ZR_4.mjs";
3
3
  //#region ../../apps/server/src/runner.ts
4
4
  let runnerInstance = null;
5
5
  function getRunnerInstance() {
@@ -1,2 +1,2 @@
1
- import { n as initRunner, t as getRunnerInstance } from "./runner-72rsqJRq.mjs";
1
+ import { n as initRunner, t as getRunnerInstance } from "./runner-B4EfMn1d.mjs";
2
2
  export { getRunnerInstance, initRunner };