@mastra/longmemeval 0.0.0-error-handler-fix-20251020202607 → 0.0.0-execa-dynamic-import-20260304221256

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 (43) hide show
  1. package/CHANGELOG.md +847 -6
  2. package/LICENSE.md +15 -0
  3. package/docs/INVESTIGATION_WORKFLOW.md +314 -0
  4. package/package.json +34 -26
  5. package/scripts/download.ts +22 -1
  6. package/scripts/generate-wm-templates.ts +2 -1
  7. package/src/cli.ts +967 -155
  8. package/src/commands/__tests__/prepare.test.ts +11 -6
  9. package/src/commands/__tests__/run.test.ts +10 -8
  10. package/src/commands/clean.ts +147 -0
  11. package/src/commands/deterministic-ids.ts +126 -0
  12. package/src/commands/find-prohibited.ts +299 -0
  13. package/src/commands/investigate.ts +2437 -0
  14. package/src/commands/list-partial.ts +137 -0
  15. package/src/commands/obscure-thread-ids.ts +237 -0
  16. package/src/commands/partial-results.ts +198 -0
  17. package/src/commands/precompute-embeddings.ts +302 -0
  18. package/src/commands/prepare.ts +732 -105
  19. package/src/commands/reconcile.ts +383 -0
  20. package/src/commands/run.ts +1240 -121
  21. package/src/commands/sessions.ts +126 -0
  22. package/src/commands/sync.ts +152 -0
  23. package/src/commands/test-cleaned.ts +53 -0
  24. package/src/commands/test-incremental.ts +120 -0
  25. package/src/commands/test-messages.ts +95 -0
  26. package/src/commands/test-prohibited.ts +244 -0
  27. package/src/commands/test-thread.ts +136 -0
  28. package/src/commands/tokens.ts +625 -0
  29. package/src/config.ts +1253 -72
  30. package/src/data/types.ts +48 -6
  31. package/src/embeddings/cached-openai-embedding-model.ts +0 -1
  32. package/src/evaluation/longmemeval-metric.ts +186 -103
  33. package/src/processors/content-sanitizer.ts +153 -0
  34. package/src/processors/date-injector.ts +111 -0
  35. package/src/processors/index.ts +3 -0
  36. package/src/processors/observation-semantic-filter.ts +1142 -0
  37. package/src/retry-model.ts +5 -3
  38. package/src/storage/__tests__/benchmark-store.test.ts +18 -35
  39. package/src/storage/benchmark-store.ts +140 -153
  40. package/src/storage/benchmark-vector.ts +1 -1
  41. package/src/storage/index.ts +1 -0
  42. package/src/storage/persistable-inmemory.ts +133 -0
  43. package/src/test-utils/mock-model.ts +35 -0
package/src/cli.ts CHANGED
@@ -1,16 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import 'dotenv/config';
3
4
  import { Command } from 'commander';
4
5
  import chalk from 'chalk';
5
- import { readFile, readdir } from 'fs/promises';
6
+ import { readFile, readdir, writeFile } from 'fs/promises';
6
7
  import { join } from 'path';
7
8
  import { existsSync, statSync } from 'fs';
8
9
  import { execSync } from 'child_process';
9
10
 
10
11
  import { DatasetLoader } from './data/loader';
11
- import type { EvaluationResult, BenchmarkMetrics, QuestionType } from './data/types';
12
+ import type { EvaluationResult, BenchmarkMetrics, QuestionType, DatasetType, MemoryConfigType } from './data/types';
12
13
  import { PrepareCommand } from './commands/prepare';
13
14
  import { RunCommand } from './commands/run';
15
+ import { SyncCommand } from './commands/sync';
16
+ import { CleanCommand } from './commands/clean';
17
+ import { ObscureThreadIdsCommand } from './commands/obscure-thread-ids';
18
+ import { SessionsCommand } from './commands/sessions';
19
+ import { DeterministicIdsCommand } from './commands/deterministic-ids';
20
+ import { ListPartialCommand } from './commands/list-partial';
21
+ import { TokensCommand } from './commands/tokens';
22
+ import { PrecomputeEmbeddingsCommand } from './commands/precompute-embeddings';
23
+ import { FindProhibitedCommand } from './commands/find-prohibited';
24
+ import { PartialResultsCommand } from './commands/partial-results';
25
+ import {
26
+ getRunVariant,
27
+ resolveConfigAlias,
28
+ getConfigAliases,
29
+ getMemoryConfig,
30
+ RUN_VARIANTS,
31
+ CONFIG_ALIASES,
32
+ } from './config';
14
33
 
15
34
  const program = new Command();
16
35
 
@@ -86,52 +105,326 @@ function calculateMetrics(results: EvaluationResult[]): BenchmarkMetrics {
86
105
  return metrics;
87
106
  }
88
107
 
108
+ // Helper to load preparation token usage from om-debug.jsonl files
109
+ interface PreparationTokenUsage {
110
+ observerInputTokens: number;
111
+ observerOutputTokens: number;
112
+ reflectorInputTokens: number;
113
+ reflectorOutputTokens: number;
114
+ totalInputTokens: number;
115
+ totalOutputTokens: number;
116
+ questionCount: number;
117
+ }
118
+
119
+ // Cache for per-question preparation token usage
120
+ interface QuestionTokenCache {
121
+ [questionId: string]: {
122
+ mtime: number; // mtime of om-debug.jsonl when cached
123
+ usage: {
124
+ observerInputTokens: number;
125
+ observerOutputTokens: number;
126
+ reflectorInputTokens: number;
127
+ reflectorOutputTokens: number;
128
+ };
129
+ };
130
+ }
131
+
132
+ interface PrepTokenCache {
133
+ [memoryConfig: string]: QuestionTokenCache;
134
+ }
135
+
136
+ const PREP_TOKEN_CACHE_FILE = '.prep-token-cache.json';
137
+
138
+ async function loadPrepTokenCache(): Promise<PrepTokenCache> {
139
+ try {
140
+ const content = await readFile(PREP_TOKEN_CACHE_FILE, 'utf-8');
141
+ return JSON.parse(content);
142
+ } catch {
143
+ return {};
144
+ }
145
+ }
146
+
147
+ async function savePrepTokenCache(cache: PrepTokenCache): Promise<void> {
148
+ await writeFile(PREP_TOKEN_CACHE_FILE, JSON.stringify(cache, null, 2));
149
+ }
150
+
151
+ async function parseDebugFileForTokens(debugFile: string): Promise<{
152
+ observerInputTokens: number;
153
+ observerOutputTokens: number;
154
+ reflectorInputTokens: number;
155
+ reflectorOutputTokens: number;
156
+ } | null> {
157
+ try {
158
+ const content = await readFile(debugFile, 'utf-8');
159
+
160
+ const result = {
161
+ observerInputTokens: 0,
162
+ observerOutputTokens: 0,
163
+ reflectorInputTokens: 0,
164
+ reflectorOutputTokens: 0,
165
+ };
166
+
167
+ // Parse pretty-printed JSON objects
168
+ const events: any[] = [];
169
+ let currentJson = '';
170
+ let braceCount = 0;
171
+
172
+ for (const line of content.split('\n')) {
173
+ const trimmed = line.trim();
174
+
175
+ if (braceCount === 0 && trimmed.startsWith('{')) {
176
+ currentJson = line;
177
+ braceCount = (trimmed.match(/{/g) || []).length - (trimmed.match(/}/g) || []).length;
178
+ } else if (braceCount > 0) {
179
+ currentJson += '\n' + line;
180
+ braceCount += (trimmed.match(/{/g) || []).length;
181
+ braceCount -= (trimmed.match(/}/g) || []).length;
182
+ }
183
+
184
+ if (braceCount === 0 && currentJson) {
185
+ try {
186
+ events.push(JSON.parse(currentJson));
187
+ } catch {
188
+ // Skip malformed JSON
189
+ }
190
+ currentJson = '';
191
+ }
192
+ }
193
+
194
+ for (const event of events) {
195
+ if (event.usage && event.usage.inputTokens) {
196
+ if (event.type === 'observation_complete') {
197
+ result.observerInputTokens += event.usage.inputTokens || 0;
198
+ result.observerOutputTokens += event.usage.outputTokens || 0;
199
+ } else if (event.type === 'reflection_complete') {
200
+ result.reflectorInputTokens += event.usage.inputTokens || 0;
201
+ result.reflectorOutputTokens += event.usage.outputTokens || 0;
202
+ }
203
+ }
204
+ }
205
+
206
+ return result;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ async function loadPreparationTokenUsage(
213
+ preparedDataDir: string,
214
+ memoryConfig: string,
215
+ questionIds?: string[],
216
+ ): Promise<PreparationTokenUsage | null> {
217
+ const usage: PreparationTokenUsage = {
218
+ observerInputTokens: 0,
219
+ observerOutputTokens: 0,
220
+ reflectorInputTokens: 0,
221
+ reflectorOutputTokens: 0,
222
+ totalInputTokens: 0,
223
+ totalOutputTokens: 0,
224
+ questionCount: 0,
225
+ };
226
+
227
+ try {
228
+ const configDir = join(preparedDataDir, 'longmemeval_s', memoryConfig);
229
+ if (!existsSync(configDir)) {
230
+ return null;
231
+ }
232
+
233
+ // Load cache
234
+ const cache = await loadPrepTokenCache();
235
+ if (!cache[memoryConfig]) {
236
+ cache[memoryConfig] = {};
237
+ }
238
+ const configCache = cache[memoryConfig];
239
+ let cacheUpdated = false;
240
+
241
+ const questionDirs = await readdir(configDir);
242
+ const dirsToProcess = questionIds ? questionDirs.filter(d => questionIds.includes(d)) : questionDirs;
243
+
244
+ for (const questionDir of dirsToProcess) {
245
+ const debugFile = join(configDir, questionDir, 'om-debug.jsonl');
246
+ if (!existsSync(debugFile)) continue;
247
+
248
+ try {
249
+ const fileStat = statSync(debugFile);
250
+ const fileMtime = fileStat.mtimeMs;
251
+
252
+ // Check if we have a valid cache entry
253
+ const cached = configCache[questionDir];
254
+ if (cached && cached.mtime === fileMtime) {
255
+ // Use cached values
256
+ usage.observerInputTokens += cached.usage.observerInputTokens;
257
+ usage.observerOutputTokens += cached.usage.observerOutputTokens;
258
+ usage.reflectorInputTokens += cached.usage.reflectorInputTokens;
259
+ usage.reflectorOutputTokens += cached.usage.reflectorOutputTokens;
260
+ usage.questionCount++;
261
+ } else {
262
+ // Parse the file and update cache
263
+ const parsed = await parseDebugFileForTokens(debugFile);
264
+ if (parsed) {
265
+ usage.observerInputTokens += parsed.observerInputTokens;
266
+ usage.observerOutputTokens += parsed.observerOutputTokens;
267
+ usage.reflectorInputTokens += parsed.reflectorInputTokens;
268
+ usage.reflectorOutputTokens += parsed.reflectorOutputTokens;
269
+ usage.questionCount++;
270
+
271
+ // Update cache
272
+ configCache[questionDir] = {
273
+ mtime: fileMtime,
274
+ usage: parsed,
275
+ };
276
+ cacheUpdated = true;
277
+ }
278
+ }
279
+ } catch {
280
+ // Skip files that can't be read
281
+ }
282
+ }
283
+
284
+ // Save cache if updated
285
+ if (cacheUpdated) {
286
+ await savePrepTokenCache(cache);
287
+ }
288
+
289
+ usage.totalInputTokens = usage.observerInputTokens + usage.reflectorInputTokens;
290
+ usage.totalOutputTokens = usage.observerOutputTokens + usage.reflectorOutputTokens;
291
+
292
+ return usage.questionCount > 0 ? usage : null;
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+
89
298
  program.name('longmemeval').description('LongMemEval benchmark for Mastra Memory').version('0.1.0');
90
299
 
300
+ // Helper to show available variants and configs
301
+ function showAvailableOptions() {
302
+ console.log(chalk.blue('\n📋 Available Run Variants:\n'));
303
+ for (const [name, variant] of Object.entries(RUN_VARIANTS)) {
304
+ const subsetInfo = variant.subset ? `${variant.subset} questions` : 'all questions';
305
+ console.log(
306
+ chalk.bold(` ${name.padEnd(10)}`),
307
+ chalk.gray(`- ${variant.description}`),
308
+ chalk.dim(`(${subsetInfo}, prepare: ${variant.prepareConcurrency}x, bench: ${variant.benchConcurrency}x)`),
309
+ );
310
+ }
311
+
312
+ console.log(chalk.blue('\n📋 Available Memory Configs:\n'));
313
+ const configNames = getConfigAliases();
314
+ // Build reverse map: full name -> short aliases
315
+ const shortAliases: Record<string, string[]> = {};
316
+ for (const [alias, fullName] of Object.entries(CONFIG_ALIASES)) {
317
+ if (alias !== fullName) {
318
+ if (!shortAliases[fullName]) shortAliases[fullName] = [];
319
+ shortAliases[fullName].push(alias);
320
+ }
321
+ }
322
+ for (const configName of configNames) {
323
+ const configAliases = shortAliases[configName];
324
+ if (configAliases && configAliases.length > 0) {
325
+ console.log(chalk.bold(` ${configName.padEnd(35)}`), chalk.gray(`(alias: ${configAliases.join(', ')})`));
326
+ } else {
327
+ console.log(chalk.bold(` ${configName}`));
328
+ }
329
+ }
330
+
331
+ console.log(chalk.gray('\n\nUsage:'));
332
+ console.log(chalk.gray(' pnpm run prepare <config> [-v quick|full]'));
333
+ console.log(chalk.gray(' pnpm run bench <config> [-v quick|full]'));
334
+ console.log(chalk.gray('\nExamples:'));
335
+ console.log(chalk.gray(' pnpm run prepare om # uses quick variant by default'));
336
+ console.log(chalk.gray(' pnpm run prepare om -v full # full benchmark'));
337
+ console.log(chalk.gray(' pnpm run bench om'));
338
+ console.log(chalk.gray(' pnpm run bench om-glm -v full'));
339
+ console.log(chalk.gray('\nAdditional flags:'));
340
+ console.log(chalk.gray(' -v, --variant Run variant (quick, full, rip) - default: quick'));
341
+ console.log(chalk.gray(' --offset <n> Skip first n questions'));
342
+ console.log(chalk.gray(' --question-id <id> Process specific question'));
343
+ console.log(chalk.gray(' -y, --yes Skip confirmation prompt'));
344
+ }
345
+
91
346
  // Prepare command
92
347
  program
93
- .command('prepare')
348
+ .command('prepare [config]')
94
349
  .description('Prepare LongMemEval data by processing through mock agents')
95
- .option('-d, --dataset <dataset>', 'Dataset to use', 'longmemeval_s')
96
- .option(
97
- '-c, --memory-config <config>',
98
- 'Memory configuration (last-k, semantic-recall, semantic-recall-reranked, working-memory, working-memory-tailored, combined, combined-tailored)',
99
- 'semantic-recall',
100
- )
350
+ .option('-v, --variant <variant>', 'Run variant (quick, full, rip)', 'quick')
101
351
  .option('-o, --output <dir>', 'Output directory for prepared data', './prepared-data')
102
- .option('--subset <n>', 'Prepare only a subset of n questions', parseInt)
103
- .option('--concurrency <n>', 'Number of questions to process in parallel', parseInt)
352
+ .option('--subset <n>', 'Override subset size', parseInt)
353
+ .option('--offset <n>', 'Skip first n questions', parseInt)
354
+ .option('--concurrency <n>', 'Override concurrency', parseInt)
104
355
  .option('--question-id <id>', 'Prepare a specific question by ID')
105
356
  .option('--resume-from-message-id <id>', 'Resume processing from a specific message ID')
106
357
  .option('--session-limit <n>', 'Limit processing to n sessions after resume point', parseInt)
107
358
  .option('--session-offset <n>', 'Start processing from the nth session (1-based)', parseInt)
108
- .action(async options => {
359
+ .option('--from-failures [path]', 'Re-prepare failed questions (uses latest failures.json if no path given)')
360
+ .option('--older-than <duration>', 'Only re-prepare questions older than this duration (e.g., "1h", "30m", "2d")')
361
+ .option('--dry-run', 'Show what would be re-prepared without actually doing it (use with --from-failures)')
362
+ .option('--force-regenerate', 'Force regeneration by deleting existing prepared data first')
363
+ .option('-y, --yes', 'Skip confirmation prompt')
364
+ // Legacy options for backwards compatibility
365
+ .option('-d, --dataset <dataset>', 'Dataset to use (legacy)')
366
+ .option('-c, --memory-config <config>', 'Memory configuration (legacy)')
367
+ .action(async (config, options) => {
109
368
  try {
110
- console.log(chalk.blue('\n🚀 LongMemEval Data Preparation\n'));
111
- console.log(chalk.gray(`Dataset: ${options.dataset}`));
112
- console.log(chalk.gray(`Memory Config: ${options.memoryConfig}`));
113
- if (options.subset) {
114
- console.log(chalk.gray(`Subset: ${options.subset} questions`));
369
+ // If no config provided, show help
370
+ if (!config && !options.dataset) {
371
+ showAvailableOptions();
372
+ process.exit(0);
115
373
  }
116
- if (options.questionId) {
117
- console.log(chalk.gray(`Question ID: ${options.questionId}`));
374
+
375
+ // Resolve variant and config (support legacy options)
376
+ let resolvedVariant: ReturnType<typeof getRunVariant>;
377
+ let resolvedConfig: string;
378
+ let dataset: string;
379
+
380
+ if (options.dataset && options.memoryConfig) {
381
+ // Legacy mode: use -d and -c flags
382
+ dataset = options.dataset;
383
+ resolvedConfig = options.memoryConfig;
384
+ resolvedVariant = {
385
+ name: 'custom',
386
+ description: 'Custom run',
387
+ dataset: dataset as any,
388
+ subset: options.subset,
389
+ prepareConcurrency: options.concurrency ?? 4,
390
+ benchConcurrency: options.concurrency ?? 10,
391
+ };
392
+ } else if (config) {
393
+ // New mode: config as positional, variant as flag (default: quick)
394
+ resolvedVariant = getRunVariant(options.variant);
395
+ resolvedConfig = resolveConfigAlias(config);
396
+ dataset = resolvedVariant.dataset;
397
+ } else {
398
+ console.error(chalk.red('Error: Please provide a <config>'));
399
+ console.error(chalk.gray('Run without arguments to see available options'));
400
+ process.exit(1);
118
401
  }
119
- if (options.resumeFromMessageId) {
120
- console.log(chalk.gray(`Resume from message ID: ${options.resumeFromMessageId}`));
402
+
403
+ // Apply overrides
404
+ const subset = options.subset ?? resolvedVariant.subset;
405
+ const perTypeCount = resolvedVariant.perTypeCount;
406
+ const concurrency = options.concurrency ?? resolvedVariant.prepareConcurrency;
407
+
408
+ console.log(chalk.blue('\n🚀 LongMemEval Data Preparation\n'));
409
+ console.log(chalk.gray(`Variant: ${resolvedVariant.name}`));
410
+ console.log(chalk.gray(`Dataset: ${dataset}`));
411
+ console.log(chalk.gray(`Memory Config: ${resolvedConfig}`));
412
+ console.log(chalk.gray(`Concurrency: ${concurrency}`));
413
+ if (perTypeCount) {
414
+ console.log(chalk.gray(`Stratified Sample: ${perTypeCount} per type`));
415
+ } else if (subset) {
416
+ console.log(chalk.gray(`Subset: ${subset} questions`));
121
417
  }
122
- if (options.sessionLimit) {
123
- console.log(chalk.gray(`Session limit: ${options.sessionLimit} sessions`));
418
+ if (options.offset) {
419
+ console.log(chalk.gray(`Offset: skipping first ${options.offset} questions`));
124
420
  }
125
- if (options.sessionOffset) {
126
- console.log(chalk.gray(`Session offset: Start from session ${options.sessionOffset}`));
421
+ if (options.questionId) {
422
+ console.log(chalk.gray(`Question ID: ${options.questionId}`));
127
423
  }
128
424
  console.log();
129
425
 
130
426
  // Check for OpenAI API key (needed for embeddings in semantic-recall)
131
- if (
132
- (options.memoryConfig === 'semantic-recall' || options.memoryConfig === 'combined') &&
133
- !process.env.OPENAI_API_KEY
134
- ) {
427
+ if ((resolvedConfig === 'semantic-recall' || resolvedConfig === 'combined') && !process.env.OPENAI_API_KEY) {
135
428
  console.error(chalk.red('Error: OPENAI_API_KEY environment variable is required for semantic recall'));
136
429
  console.error(chalk.gray('Please set it in your environment or .env file'));
137
430
  process.exit(1);
@@ -139,55 +432,74 @@ program
139
432
 
140
433
  // Validate dataset option
141
434
  const validDatasets = ['longmemeval_s', 'longmemeval_m', 'longmemeval_oracle'];
142
- if (!validDatasets.includes(options.dataset)) {
143
- console.error(chalk.red(`Invalid dataset: ${options.dataset}`));
435
+ if (!validDatasets.includes(dataset)) {
436
+ console.error(chalk.red(`Invalid dataset: ${dataset}`));
144
437
  console.error(chalk.gray(`Valid options: ${validDatasets.join(', ')}`));
145
438
  process.exit(1);
146
439
  }
147
440
 
148
441
  // Check if dataset exists and download if needed
149
- await ensureDatasetExists(options.dataset);
150
-
151
- // Show warning and ask for confirmation
152
- console.log(chalk.yellow('\n⚠️ WARNING'));
153
- console.log(chalk.yellow('━'.repeat(50)));
154
- console.log(chalk.bold('\nPreparing this data can be very expensive!\n'));
155
- console.log('This process will:');
156
- console.log(' • Process many conversations through AI models');
157
- console.log(' • Generate embeddings for semantic recall');
158
- console.log(' • Potentially use significant API credits\n');
159
- console.log(chalk.gray('Memory configs like "working-memory" and "combined" are especially costly.\n'));
160
-
161
- const readline = await import('readline');
162
- const rl = readline.createInterface({
163
- input: process.stdin,
164
- output: process.stdout,
165
- });
166
-
167
- const answer = await new Promise<string>(resolve => {
168
- rl.question(chalk.bold('Are you sure you want to continue? (y/N): '), resolve);
169
- });
170
- rl.close();
442
+ await ensureDatasetExists(dataset);
171
443
 
172
- if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
173
- console.log(chalk.gray('\nCancelled by user.'));
174
- process.exit(0);
444
+ // For readOnlyConfig, no preparation is needed
445
+ const configDef = getMemoryConfig(resolvedConfig as MemoryConfigType);
446
+ if (configDef.readOnlyConfig && configDef.baseConfig) {
447
+ console.log(
448
+ chalk.green(`✓ Config "${resolvedConfig}" is read-only and uses data from "${configDef.baseConfig}"`),
449
+ );
450
+ console.log(chalk.gray(` No preparation needed. Run benchmark directly with: pnpm bench ${resolvedConfig}`));
451
+ console.log(chalk.gray(` Make sure "${configDef.baseConfig}" is prepared first.\n`));
452
+ return;
175
453
  }
176
454
 
177
- console.log(); // Add spacing before continuing
455
+ // Show warning and ask for confirmation (skip if -y flag is passed)
456
+ if (!options.yes) {
457
+ console.log(chalk.yellow('\n⚠️ WARNING'));
458
+ console.log(chalk.yellow('━'.repeat(50)));
459
+ console.log(chalk.bold('\nPreparing this data can be very expensive!\n'));
460
+ console.log('This process will:');
461
+ console.log(' • Process many conversations through AI models');
462
+ console.log(' • Generate embeddings for semantic recall');
463
+ console.log(' • Potentially use significant API credits\n');
464
+ console.log(chalk.gray('Memory configs like "working-memory" and "combined" are especially costly.\n'));
465
+
466
+ const readline = await import('readline');
467
+ const rl = readline.createInterface({
468
+ input: process.stdin,
469
+ output: process.stdout,
470
+ });
471
+
472
+ const answer = await new Promise<string>(resolve => {
473
+ rl.question(chalk.bold('Are you sure you want to continue? (y/N): '), resolve);
474
+ });
475
+ rl.close();
476
+
477
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
478
+ console.log(chalk.gray('\nCancelled by user.'));
479
+ process.exit(0);
480
+ }
481
+
482
+ console.log(); // Add spacing before continuing
483
+ }
178
484
 
179
485
  // Run prepare command
180
486
  const prepareCommand = new PrepareCommand();
181
487
  await prepareCommand.run({
182
- dataset: options.dataset,
183
- memoryConfig: options.memoryConfig,
488
+ dataset: dataset as DatasetType,
489
+ memoryConfig: resolvedConfig as MemoryConfigType,
184
490
  outputDir: options.output,
185
- subset: options.subset,
186
- concurrency: options.concurrency,
491
+ subset: perTypeCount ? undefined : subset, // Don't use subset if using perTypeCount
492
+ perTypeCount,
493
+ offset: options.offset,
494
+ concurrency,
187
495
  questionId: options.questionId,
188
496
  resumeFromMessageId: options.resumeFromMessageId,
189
497
  sessionLimit: options.sessionLimit,
190
498
  sessionOffset: options.sessionOffset,
499
+ fromFailures: options.fromFailures,
500
+ olderThan: options.olderThan,
501
+ dryRun: options.dryRun,
502
+ forceRegenerate: options.forceRegenerate,
191
503
  });
192
504
 
193
505
  // Force exit after completion
@@ -200,25 +512,99 @@ program
200
512
  }
201
513
  });
202
514
 
203
- // Run benchmark command
515
+ // Run benchmark command (aliased as 'bench' too)
204
516
  program
205
- .command('run')
517
+ .command('run [config]')
518
+ .alias('bench')
206
519
  .description('Run LongMemEval benchmark using prepared data')
207
- .requiredOption('-d, --dataset <dataset>', 'Dataset to use (longmemeval_s, longmemeval_m, longmemeval_oracle)')
208
- .requiredOption('-m, --model <model>', 'Model to use (e.g., gpt-4o, claude-3-opus)')
209
- .option(
210
- '-c, --memory-config <config>',
211
- 'Memory configuration (last-k, semantic-recall, semantic-recall-reranked, working-memory, working-memory-tailored, combined, combined-tailored)',
212
- 'semantic-recall',
213
- )
520
+ .option('-v, --variant <variant>', 'Run variant (quick, full, rip, sample, sample-comb)', 'quick')
214
521
  .option('-o, --output <dir>', 'Output directory for results', './results')
215
522
  .option('--prepared-data <dir>', 'Directory containing prepared data', './prepared-data')
216
- .option('--subset <n>', 'Run on subset of n questions', parseInt)
217
- .option('--concurrency <n>', 'Number of parallel requests (default: 5)', parseInt)
523
+ .option('--subset <n>', 'Override subset size', parseInt)
524
+ .option('--offset <n>', 'Skip first n questions', parseInt)
525
+ .option('--concurrency <n>', 'Override concurrency', parseInt)
218
526
  .option('--question-id <id>', 'Focus on a specific question by ID')
219
- .action(async options => {
527
+ .option('-t, --type <type>', 'Filter to a specific question type (e.g., multi-session, knowledge-update)')
528
+ .option('--comb-offset <n>', 'Comb sampling: stride between questions (for sample-comb variant)', parseInt)
529
+ .option('--start-offset <n>', 'Comb sampling: starting index (for sample-comb variant)', parseInt)
530
+ .option('--no-fixed', 'Skip improved/fixed question evaluation')
531
+ .option(
532
+ '--from-failures [path]',
533
+ 'Re-run only failed questions from a previous run (path to failures.json or "latest")',
534
+ )
535
+ .option(
536
+ '--older-than <duration>',
537
+ 'With --from-failures: only re-run questions prepared before this duration (e.g., "1h", "30m", "2d")',
538
+ )
539
+ .option('--resume [run-id]', 'Resume a partial run (omit run-id to auto-detect most recent)')
540
+ // Legacy options for backwards compatibility
541
+ .option('-d, --dataset <dataset>', 'Dataset to use (legacy)')
542
+ .option('-c, --memory-config <config>', 'Memory configuration (legacy)')
543
+ .action(async (config, options) => {
220
544
  try {
545
+ // If no config provided, show help
546
+ if (!config && !options.dataset) {
547
+ showAvailableOptions();
548
+ process.exit(0);
549
+ }
550
+
551
+ // Resolve variant and config (support legacy options)
552
+ let resolvedVariant: ReturnType<typeof getRunVariant>;
553
+ let resolvedConfig: string;
554
+ let dataset: string;
555
+
556
+ if (options.dataset && options.memoryConfig) {
557
+ // Legacy mode: use -d and -c flags
558
+ dataset = options.dataset;
559
+ resolvedConfig = options.memoryConfig;
560
+ resolvedVariant = {
561
+ name: 'custom',
562
+ description: 'Custom run',
563
+ dataset: dataset as any,
564
+ subset: options.subset,
565
+ prepareConcurrency: options.concurrency ?? 4,
566
+ benchConcurrency: options.concurrency ?? 10,
567
+ };
568
+ } else if (config) {
569
+ // New mode: config as positional, variant as flag (default: quick)
570
+ resolvedVariant = getRunVariant(options.variant);
571
+ resolvedConfig = resolveConfigAlias(config);
572
+ dataset = resolvedVariant.dataset;
573
+ } else {
574
+ console.error(chalk.red('Error: Please provide a <config>'));
575
+ console.error(chalk.gray('Run without arguments to see available options'));
576
+ process.exit(1);
577
+ }
578
+
579
+ // Apply overrides
580
+ const subset = options.subset ?? resolvedVariant.subset;
581
+ const perTypeCount = resolvedVariant.perTypeCount;
582
+ const combSampleSize = resolvedVariant.combSampleSize;
583
+ const combOffset = options.combOffset ?? resolvedVariant.combOffset;
584
+ const combStartOffset = options.startOffset ?? resolvedVariant.combStartOffset;
585
+ const concurrency = options.concurrency ?? resolvedVariant.benchConcurrency;
586
+
221
587
  console.log(chalk.blue('\n🚀 LongMemEval Benchmark Runner\n'));
588
+ console.log(chalk.gray(`Variant: ${resolvedVariant.name}`));
589
+ console.log(chalk.gray(`Dataset: ${dataset}`));
590
+ console.log(chalk.gray(`Memory Config: ${resolvedConfig}`));
591
+ console.log(chalk.gray(`Concurrency: ${concurrency}`));
592
+ if (combSampleSize) {
593
+ console.log(
594
+ chalk.gray(`Comb Sample: ${combSampleSize} per type (offset=${combOffset}, start=${combStartOffset})`),
595
+ );
596
+ } else if (perTypeCount) {
597
+ console.log(chalk.gray(`Stratified Sample: ${perTypeCount} per type`));
598
+ } else if (subset) {
599
+ console.log(chalk.gray(`Subset: ${subset} questions`));
600
+ }
601
+ if (options.offset) {
602
+ console.log(chalk.gray(`Offset: skipping first ${options.offset} questions`));
603
+ }
604
+ if (options.questionId) {
605
+ console.log(chalk.gray(`Question ID: ${options.questionId}`));
606
+ }
607
+ console.log();
222
608
 
223
609
  // Check for OpenAI API key
224
610
  if (!process.env.OPENAI_API_KEY) {
@@ -229,8 +615,8 @@ program
229
615
 
230
616
  // Validate dataset option
231
617
  const validDatasets = ['longmemeval_s', 'longmemeval_m', 'longmemeval_oracle'];
232
- if (!validDatasets.includes(options.dataset)) {
233
- console.error(chalk.red(`Invalid dataset: ${options.dataset}`));
618
+ if (!validDatasets.includes(dataset)) {
619
+ console.error(chalk.red(`Invalid dataset: ${dataset}`));
234
620
  console.error(chalk.gray(`Valid options: ${validDatasets.join(', ')}`));
235
621
  process.exit(1);
236
622
  }
@@ -238,14 +624,23 @@ program
238
624
  // Run benchmark using prepared data
239
625
  const runCommand = new RunCommand();
240
626
  await runCommand.run({
241
- dataset: options.dataset,
242
- memoryConfig: options.memoryConfig,
243
- model: options.model,
627
+ dataset: dataset as DatasetType,
628
+ memoryConfig: resolvedConfig as MemoryConfigType,
244
629
  preparedDataDir: options.preparedData,
245
630
  outputDir: options.output,
246
- subset: options.subset,
247
- concurrency: options.concurrency,
631
+ subset: combSampleSize || perTypeCount ? undefined : subset, // Don't use subset if using sampling
632
+ perTypeCount,
633
+ combSampleSize,
634
+ combOffset,
635
+ combStartOffset,
636
+ offset: options.offset,
637
+ concurrency,
248
638
  questionId: options.questionId,
639
+ questionType: options.type,
640
+ skipFixed: options.fixed === false, // --no-fixed sets options.fixed to false
641
+ fromFailures: options.fromFailures,
642
+ resume: options.resume,
643
+ olderThan: options.olderThan,
249
644
  });
250
645
 
251
646
  // Force exit after completion
@@ -387,14 +782,104 @@ program
387
782
  }
388
783
  });
389
784
 
785
+ // Sync command - sync improved_question/improved_answer from dataset to prepared meta.json files
786
+ program
787
+ .command('sync')
788
+ .description('Sync improved_question and improved_answer from dataset JSON to prepared meta.json files')
789
+ .requiredOption('-d, --dataset <dataset>', 'Dataset to sync from (longmemeval_s, longmemeval_m, longmemeval_oracle)')
790
+ .option('-c, --memory-config <config>', 'Memory configuration', 'working-memory')
791
+ .option('--prepared-data <dir>', 'Directory containing prepared data', './prepared-data')
792
+ .action(async options => {
793
+ try {
794
+ console.log(chalk.blue('\n🔄 Syncing Improved Questions/Answers\n'));
795
+
796
+ // Validate dataset option
797
+ const validDatasets = ['longmemeval_s', 'longmemeval_m', 'longmemeval_oracle'];
798
+ if (!validDatasets.includes(options.dataset)) {
799
+ console.error(chalk.red(`Invalid dataset: ${options.dataset}`));
800
+ console.error(chalk.gray(`Valid options: ${validDatasets.join(', ')}`));
801
+ process.exit(1);
802
+ }
803
+
804
+ const syncCommand = new SyncCommand();
805
+ await syncCommand.run({
806
+ dataset: options.dataset,
807
+ memoryConfig: options.memoryConfig,
808
+ preparedDataDir: options.preparedData,
809
+ });
810
+ } catch (error) {
811
+ console.error(chalk.red('\nError:'), error);
812
+ process.exit(1);
813
+ }
814
+ });
815
+
390
816
  // Results command - shows latest results for each memory configuration
817
+ program
818
+ .command('clean')
819
+ .description('Delete prepared data by offset/subset')
820
+ .requiredOption('-d, --dataset <name>', 'Dataset name (longmemeval_s, longmemeval_m, longmemeval_oracle)')
821
+ .option('-c, --memory-config <type>', 'Memory configuration to clean', 'observational-memory')
822
+ .option('-o, --offset <n>', 'Skip first N questions (delete from N+1 onwards)', parseInt)
823
+ .option('-s, --subset <n>', 'Only delete N questions (after offset)', parseInt)
824
+ .option('-q, --question-id <id>', 'Delete a specific question by ID')
825
+ .option('-p, --prepared-data <dir>', 'Prepared data directory', './prepared-data')
826
+ .option('--partial', 'Only delete partially prepared questions (have progress.json but no meta.json)')
827
+ .option('--dry-run', 'Show what would be deleted without actually deleting')
828
+ .action(async options => {
829
+ try {
830
+ const cleanCommand = new CleanCommand();
831
+ await cleanCommand.run({
832
+ dataset: options.dataset,
833
+ memoryConfig: options.memoryConfig,
834
+ preparedDataDir: options.preparedData,
835
+ offset: options.offset,
836
+ subset: options.subset,
837
+ questionId: options.questionId,
838
+ dryRun: options.dryRun,
839
+ partial: options.partial,
840
+ });
841
+ } catch (error) {
842
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
843
+ process.exit(1);
844
+ }
845
+ });
846
+
847
+ // Obscure thread IDs command - replace thread IDs with hashed versions to prevent LLM bias
848
+ program
849
+ .command('obscure-thread-ids')
850
+ .description('Replace thread IDs in prepared om.json files with hashed versions to prevent LLM bias')
851
+ .requiredOption('-d, --dataset <name>', 'Dataset name (longmemeval_s, longmemeval_m, longmemeval_oracle)')
852
+ .option('-c, --memory-config <type>', 'Memory configuration', 'observational-memory')
853
+ .option('-p, --prepared-data <dir>', 'Prepared data directory', './prepared-data')
854
+ .option('--dry-run', 'Show what would be changed without actually modifying files')
855
+ .action(async options => {
856
+ try {
857
+ const obscureCommand = new ObscureThreadIdsCommand();
858
+ await obscureCommand.run({
859
+ dataset: options.dataset,
860
+ memoryConfig: options.memoryConfig,
861
+ preparedDataDir: options.preparedData,
862
+ dryRun: options.dryRun,
863
+ });
864
+ } catch (error) {
865
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
866
+ process.exit(1);
867
+ }
868
+ });
869
+
391
870
  program
392
871
  .command('results')
393
872
  .description('Show latest benchmark results for each memory configuration')
394
873
  .option('-r, --results <dir>', 'Results directory', './results')
395
874
  .option('-d, --dataset <dataset>', 'Filter by dataset')
396
- .option('-a, --all', 'Show all results, not just latest')
875
+ .option('-l, --latest', 'Show only the latest result per config')
876
+ .option('--min-questions <n>', 'Minimum questions to include (default: 20)', parseInt)
877
+ .option('-s, --sort <by>', 'Sort by: date, accuracy, fixed (default: date)', 'date')
878
+ .option('--no-fixed', 'Hide fixed accuracy numbers')
397
879
  .action(async options => {
880
+ const minQuestions = options.minQuestions ?? 20;
881
+ const sortBy = options.sort ?? 'date';
882
+ const showFixed = options.fixed !== false;
398
883
  try {
399
884
  console.log(chalk.blue('\n📊 Benchmark Results Summary\n'));
400
885
 
@@ -414,6 +899,7 @@ program
414
899
  metrics: any;
415
900
  config: any;
416
901
  timestamp: string;
902
+ metricsPath: string;
417
903
  }> = [];
418
904
 
419
905
  // First, try new structure
@@ -437,11 +923,17 @@ program
437
923
  continue;
438
924
  }
439
925
 
926
+ // Filter by minimum questions
927
+ if (data.total_questions < minQuestions) {
928
+ continue;
929
+ }
930
+
440
931
  allRuns.push({
441
932
  runId: runDir,
442
933
  metrics: data,
443
934
  config: data.config,
444
935
  timestamp: data.timestamp,
936
+ metricsPath,
445
937
  });
446
938
  } catch (error) {
447
939
  // Skip runs with missing or invalid metrics
@@ -465,11 +957,17 @@ program
465
957
  continue;
466
958
  }
467
959
 
960
+ // Filter by minimum questions
961
+ if (data.total_questions < minQuestions) {
962
+ continue;
963
+ }
964
+
468
965
  allRuns.push({
469
966
  runId: runDir,
470
967
  metrics: data,
471
968
  config: data.config,
472
969
  timestamp: data.timestamp,
970
+ metricsPath,
473
971
  });
474
972
  } catch (error) {
475
973
  // Skip runs with missing or invalid metrics
@@ -481,106 +979,175 @@ program
481
979
  return;
482
980
  }
483
981
 
484
- // Group by memory configuration
485
- const byMemoryConfig = new Map<string, typeof allRuns>();
486
- for (const run of allRuns) {
487
- const key = `${run.config.dataset}_${run.config.memoryConfig}`;
488
- if (!byMemoryConfig.has(key)) {
489
- byMemoryConfig.set(key, []);
982
+ // Sort all runs based on sortBy option (best/newest at bottom for terminal viewing)
983
+ let runsToShow = [...allRuns];
984
+
985
+ if (options.latest) {
986
+ // Group by config and take only the latest from each
987
+ const byConfig = new Map<string, (typeof allRuns)[0]>();
988
+ for (const run of allRuns) {
989
+ const key = `${run.config.dataset}_${run.config.memoryConfig}`;
990
+ const existing = byConfig.get(key);
991
+ if (!existing || run.timestamp > existing.timestamp) {
992
+ byConfig.set(key, run);
993
+ }
490
994
  }
491
- byMemoryConfig.get(key)!.push(run);
995
+ runsToShow = Array.from(byConfig.values());
492
996
  }
493
997
 
494
- // Sort groups by worst performing to best performing (based on latest run)
495
- const sortedConfigs = Array.from(byMemoryConfig.entries()).sort(([_aKey, aRuns], [_bKey, bRuns]) => {
496
- // Get latest run for each config (already sorted by timestamp)
497
- const aLatest = aRuns.sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
498
- const bLatest = bRuns.sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
499
-
500
- // Sort by overall accuracy (worst first)
501
- return aLatest.metrics.overall_accuracy - bLatest.metrics.overall_accuracy;
998
+ // Sort runs (oldest/worst first, newest/best at bottom for terminal viewing)
999
+ runsToShow.sort((a, b) => {
1000
+ if (sortBy === 'date') {
1001
+ // Sort by date (oldest first, newest at bottom)
1002
+ return a.timestamp.localeCompare(b.timestamp);
1003
+ } else if (sortBy === 'fixed') {
1004
+ // Sort by fixed accuracy (worst first, best at bottom), fall back to vanilla if no fixed
1005
+ const aFixed = a.metrics.fixed_overall_accuracy ?? a.metrics.overall_accuracy;
1006
+ const bFixed = b.metrics.fixed_overall_accuracy ?? b.metrics.overall_accuracy;
1007
+ return aFixed - bFixed;
1008
+ } else {
1009
+ // Sort by vanilla accuracy (worst first, best at bottom)
1010
+ return a.metrics.overall_accuracy - b.metrics.overall_accuracy;
1011
+ }
502
1012
  });
503
1013
 
504
- for (const [_configKey, runs] of sortedConfigs) {
505
- // Sort runs by timestamp (newest first)
506
- runs.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
507
-
508
- // Show latest or all
509
- const runsToShow = options.all ? runs : [runs[0]];
510
-
511
- for (const run of runsToShow) {
512
- // Get terminal width, default to 80 if not available
513
- const terminalWidth = process.stdout.columns || 80;
514
- const lineWidth = Math.min(terminalWidth - 1, 80); // Cap at 80 for readability
515
-
516
- console.log(chalk.bold('\n' + '═'.repeat(lineWidth) + '\n'));
517
-
518
- // Configuration header
519
- console.log(chalk.bold('Configuration:\n'));
520
- console.log(chalk.gray('Dataset:'), chalk.cyan(run.config.dataset));
521
- console.log(chalk.gray('Model:'), chalk.cyan(run.config.model));
522
- console.log(chalk.gray('Memory Config:'), chalk.cyan(run.config.memoryConfig));
523
- if (run.config.subset) {
524
- console.log(chalk.gray('Subset:'), chalk.cyan(`${run.config.subset} questions`));
525
- }
526
- console.log(chalk.gray('Run ID:'), chalk.dim(run.runId));
527
- console.log(chalk.gray('Timestamp:'), chalk.dim(new Date(run.timestamp).toLocaleString()));
528
- console.log(chalk.gray('─'.repeat(Math.min(lineWidth, 60))));
529
-
530
- // Display metrics using same format as regular runs
531
- const metrics = run.metrics;
1014
+ for (const run of runsToShow) {
1015
+ // Get terminal width, default to 80 if not available
1016
+ const terminalWidth = process.stdout.columns || 80;
1017
+ const lineWidth = Math.min(terminalWidth - 1, 80); // Cap at 80 for readability
532
1018
 
533
- // Recalculate overall accuracy using the new formula (average of type averages)
534
- const typeAccuracies = Object.values(metrics.accuracy_by_type).map((t: any) => t.accuracy);
535
- const recalculatedOverall =
536
- typeAccuracies.length > 0 ? typeAccuracies.reduce((sum, acc) => sum + acc, 0) / typeAccuracies.length : 0;
537
- metrics.overall_accuracy = recalculatedOverall;
1019
+ console.log(chalk.bold('\n' + '═'.repeat(lineWidth) + '\n'));
538
1020
 
539
- // Question type breakdown
1021
+ // Configuration header
1022
+ console.log(chalk.bold('Configuration:\n'));
1023
+ console.log(chalk.gray('Dataset:'), chalk.cyan(run.config.dataset));
1024
+ console.log(chalk.gray('Model:'), chalk.cyan(run.config.model));
1025
+ console.log(chalk.gray('Memory Config:'), chalk.cyan(run.config.memoryConfig));
1026
+ if (run.config.subset) {
1027
+ console.log(chalk.gray('Subset:'), chalk.cyan(`${run.config.subset} questions`));
1028
+ }
1029
+ console.log(chalk.gray('Run ID:'), chalk.dim(run.runId));
1030
+ console.log(chalk.gray('Timestamp:'), chalk.dim(new Date(run.timestamp).toLocaleString()));
1031
+ // Make path relative to cwd
1032
+ const relativePath = require('path').relative(process.cwd(), require('path').resolve(run.metricsPath));
1033
+ console.log(chalk.gray('Metrics:'), chalk.blue(relativePath));
1034
+ console.log(chalk.gray('─'.repeat(Math.min(lineWidth, 60))));
1035
+
1036
+ // Display metrics using same format as regular runs
1037
+ const metrics = run.metrics;
1038
+
1039
+ // Recalculate overall accuracy using the new formula (average of type averages)
1040
+ const typeAccuracies = Object.values(metrics.accuracy_by_type).map((t: any) => t.accuracy);
1041
+ const recalculatedOverall =
1042
+ typeAccuracies.length > 0 ? typeAccuracies.reduce((sum, acc) => sum + acc, 0) / typeAccuracies.length : 0;
1043
+ metrics.overall_accuracy = recalculatedOverall;
1044
+
1045
+ // Check if fixed accuracy data exists
1046
+ const hasFixedAccuracy =
1047
+ showFixed && metrics.fixed_accuracy_by_type && Object.keys(metrics.fixed_accuracy_by_type).length > 0;
1048
+
1049
+ // Question type breakdown
1050
+ if (hasFixedAccuracy) {
1051
+ console.log(chalk.bold('\nAccuracy by Question Type:'), chalk.gray('(vanilla → fixed)'));
1052
+ } else {
540
1053
  console.log(chalk.bold('\nAccuracy by Question Type:'));
1054
+ }
541
1055
 
542
- // Sort question types alphabetically
543
- const sortedTypes = Object.entries(metrics.accuracy_by_type).sort(([a], [b]) => a.localeCompare(b));
1056
+ // Sort question types alphabetically
1057
+ const sortedTypes = Object.entries(metrics.accuracy_by_type).sort(([a], [b]) => a.localeCompare(b));
544
1058
 
545
- for (const [type, typeMetrics] of sortedTypes) {
546
- const { correct, total, accuracy } = typeMetrics as any;
547
- const typeColor = accuracy >= 0.8 ? 'green' : accuracy >= 0.6 ? 'yellow' : 'red';
1059
+ for (const [type, typeMetrics] of sortedTypes) {
1060
+ const { correct, total, accuracy } = typeMetrics as any;
1061
+ const typeColor = accuracy >= 0.8 ? 'green' : accuracy >= 0.6 ? 'yellow' : 'red';
548
1062
 
549
- // Create a simple progress bar
550
- const barLength = 20;
551
- const filledLength = Math.round(accuracy * barLength);
552
- const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
1063
+ // Create a simple progress bar
1064
+ const barLength = 20;
1065
+ const filledLength = Math.round(accuracy * barLength);
1066
+ const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
553
1067
 
554
- console.log(
555
- chalk.gray(` ${type.padEnd(25)}:`),
556
- chalk[typeColor](`${(accuracy * 100).toFixed(1).padStart(5)}%`),
557
- chalk.gray(`[${bar}]`),
558
- chalk.gray(`(${correct}/${total})`),
559
- );
1068
+ let fixedPart = '';
1069
+ if (hasFixedAccuracy && metrics.fixed_accuracy_by_type[type]) {
1070
+ const fixedMetrics = metrics.fixed_accuracy_by_type[type] as any;
1071
+ const fixedColor = fixedMetrics.accuracy >= 0.8 ? 'green' : fixedMetrics.accuracy >= 0.6 ? 'yellow' : 'red';
1072
+ fixedPart = chalk.gray(' → ') + chalk[fixedColor](`${(fixedMetrics.accuracy * 100).toFixed(1)}%`);
560
1073
  }
561
1074
 
562
- // Abstention is hidden - it tests LLM reasoning ability rather than memory system performance
1075
+ console.log(
1076
+ chalk.gray(` ${type.padEnd(25)}:`),
1077
+ chalk[typeColor](`${(accuracy * 100).toFixed(1).padStart(5)}%`) + fixedPart,
1078
+ chalk.gray(`[${bar}]`),
1079
+ chalk.gray(`(${correct}/${total})`),
1080
+ );
1081
+ }
563
1082
 
564
- // Overall summary at the bottom
565
- console.log();
566
- const accuracyColor =
567
- metrics.overall_accuracy >= 0.8 ? 'green' : metrics.overall_accuracy >= 0.6 ? 'yellow' : 'red';
1083
+ // Abstention is hidden - it tests LLM reasoning ability rather than memory system performance
1084
+
1085
+ // Overall summary at the bottom
1086
+ console.log();
1087
+ const accuracyColor =
1088
+ metrics.overall_accuracy >= 0.8 ? 'green' : metrics.overall_accuracy >= 0.6 ? 'yellow' : 'red';
1089
+
1090
+ if (hasFixedAccuracy && metrics.fixed_overall_accuracy != null) {
1091
+ const fixedOverallColor =
1092
+ metrics.fixed_overall_accuracy >= 0.8 ? 'green' : metrics.fixed_overall_accuracy >= 0.6 ? 'yellow' : 'red';
1093
+ console.log(
1094
+ chalk.bold('Overall Accuracy:'),
1095
+ chalk[accuracyColor](`${(metrics.overall_accuracy * 100).toFixed(2)}%`),
1096
+ chalk.gray('→'),
1097
+ chalk[fixedOverallColor](`${(metrics.fixed_overall_accuracy * 100).toFixed(2)}%`),
1098
+ chalk.gray('(fixed)'),
1099
+ );
1100
+ } else {
568
1101
  console.log(
569
1102
  chalk.bold('Overall Accuracy:'),
570
1103
  chalk[accuracyColor](`${(metrics.overall_accuracy * 100).toFixed(2)}%`),
571
1104
  chalk.gray(`(average of ${Object.keys(metrics.accuracy_by_type).length} question types)`),
572
1105
  );
573
1106
  }
1107
+
1108
+ // Token usage summary (if available)
1109
+ const formatTokens = (n: number) => n.toLocaleString();
1110
+
1111
+ if (metrics.total_usage) {
1112
+ console.log(
1113
+ chalk.gray('Answering Tokens:'),
1114
+ chalk.cyan(formatTokens(metrics.total_usage.inputTokens)),
1115
+ chalk.gray('input,'),
1116
+ chalk.cyan(formatTokens(metrics.total_usage.outputTokens)),
1117
+ chalk.gray('output'),
1118
+ chalk.gray(
1119
+ `(avg ${formatTokens(Math.round(metrics.total_usage.inputTokens / metrics.total_questions))}/q)`,
1120
+ ),
1121
+ );
1122
+ }
1123
+
1124
+ // Load preparation token usage from om-debug.jsonl files
1125
+ const prepUsage = await loadPreparationTokenUsage('./prepared-data', run.config.memoryConfig);
1126
+ if (prepUsage && prepUsage.totalInputTokens > 0) {
1127
+ console.log(
1128
+ chalk.gray('Preparation Tokens:'),
1129
+ chalk.cyan(formatTokens(prepUsage.totalInputTokens)),
1130
+ chalk.gray('input,'),
1131
+ chalk.cyan(formatTokens(prepUsage.totalOutputTokens)),
1132
+ chalk.gray('output'),
1133
+ chalk.gray(
1134
+ `(Observer: ${formatTokens(prepUsage.observerInputTokens)}, Reflector: ${formatTokens(prepUsage.reflectorInputTokens)})`,
1135
+ ),
1136
+ );
1137
+ }
574
1138
  }
575
1139
 
576
1140
  // Get terminal width for final separator
577
1141
  const terminalWidth = process.stdout.columns || 80;
578
1142
  const lineWidth = Math.min(terminalWidth - 1, 80);
579
1143
 
1144
+ // Count unique configurations
1145
+ const uniqueConfigs = new Set(allRuns.map(r => `${r.config.dataset}_${r.config.memoryConfig}`));
1146
+
580
1147
  console.log(chalk.bold('\n' + '═'.repeat(lineWidth)));
581
- console.log(chalk.gray(`\nFound ${allRuns.length} total runs across ${byMemoryConfig.size} configurations`));
582
- if (!options.all && byMemoryConfig.size > 0) {
583
- console.log(chalk.gray('Use --all to see all runs, not just the latest'));
1148
+ console.log(chalk.gray(`\nFound ${allRuns.length} total runs across ${uniqueConfigs.size} configurations`));
1149
+ if (!options.latest && uniqueConfigs.size > 0 && allRuns.length > uniqueConfigs.size) {
1150
+ console.log(chalk.gray('Use --latest to see only the latest run per config'));
584
1151
  }
585
1152
  } catch (error) {
586
1153
  console.error(chalk.red('\nError:'), error);
@@ -670,8 +1237,8 @@ async function ensureDatasetExists(dataset: string) {
670
1237
  console.log(chalk.blue('Downloading dataset...\n'));
671
1238
 
672
1239
  try {
673
- // Run the download script
674
- execSync('pnpm download', { stdio: 'inherit' });
1240
+ // Run the download script with specific dataset
1241
+ execSync(`pnpm download -- --dataset ${dataset}`, { stdio: 'inherit' });
675
1242
 
676
1243
  // Verify download succeeded
677
1244
  if (!existsSync(datasetPath) || statSync(datasetPath).size < 1000000) {
@@ -687,4 +1254,249 @@ async function ensureDatasetExists(dataset: string) {
687
1254
  }
688
1255
  }
689
1256
 
1257
+ // Sessions command - browse answer sessions for a question
1258
+ program
1259
+ .command('sessions')
1260
+ .description('Browse answer sessions for a specific question ID')
1261
+ .requiredOption('-d, --dataset <dataset>', 'Dataset to use (longmemeval_s, longmemeval_m, longmemeval_oracle)')
1262
+ .requiredOption('-q, --question-id <id>', 'Question ID to browse')
1263
+ .option('-a, --all', 'Show all haystack sessions, not just answer sessions')
1264
+ .action(async options => {
1265
+ try {
1266
+ const sessionsCommand = new SessionsCommand();
1267
+ await sessionsCommand.run({
1268
+ dataset: options.dataset,
1269
+ questionId: options.questionId,
1270
+ showAll: options.all,
1271
+ });
1272
+ } catch (error) {
1273
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1274
+ process.exit(1);
1275
+ }
1276
+ });
1277
+
1278
+ // Deterministic IDs command - update message IDs to be deterministic
1279
+ program
1280
+ .command('deterministic-ids')
1281
+ .description('Update message IDs in prepared data to be deterministic (thread_id_msg_index)')
1282
+ .option('--prepared-data-dir <dir>', 'Directory containing prepared data')
1283
+ .option('-q, --question-id <id>', 'Only update a specific question')
1284
+ .action(async options => {
1285
+ try {
1286
+ const command = new DeterministicIdsCommand({
1287
+ preparedDataDir: options.preparedDataDir,
1288
+ questionId: options.questionId,
1289
+ });
1290
+ await command.run();
1291
+ } catch (error) {
1292
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1293
+ process.exit(1);
1294
+ }
1295
+ });
1296
+
1297
+ // List partial command - find partially prepared questions
1298
+ program
1299
+ .command('list-partial')
1300
+ .description('List partially prepared or failed questions')
1301
+ .option('--prepared-data-dir <dir>', 'Directory containing prepared data')
1302
+ .action(async options => {
1303
+ try {
1304
+ const command = new ListPartialCommand({
1305
+ preparedDataDir: options.preparedDataDir,
1306
+ });
1307
+ await command.run();
1308
+ } catch (error) {
1309
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1310
+ process.exit(1);
1311
+ }
1312
+ });
1313
+
1314
+ // Tokens command - estimate token counts for questions
1315
+ program
1316
+ .command('tokens')
1317
+ .description('Estimate token counts for LongMemEval questions')
1318
+ .requiredOption('-d, --dataset <dataset>', 'Dataset to use (longmemeval_s, longmemeval_m, longmemeval_oracle)')
1319
+ .option('-q, --question-id <id>', 'Analyze a specific question by ID')
1320
+ .option('-o, --offset <n>', 'Skip first n questions', parseInt)
1321
+ .option('-s, --subset <n>', 'Analyze only n questions (after offset)', parseInt)
1322
+ .option('-p, --prepared-data <dir>', 'Directory containing prepared data', './prepared-data')
1323
+ .option('--sessions', 'Show per-session breakdown')
1324
+ .option('--top <n>', 'Show top N largest questions in aggregate view', parseInt)
1325
+ .option('--observations-only', 'Only count observation tokens from prepared data (fast)')
1326
+ .option('-c, --config <config>', 'Config to use for observations-only mode', 'observational-memory')
1327
+ .action(async options => {
1328
+ try {
1329
+ const tokensCommand = new TokensCommand();
1330
+ await tokensCommand.run({
1331
+ dataset: options.dataset,
1332
+ questionId: options.questionId,
1333
+ offset: options.offset,
1334
+ subset: options.subset,
1335
+ preparedDataDir: options.preparedData,
1336
+ showSessions: options.sessions,
1337
+ topN: options.top,
1338
+ observationsOnly: options.observationsOnly,
1339
+ config: options.config,
1340
+ });
1341
+ } catch (error) {
1342
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1343
+ process.exit(1);
1344
+ }
1345
+ });
1346
+
1347
+ program
1348
+ .command('precompute-embeddings')
1349
+ .description('Precompute embeddings for RAG-based observation filtering')
1350
+ .requiredOption('-d, --dataset <dataset>', 'Dataset to use (longmemeval_s, longmemeval_m, longmemeval_oracle)')
1351
+ .requiredOption('-c, --memory-config <config>', 'Memory configuration to use')
1352
+ .option('-o, --offset <n>', 'Skip first n questions', parseInt)
1353
+ .option('-s, --subset <n>', 'Process only n questions (after offset)', parseInt)
1354
+ .option('-p, --prepared-data <dir>', 'Directory containing prepared data', './prepared-data')
1355
+ .option('-b, --batch-size <n>', 'Batch size for embedding (default: 100)', parseInt)
1356
+ .option('--cooldown <ms>', 'Cooldown in ms between questions (default: 1000)', parseInt)
1357
+ .action(async options => {
1358
+ try {
1359
+ const command = new PrecomputeEmbeddingsCommand();
1360
+ await command.run({
1361
+ dataset: options.dataset,
1362
+ memoryConfig: options.memoryConfig,
1363
+ preparedDataDir: options.preparedData,
1364
+ offset: options.offset,
1365
+ subset: options.subset,
1366
+ batchSize: options.batchSize,
1367
+ cooldown: options.cooldown,
1368
+ });
1369
+ } catch (error) {
1370
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1371
+ process.exit(1);
1372
+ }
1373
+ });
1374
+
1375
+ program
1376
+ .command('find-prohibited')
1377
+ .description('Find messages that trigger Gemini PROHIBITED_CONTENT filter using binary search')
1378
+ .requiredOption('-d, --dataset <dataset>', 'Dataset to use (longmemeval_s, longmemeval_m, longmemeval_oracle)')
1379
+ .requiredOption('-q, --question-id <id>', 'Question ID to analyze')
1380
+ .action(async options => {
1381
+ try {
1382
+ const command = new FindProhibitedCommand();
1383
+ await command.run({
1384
+ dataset: options.dataset,
1385
+ questionId: options.questionId,
1386
+ });
1387
+ } catch (error) {
1388
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1389
+ process.exit(1);
1390
+ }
1391
+ });
1392
+
1393
+ program
1394
+ .command('partial [config]')
1395
+ .description('Check partial or complete benchmark results')
1396
+ .option('-o, --output <dir>', 'Results directory', './results')
1397
+ .option('--run-id <id>', 'Specific run ID to check')
1398
+ .action(async (config, options) => {
1399
+ try {
1400
+ const command = new PartialResultsCommand();
1401
+ await command.run({
1402
+ config,
1403
+ runId: options.runId,
1404
+ outputDir: options.output,
1405
+ });
1406
+ } catch (error) {
1407
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1408
+ process.exit(1);
1409
+ }
1410
+ });
1411
+
1412
+ // Investigate command
1413
+ import { InvestigateCommand } from './commands/investigate';
1414
+
1415
+ program
1416
+ .command('investigate [run-id]')
1417
+ .description('Setup and manage investigation of failed questions')
1418
+ .option('-l, --list', 'List all runs with failures')
1419
+ .option('-c, --config <config>', 'Filter by config name (used with --list)')
1420
+ .option('--status', 'Show investigation status')
1421
+ .option('--next', 'Open next uninvestigated question')
1422
+ .option('--done <question-id>', 'Mark a question as investigated')
1423
+ .option('--fixed <question-id>', 'Mark a question as fix-implemented')
1424
+ .option('--sync', 'Sync fixes to longmemeval_s.json')
1425
+ .option('-o, --output <dir>', 'Investigation output directory', './investigations')
1426
+ .option('-r, --results <dir>', 'Results directory', './results')
1427
+ .option('-p, --prepared-data <dir>', 'Prepared data directory', './prepared-data')
1428
+ .option('--data <dir>', 'Dataset directory', './data')
1429
+ .option('--editor <cmd>', 'Editor command to open files', process.env.EDITOR || 'code')
1430
+ .option('--inspect <question-id>', "Inspect a question's data")
1431
+ .option('--search <keyword>', 'Search observations for a keyword')
1432
+ .option('--trace <keyword>', 'Trace information flow for a keyword')
1433
+ .option('-q, --question-id <id>', 'Question ID for search/trace/date')
1434
+ .option('--date <date>', 'View observations around a date (e.g., "2023/05/29", "May 29")')
1435
+ .option('--context <days>', 'Days of context around --date (default: 1)', '1')
1436
+ .option('--session <idx>', 'View a specific session from original dataset')
1437
+ .option('--list-sessions', 'List all sessions with dates for a question')
1438
+ .option('--check-stale', 'Check if prepared data is stale (pre-cursor-fix)')
1439
+ .option('--stale-only', 'Only list stale questions (use with --check-stale)')
1440
+ .option('--check-duplicates', 'Check for duplicate thread blocks in observations')
1441
+ .option('--baseline', 'Run comprehensive data quality check for a question')
1442
+ .option('--search-original <keyword>', 'Search original dataset for a keyword (shows full context)')
1443
+ .option('--improve <question-id>', 'Add improvements to a question in the dataset')
1444
+ .option('--improve-question <text>', 'Improved question text (use with --improve)')
1445
+ .option('--improve-answer <text>', 'Improved answer text (use with --improve)')
1446
+ .option('--improve-note <text>', 'Improvement note (use with --improve)')
1447
+ .option(
1448
+ '--category <category>',
1449
+ 'Failure category: observer-miss, reflector-loss, agent-reasoning, dataset-error, data-freshness, knowledge-update, rag-miss, other',
1450
+ )
1451
+ .option('--clear-improved [fields]', 'Clear improved fields: all, question, answer, note, category (comma-separated)')
1452
+ .option('--prepare-stale', '(deprecated) Use --print-prepare-command instead')
1453
+ .option('--print-prepare-command', 'Print a prepare command for stale/partial questions')
1454
+ .option('--dry-run', 'Show what would be prepared without actually doing it')
1455
+ .action(async (runId, options) => {
1456
+ try {
1457
+ const command = new InvestigateCommand({
1458
+ outputDir: options.output,
1459
+ resultsDir: options.results,
1460
+ preparedDataDir: options.preparedData,
1461
+ datasetDir: options.data,
1462
+ editor: options.editor,
1463
+ });
1464
+ await command.run({
1465
+ runId,
1466
+ list: options.list,
1467
+ config: options.config,
1468
+ status: options.status,
1469
+ next: options.next,
1470
+ done: options.done,
1471
+ fixed: options.fixed,
1472
+ sync: options.sync,
1473
+ inspect: options.inspect,
1474
+ search: options.search,
1475
+ trace: options.trace,
1476
+ questionId: options.questionId,
1477
+ date: options.date,
1478
+ context: options.context ? parseInt(options.context, 10) : 1,
1479
+ session: options.session !== undefined ? parseInt(options.session, 10) : undefined,
1480
+ listSessions: options.listSessions,
1481
+ checkStale: options.checkStale,
1482
+ staleOnly: options.staleOnly,
1483
+ checkDuplicates: options.checkDuplicates,
1484
+ baseline: options.baseline,
1485
+ searchOriginal: options.searchOriginal,
1486
+ improve: options.improve,
1487
+ improveQuestion: options.improveQuestion,
1488
+ improveAnswer: options.improveAnswer,
1489
+ improveNote: options.improveNote,
1490
+ category: options.category,
1491
+ clearImproved: options.clearImproved,
1492
+ prepareStale: options.prepareStale,
1493
+ printPrepareCommand: options.printPrepareCommand,
1494
+ dryRun: options.dryRun,
1495
+ });
1496
+ } catch (error) {
1497
+ console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
1498
+ process.exit(1);
1499
+ }
1500
+ });
1501
+
690
1502
  program.parse();