codemem 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, applyBootstrapSnapshot, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getWorkspaceCodememConfigPath, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
2
+ import { DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadObserverConfig, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -242,7 +242,7 @@ function envNotDisabled(value) {
242
242
  const normalized = String(value ?? "").trim().toLowerCase();
243
243
  return normalized !== "0" && normalized !== "false" && normalized !== "off";
244
244
  }
245
- function parsePositiveInt(value, fallback) {
245
+ function parsePositiveInt$1(value, fallback) {
246
246
  const parsed = Number.parseInt(String(value ?? ""), 10);
247
247
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
248
248
  return parsed;
@@ -269,8 +269,8 @@ function resolveInjectProject(payload) {
269
269
  async function buildLocalPack(context, project, dbPath) {
270
270
  const store = new MemoryStore(dbPath);
271
271
  try {
272
- const limit = parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8);
273
- const budget = parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
272
+ const limit = parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8);
273
+ const budget = parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
274
274
  const filters = {};
275
275
  if (project) filters.project = project;
276
276
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
@@ -281,11 +281,11 @@ async function buildLocalPack(context, project, dbPath) {
281
281
  }
282
282
  async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
283
283
  const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
284
- const port = parsePositiveInt(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
284
+ const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
285
285
  const url = new URL(`http://${host}:${port}/api/pack`);
286
286
  url.searchParams.set("context", context);
287
- url.searchParams.set("limit", String(parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8)));
288
- url.searchParams.set("token_budget", String(parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
287
+ url.searchParams.set("limit", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8)));
288
+ url.searchParams.set("token_budget", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
289
289
  if (project) url.searchParams.set("project", project);
290
290
  const controller = new AbortController();
291
291
  const timeout = setTimeout(() => controller.abort(), maxTimeMs);
@@ -308,8 +308,8 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
308
308
  const httpPack = deps.httpPack ?? tryHttpPack;
309
309
  const resolveDb = deps.resolveDb ?? resolveDbPath;
310
310
  const project = resolveInjectProject(payload);
311
- const maxChars = parsePositiveInt(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
312
- const httpMaxTimeMs = parsePositiveInt(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
311
+ const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
312
+ const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
313
313
  let additionalContext = "";
314
314
  try {
315
315
  additionalContext = await buildPack(context, project, resolveDb(resolveDbOpt(opts)));
@@ -1361,6 +1361,119 @@ pruneMemCmd.action((opts) => {
1361
1361
  }
1362
1362
  });
1363
1363
  dbCommand.addCommand(pruneMemCmd);
1364
+ var dedupCmd = new Command("dedup-memories").configureHelp(helpStyle).description("Deactivate near-duplicate memories (cross-session, same normalized title within time window)").option("--window <ms>", "max time gap in milliseconds between duplicates (default: 3600000)").option("--limit <n>", "max pairs to check").option("--dry-run", "preview deactivations without writing");
1365
+ addDbOption(dedupCmd);
1366
+ addJsonOption(dedupCmd);
1367
+ dedupCmd.action((opts) => {
1368
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1369
+ try {
1370
+ const windowMs = parseOptionalPositiveInt$1(opts.window);
1371
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1372
+ const result = dedupNearDuplicateMemories(store.db, {
1373
+ windowMs,
1374
+ limit,
1375
+ dryRun: opts.dryRun === true
1376
+ });
1377
+ if (opts.json) {
1378
+ console.log(JSON.stringify(result, null, 2));
1379
+ return;
1380
+ }
1381
+ const action = opts.dryRun ? "Would deactivate" : "Deactivated";
1382
+ p.intro("codemem db dedup-memories");
1383
+ if (result.pairs.length > 0 && result.pairs.length <= 20) for (const pair of result.pairs) p.log.info(`${action} [${pair.deactivated_id}] (kept [${pair.kept_id}]): ${pair.title.slice(0, 80)}`);
1384
+ p.outro(`${action} ${result.deactivated} duplicates from ${result.checked} pairs`);
1385
+ } catch (error) {
1386
+ p.log.error(error instanceof Error ? error.message : String(error));
1387
+ process.exitCode = 1;
1388
+ } finally {
1389
+ store.close();
1390
+ }
1391
+ });
1392
+ dbCommand.addCommand(dedupCmd);
1393
+ var backfillDedupKeysCmd = new Command("backfill-dedup-keys").configureHelp(helpStyle).description("Populate missing memory_items.dedup_key values for legacy rows").option("--limit <n>", "max memories to check").option("--dry-run", "preview updates without writing");
1394
+ addDbOption(backfillDedupKeysCmd);
1395
+ addJsonOption(backfillDedupKeysCmd);
1396
+ backfillDedupKeysCmd.action((opts) => {
1397
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1398
+ try {
1399
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1400
+ const result = backfillMemoryDedupKeys(store.db, {
1401
+ limit,
1402
+ dryRun: opts.dryRun === true
1403
+ });
1404
+ if (opts.json) {
1405
+ console.log(JSON.stringify(result, null, 2));
1406
+ return;
1407
+ }
1408
+ const action = opts.dryRun ? "Would update" : "Updated";
1409
+ p.intro("codemem db backfill-dedup-keys");
1410
+ p.log.success(`${action} ${result.updated} memories (skipped ${result.skipped})`);
1411
+ p.outro(`Checked ${result.checked} memories`);
1412
+ } catch (error) {
1413
+ p.log.error(error instanceof Error ? error.message : String(error));
1414
+ process.exitCode = 1;
1415
+ } finally {
1416
+ store.close();
1417
+ }
1418
+ });
1419
+ dbCommand.addCommand(backfillDedupKeysCmd);
1420
+ var backfillNarrativeCmd = new Command("backfill-narrative").configureHelp(helpStyle).description("Extract narrative from session_summary body_text (## Completed / ## Learned sections)").option("--limit <n>", "max memories to check").option("--dry-run", "preview updates without writing");
1421
+ addDbOption(backfillNarrativeCmd);
1422
+ addJsonOption(backfillNarrativeCmd);
1423
+ backfillNarrativeCmd.action((opts) => {
1424
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1425
+ try {
1426
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1427
+ const result = backfillNarrativeFromBody(store.db, {
1428
+ limit,
1429
+ dryRun: opts.dryRun === true
1430
+ });
1431
+ if (opts.json) {
1432
+ console.log(JSON.stringify(result, null, 2));
1433
+ return;
1434
+ }
1435
+ const action = opts.dryRun ? "Would update" : "Updated";
1436
+ p.intro("codemem db backfill-narrative");
1437
+ p.log.success(`${action} ${result.updated} memories (skipped ${result.skipped})`);
1438
+ p.outro(`Checked ${result.checked} memories`);
1439
+ } catch (error) {
1440
+ p.log.error(error instanceof Error ? error.message : String(error));
1441
+ process.exitCode = 1;
1442
+ } finally {
1443
+ store.close();
1444
+ }
1445
+ });
1446
+ dbCommand.addCommand(backfillNarrativeCmd);
1447
+ var aiBackfillStructuredCmd = new Command("ai-backfill-structured").configureHelp(helpStyle).description("Use GPT-5.4 to populate missing narrative/facts/concepts for older non-session-summary memories").option("--limit <n>", "max memories to check").option("--kinds <csv>", "comma-separated kinds to target").option("--overwrite", "overwrite existing structured fields instead of only filling missing ones").option("--dry-run", "preview updates without writing");
1448
+ addDbOption(aiBackfillStructuredCmd);
1449
+ addJsonOption(aiBackfillStructuredCmd);
1450
+ aiBackfillStructuredCmd.action(async (opts) => {
1451
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1452
+ try {
1453
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1454
+ const kinds = parseKindsCsv(opts.kinds);
1455
+ const result = await aiBackfillStructuredContent(store.db, {
1456
+ limit,
1457
+ kinds,
1458
+ overwrite: opts.overwrite === true,
1459
+ dryRun: opts.dryRun === true
1460
+ });
1461
+ if (opts.json) {
1462
+ console.log(JSON.stringify(result, null, 2));
1463
+ return;
1464
+ }
1465
+ const action = opts.dryRun ? "Would update" : "Updated";
1466
+ p.intro("codemem db ai-backfill-structured");
1467
+ p.log.success(`${action} ${result.updated} memories (skipped ${result.skipped}, failed ${result.failed})`);
1468
+ p.outro(`Checked ${result.checked} memories`);
1469
+ } catch (error) {
1470
+ p.log.error(error instanceof Error ? error.message : String(error));
1471
+ process.exitCode = 1;
1472
+ } finally {
1473
+ store.close();
1474
+ }
1475
+ });
1476
+ dbCommand.addCommand(aiBackfillStructuredCmd);
1364
1477
  //#endregion
1365
1478
  //#region src/commands/embed.ts
1366
1479
  function parseOptionalPositiveInt(value) {
@@ -1601,13 +1714,22 @@ var mcpCommand = mcpCmd.action(async (opts) => {
1601
1714
  });
1602
1715
  //#endregion
1603
1716
  //#region src/commands/pack-shared.ts
1717
+ var PackUsageError = class extends Error {
1718
+ constructor(message) {
1719
+ super(message);
1720
+ this.name = "PackUsageError";
1721
+ }
1722
+ };
1604
1723
  function collectWorkingSetFile(value, previous) {
1605
1724
  return [...previous, value];
1606
1725
  }
1726
+ function addPackRequestOptions(command) {
1727
+ return command.option("-n, --limit <n>", "max items", "10").option("--budget <tokens>", "token budget").option("--token-budget <tokens>", "token budget").option("--working-set-file <path>", "recently modified file path used as ranking hint", collectWorkingSetFile, []).option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--compact", "render a scannable index with full detail only for top N items").option("--compact-detail <n>", "items to show in full detail in compact mode (default 3)");
1728
+ }
1607
1729
  function buildPackRequestOptions(opts, ctx = {}) {
1608
- const limit = Number.parseInt(opts.limit ?? "10", 10) || 10;
1730
+ const limit = parsePositiveInt(opts.limit ?? "10", "limit");
1609
1731
  const budgetRaw = opts.tokenBudget ?? opts.budget;
1610
- const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
1732
+ const budget = budgetRaw ? parseNonNegativeInt(budgetRaw, "token budget") : void 0;
1611
1733
  const filters = {};
1612
1734
  if (!opts.allProjects) {
1613
1735
  const defaultProject = ctx.envProject?.trim() || null;
@@ -1617,12 +1739,30 @@ function buildPackRequestOptions(opts, ctx = {}) {
1617
1739
  if (project) filters.project = project;
1618
1740
  }
1619
1741
  if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
1742
+ let renderOptions;
1743
+ if (opts.compact || opts.compactDetail != null) {
1744
+ renderOptions = { compact: true };
1745
+ if (opts.compactDetail != null) renderOptions.compactDetailCount = parseNonNegativeInt(opts.compactDetail, "compact detail count");
1746
+ }
1620
1747
  return {
1621
1748
  limit,
1622
1749
  budget,
1623
- filters
1750
+ filters,
1751
+ renderOptions
1624
1752
  };
1625
1753
  }
1754
+ function parsePositiveInt(value, label) {
1755
+ if (!/^\d+$/.test(value.trim())) throw new PackUsageError(`${label} must be a positive integer`);
1756
+ const parsed = Number.parseInt(value, 10);
1757
+ if (!Number.isFinite(parsed) || parsed < 1) throw new PackUsageError(`${label} must be a positive integer`);
1758
+ return parsed;
1759
+ }
1760
+ function parseNonNegativeInt(value, label) {
1761
+ if (!/^\d+$/.test(value.trim())) throw new PackUsageError(`${label} must be a non-negative integer`);
1762
+ const parsed = Number.parseInt(value, 10);
1763
+ if (!Number.isFinite(parsed) || parsed < 0) throw new PackUsageError(`${label} must be a non-negative integer`);
1764
+ return parsed;
1765
+ }
1626
1766
  //#endregion
1627
1767
  //#region src/commands/memory.ts
1628
1768
  /**
@@ -1767,16 +1907,19 @@ function createInjectMemoryCommand() {
1767
1907
  return cmd;
1768
1908
  }
1769
1909
  function createMemoryRoleReportCommand() {
1770
- const cmd = new Command("role-report").configureHelp(helpStyle).description("Analyze inferred memory roles in a DB snapshot").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against the snapshot", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
1910
+ const cmd = new Command("role-report").configureHelp(helpStyle).description("Analyze inferred memory roles in a DB snapshot").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against the snapshot", (value, prev) => [...prev, value], []).option("--scenario <id>", "run a named injection-first eval scenario pack (can be repeated)", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
1771
1911
  addDbOption(cmd);
1772
1912
  addJsonOption(cmd);
1773
1913
  cmd.action((opts) => {
1774
1914
  const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
1915
+ const invalidScenario = (opts.scenario ?? []).find((id) => getInjectionEvalScenarioPack(id) == null);
1916
+ if (invalidScenario) throw new Error(`Unknown eval scenario pack: ${invalidScenario}`);
1917
+ const probes = [...opts.probe ?? [], ...getInjectionEvalScenarioPrompts(opts.scenario ?? [])];
1775
1918
  const result = getMemoryRoleReport(resolveDbOpt(opts), {
1776
1919
  project,
1777
1920
  allProjects: opts.allProjects === true,
1778
1921
  includeInactive: opts.inactive === true,
1779
- probes: opts.probe
1922
+ probes
1780
1923
  });
1781
1924
  if (opts.json) {
1782
1925
  console.log(JSON.stringify(result, null, 2));
@@ -1800,17 +1943,23 @@ function createMemoryRoleReportCommand() {
1800
1943
  p.log.message(` summary_unmapped ${result.summary_mapping.unmapped}`);
1801
1944
  p.log.info("Project quality:");
1802
1945
  for (const [bucket, count] of Object.entries(result.project_quality)) p.log.message(` ${bucket.padEnd(12)} ${String(count)}`);
1946
+ p.log.info("Session classes:");
1947
+ for (const [bucket, count] of Object.entries(result.session_class_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1948
+ p.log.info("Summary dispositions:");
1949
+ for (const [bucket, count] of Object.entries(result.summary_disposition_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1803
1950
  if (result.probe_results.length > 0) {
1804
1951
  p.log.info("Probe results:");
1805
1952
  for (const probe of result.probe_results) {
1806
1953
  p.log.message(` query: ${probe.query}`);
1954
+ if (probe.scenario_id) p.log.message(` scenario: ${probe.scenario_id} (${probe.scenario_category ?? "unknown"})${probe.scenario_title ? ` — ${probe.scenario_title}` : ""}`);
1807
1955
  p.log.message(` mode: ${probe.mode}`);
1808
1956
  p.log.message(` top roles: durable=${probe.top_role_counts.durable} recap=${probe.top_role_counts.recap} ephemeral=${probe.top_role_counts.ephemeral} general=${probe.top_role_counts.general}`);
1809
1957
  p.log.message(` top mapping: mapped=${probe.top_mapping_counts.mapped} unmapped=${probe.top_mapping_counts.unmapped}`);
1810
1958
  p.log.message(` burden: recap_share=${probe.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.top_burden.recap_unmapped_share.toFixed(2)}`);
1811
1959
  if (probe.simulated_demoted_unmapped_recap) p.log.message(` simulated demote-unmapped-recap burden: recap_share=${probe.simulated_demoted_unmapped_recap.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.simulated_demoted_unmapped_recap.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.simulated_demoted_unmapped_recap.top_burden.recap_unmapped_share.toFixed(2)}`);
1812
1960
  if (probe.simulated_demoted_unmapped_recap_and_ephemeral) p.log.message(` simulated demote-unmapped-recap+ephemeral burden: recap_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.recap_unmapped_share.toFixed(2)}`);
1813
- for (const item of probe.items.slice(0, 5)) p.log.message(` [${item.id}] (${item.kind}/${item.role}/${item.mapping}) ${item.title} ${item.role_reason}`);
1961
+ if (probe.scenario_score) p.log.message(` scenario score: mode_match=${probe.scenario_score.mode_match ? "yes" : "no"} top1_primary=${probe.scenario_score.primary_in_top1 ? "yes" : "no"} top3_primary=${probe.scenario_score.primary_in_top3_count} top1_anti=${probe.scenario_score.anti_signal_in_top1 ? "yes" : "no"} primary=${probe.scenario_score.primary_match_count} anti=${probe.scenario_score.anti_signal_count} recap=${probe.scenario_score.recap_count} unmapped_recap=${probe.scenario_score.unmapped_recap_count} chatter=${probe.scenario_score.administrative_chatter_count} net=${probe.scenario_score.score}`);
1962
+ for (const item of probe.items.slice(0, 5)) p.log.message(` [${item.id}] (${item.kind}/${item.role}/${item.mapping}/${item.session_class}/${item.summary_disposition}) ${item.title} — ${item.role_reason}`);
1814
1963
  }
1815
1964
  }
1816
1965
  p.outro("done");
@@ -1818,14 +1967,18 @@ function createMemoryRoleReportCommand() {
1818
1967
  return cmd;
1819
1968
  }
1820
1969
  function createMemoryRoleCompareCommand() {
1821
- const cmd = new Command("role-compare").configureHelp(helpStyle).description("Compare inferred memory-role and probe metrics across two DB snapshots").argument("<baseline_db>", "baseline sqlite database path").argument("<candidate_db>", "candidate sqlite database path").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against both snapshots", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
1970
+ const cmd = new Command("role-compare").configureHelp(helpStyle).description("Compare inferred memory-role and probe metrics across two DB snapshots").argument("<baseline_db>", "baseline sqlite database path").argument("<candidate_db>", "candidate sqlite database path").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against both snapshots", (value, prev) => [...prev, value], []).option("--scenario <id>", "run a named injection-first eval scenario pack (can be repeated)", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
1822
1971
  addJsonOption(cmd);
1823
1972
  cmd.action((baselineDb, candidateDb, opts) => {
1973
+ const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
1974
+ const invalidScenario = (opts.scenario ?? []).find((id) => getInjectionEvalScenarioPack(id) == null);
1975
+ if (invalidScenario) throw new Error(`Unknown eval scenario pack: ${invalidScenario}`);
1976
+ const probes = [...opts.probe ?? [], ...getInjectionEvalScenarioPrompts(opts.scenario ?? [])];
1824
1977
  const result = compareMemoryRoleReports(baselineDb, candidateDb, {
1825
- project: opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null),
1978
+ project,
1826
1979
  allProjects: opts.allProjects === true,
1827
1980
  includeInactive: opts.inactive === true,
1828
- probes: opts.probe
1981
+ probes
1829
1982
  });
1830
1983
  if (opts.json) {
1831
1984
  console.log(JSON.stringify(result, null, 2));
@@ -1843,6 +1996,10 @@ function createMemoryRoleCompareCommand() {
1843
1996
  ].join("\n"));
1844
1997
  p.log.info("Role deltas:");
1845
1998
  for (const [role, count] of Object.entries(result.delta.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
1999
+ p.log.info("Session class deltas:");
2000
+ for (const [bucket, count] of Object.entries(result.delta.session_class_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
2001
+ p.log.info("Summary disposition deltas:");
2002
+ for (const [bucket, count] of Object.entries(result.delta.summary_disposition_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1846
2003
  if (result.probe_comparisons.length > 0) {
1847
2004
  p.log.info("Probe comparisons:");
1848
2005
  for (const probe of result.probe_comparisons) {
@@ -1851,12 +2008,288 @@ function createMemoryRoleCompareCommand() {
1851
2008
  p.log.message(` overlap: shared_top_keys=${probe.shared_item_keys.length} baseline_top=${probe.baseline_item_ids.slice(0, 5).join(",") || "-"} candidate_top=${probe.candidate_item_ids.slice(0, 5).join(",") || "-"}`);
1852
2009
  if (probe.delta_top_burden) p.log.message(` burden delta: recap_share=${probe.delta_top_burden.recap_share.toFixed(2)} unmapped_share=${probe.delta_top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.delta_top_burden.recap_unmapped_share.toFixed(2)}`);
1853
2010
  if (probe.delta_top_mapping_counts) p.log.message(` mapping delta: mapped=${probe.delta_top_mapping_counts.mapped} unmapped=${probe.delta_top_mapping_counts.unmapped}`);
2011
+ if (probe.baseline_scenario_score || probe.candidate_scenario_score) p.log.message(` scenario scores: baseline=${probe.baseline_scenario_score?.score ?? "-"} candidate=${probe.candidate_scenario_score?.score ?? "-"}`);
2012
+ if (probe.delta_scenario_score) p.log.message(` scenario delta: mode_match=${probe.delta_scenario_score.mode_match ?? "-"} top1_primary=${probe.delta_scenario_score.primary_in_top1 ?? "-"} top3_primary=${probe.delta_scenario_score.primary_in_top3_count ?? "-"} top1_anti=${probe.delta_scenario_score.anti_signal_in_top1 ?? "-"} primary=${probe.delta_scenario_score.primary_match_count ?? "-"} anti=${probe.delta_scenario_score.anti_signal_count ?? "-"} recap=${probe.delta_scenario_score.recap_count ?? "-"} unmapped_recap=${probe.delta_scenario_score.unmapped_recap_count ?? "-"} chatter=${probe.delta_scenario_score.administrative_chatter_count ?? "-"} net=${probe.delta_scenario_score.score ?? "-"}`);
1854
2013
  }
1855
2014
  }
1856
2015
  p.outro("done");
1857
2016
  });
1858
2017
  return cmd;
1859
2018
  }
2019
+ function createMemoryExtractionReportCommand() {
2020
+ const cmd = new Command("extraction-report").configureHelp(helpStyle).description("Score extracted memories for a session against a built-in extraction eval rubric").option("--session-id <id>", "session ID to evaluate").option("--batch-id <id>", "raw-event flush batch ID to evaluate").requiredOption("--scenario <id>", "built-in extraction eval scenario ID").option("--inactive", "include inactive memories");
2021
+ addDbOption(cmd);
2022
+ addJsonOption(cmd);
2023
+ cmd.action((opts) => {
2024
+ const sessionIdInput = opts.sessionId?.trim() ?? "";
2025
+ const batchIdInput = opts.batchId?.trim() ?? "";
2026
+ const hasSessionId = sessionIdInput.length > 0;
2027
+ const hasBatchId = batchIdInput.length > 0;
2028
+ if (hasSessionId === hasBatchId) throw new Error("Provide exactly one of --session-id or --batch-id");
2029
+ const sessionId = hasSessionId ? parseStrictPositiveId(sessionIdInput) : null;
2030
+ if (hasSessionId && sessionId === null) throw new Error(`Invalid session ID: ${sessionIdInput || opts.sessionId}`);
2031
+ const batchId = hasBatchId ? parseStrictPositiveId(batchIdInput) : null;
2032
+ if (hasBatchId && batchId === null) throw new Error(`Invalid batch ID: ${batchIdInput || opts.batchId}`);
2033
+ const scenarioId = opts.scenario?.trim() ?? "";
2034
+ const scenario = getSessionExtractionEvalScenario(scenarioId);
2035
+ if (!scenario) throw new Error(`Unknown extraction eval scenario: ${scenarioId || opts.scenario}`);
2036
+ const result = batchId != null ? getSessionExtractionEval(resolveDbOpt(opts), {
2037
+ batchId,
2038
+ scenarioId: scenario.id,
2039
+ includeInactive: opts.inactive === true
2040
+ }) : getSessionExtractionEval(resolveDbOpt(opts), {
2041
+ sessionId,
2042
+ scenarioId: scenario.id,
2043
+ includeInactive: opts.inactive === true
2044
+ });
2045
+ if (opts.json) {
2046
+ console.log(JSON.stringify(result, null, 2));
2047
+ return;
2048
+ }
2049
+ p.intro("codemem memory extraction-report");
2050
+ p.log.info([
2051
+ `Scenario: ${result.scenario.id} — ${result.scenario.title}`,
2052
+ `Target: ${result.target.type}${result.target.batchId != null ? ` #${result.target.batchId}` : ""}`,
2053
+ `Session: ${result.session.id} (${result.session.project ?? "no-project"})`,
2054
+ `Session class: ${result.session.sessionClass}`,
2055
+ `Summary disposition: ${result.session.summaryDisposition}`
2056
+ ].join("\n"));
2057
+ p.log.info([
2058
+ `Pass: ${result.pass ? "yes" : "no"}`,
2059
+ `Summary count: ${result.counts.summaries}`,
2060
+ `Observation count: ${result.counts.observations}`,
2061
+ `Summary thread coverage: ${result.coverage.summaryThreadCoverage}`,
2062
+ `Observation thread coverage: ${result.coverage.observationThreadCoverage}`,
2063
+ `Total thread coverage: ${result.coverage.totalThreadCoverage}`,
2064
+ `Duplicate observation threads: ${result.coverage.duplicateObservationThreads}`
2065
+ ].join("\n"));
2066
+ if (result.failureReasons.length > 0) {
2067
+ p.log.warn("Failure reasons:");
2068
+ for (const reason of result.failureReasons) p.log.message(` - ${reason}`);
2069
+ }
2070
+ p.log.info("Thread coverage:");
2071
+ for (const thread of result.threads) p.log.message(` ${thread.id.padEnd(22)} summary=${thread.summaryMatch ? "yes" : "no"} observations=${thread.observationMatch ? "yes" : "no"}`);
2072
+ p.outro("done");
2073
+ });
2074
+ return cmd;
2075
+ }
2076
+ function createMemoryExtractionReplayCommand() {
2077
+ const cmd = new Command("extraction-replay").configureHelp(helpStyle).description("Re-run the observer on a historical flush batch without persisting, then score the fresh output").requiredOption("--batch-id <id>", "raw-event flush batch ID to replay").option("--transcript-budget <chars>", "override replay transcript budget in characters (replay only)").option("--observer-tier-routing", "use replay-only benchmark-backed observer tier routing").option("--observer-temperature <value>", "override observer temperature for replay only").option("--openai-responses", "use OpenAI Responses API for replay only").option("--reasoning-effort <level>", "set OpenAI reasoning.effort for replay only (responses path)").option("--reasoning-summary <mode>", "set OpenAI reasoning.summary for replay only (responses path)").option("--max-output-tokens <n>", "override OpenAI max_output_tokens for replay only (responses path)").requiredOption("--scenario <id>", "built-in extraction eval scenario ID");
2078
+ addDbOption(cmd);
2079
+ addJsonOption(cmd);
2080
+ cmd.action(async (opts) => {
2081
+ const batchIdInput = opts.batchId?.trim() ?? "";
2082
+ const batchId = parseStrictPositiveId(batchIdInput);
2083
+ if (batchId === null) throw new Error(`Invalid batch ID: ${batchIdInput || opts.batchId}`);
2084
+ const scenarioId = opts.scenario?.trim() ?? "";
2085
+ const scenario = getSessionExtractionEvalScenario(scenarioId);
2086
+ if (!scenario) throw new Error(`Unknown extraction eval scenario: ${scenarioId || opts.scenario}`);
2087
+ const transcriptBudgetInput = opts.transcriptBudget?.trim() ?? "";
2088
+ const transcriptBudget = transcriptBudgetInput.length > 0 ? parseStrictPositiveId(transcriptBudgetInput) : null;
2089
+ if (transcriptBudgetInput.length > 0 && transcriptBudget === null) throw new Error(`Invalid transcript budget: ${transcriptBudgetInput || opts.transcriptBudget}`);
2090
+ const observerTemperatureInput = opts.observerTemperature?.trim() ?? "";
2091
+ let observerTemperature;
2092
+ if (observerTemperatureInput.length > 0) {
2093
+ const parsed = Number(observerTemperatureInput);
2094
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid observer temperature: ${observerTemperatureInput || opts.observerTemperature}`);
2095
+ observerTemperature = parsed;
2096
+ }
2097
+ const maxOutputTokensInput = opts.maxOutputTokens?.trim() ?? "";
2098
+ const maxOutputTokens = maxOutputTokensInput.length > 0 ? parseStrictPositiveId(maxOutputTokensInput) : null;
2099
+ if (maxOutputTokensInput.length > 0 && maxOutputTokens === null) throw new Error(`Invalid max output tokens: ${maxOutputTokensInput || opts.maxOutputTokens}`);
2100
+ const observerConfig = loadObserverConfig();
2101
+ const observerConfigWithOverrides = {
2102
+ ...observerConfig,
2103
+ observerTemperature: observerTemperature ?? observerConfig.observerTemperature,
2104
+ observerOpenAIUseResponses: opts.openaiResponses === true,
2105
+ observerReasoningEffort: opts.reasoningEffort?.trim() || null,
2106
+ observerReasoningSummary: opts.reasoningSummary?.trim() || null,
2107
+ observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxTokens
2108
+ };
2109
+ const observer = new ObserverClient(observerConfigWithOverrides);
2110
+ const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
2111
+ batchId,
2112
+ scenarioId: scenario.id,
2113
+ transcriptBudget: transcriptBudget ?? void 0
2114
+ }) : await replayBatchExtraction(resolveDbOpt(opts), observer, {
2115
+ batchId,
2116
+ scenarioId: scenario.id,
2117
+ transcriptBudget: transcriptBudget ?? void 0
2118
+ });
2119
+ if (opts.json) {
2120
+ console.log(JSON.stringify(result, null, 2));
2121
+ return;
2122
+ }
2123
+ p.intro("codemem memory extraction-replay");
2124
+ p.log.info([
2125
+ `Scenario: ${result.scenario.id} — ${result.scenario.title}`,
2126
+ `Batch: ${result.target.batchId}`,
2127
+ `Session: ${result.target.sessionId}`,
2128
+ `Observer: ${result.observer.provider}/${result.observer.model}`,
2129
+ `Tier: ${result.observer.tier ?? "manual"}`,
2130
+ `OpenAI Responses: ${result.observer.openaiUseResponses ? "yes" : "no"}`,
2131
+ `Reasoning effort: ${result.observer.reasoningEffort ?? "none"}`,
2132
+ `Classification: ${result.classification.status}`,
2133
+ `Pass: ${result.evaluation.pass ? "yes" : "no"}`
2134
+ ].join("\n"));
2135
+ if (result.classification.reason) p.log.message(`Classification reason: ${result.classification.reason}`);
2136
+ if (result.evaluation.failureReasons.length > 0) {
2137
+ p.log.warn("Failure reasons:");
2138
+ for (const reason of result.evaluation.failureReasons) p.log.message(` - ${reason}`);
2139
+ }
2140
+ p.log.info([
2141
+ `Fresh summaries: ${result.evaluation.counts.summaries}`,
2142
+ `Fresh observations: ${result.evaluation.counts.observations}`,
2143
+ `Summary thread coverage: ${result.evaluation.coverage.summaryThreadCoverage}`,
2144
+ `Observation thread coverage: ${result.evaluation.coverage.observationThreadCoverage}`,
2145
+ `Total thread coverage: ${result.evaluation.coverage.totalThreadCoverage}`
2146
+ ].join("\n"));
2147
+ p.outro("done");
2148
+ });
2149
+ return cmd;
2150
+ }
2151
+ function createMemoryExtractionBenchmarkCommand() {
2152
+ const cmd = new Command("extraction-benchmark").configureHelp(helpStyle).description("Run the formal extraction replay benchmark set and print a cost/quality scoreboard").requiredOption("--benchmark <id>", "benchmark profile id").option("--observer-provider <provider>", "override observer provider for this benchmark run").option("--observer-model <model>", "override observer model for this benchmark run").option("--observer-tier-routing", "use replay-only benchmark-backed observer tier routing").option("--openai-responses", "use OpenAI Responses API for this benchmark run").option("--reasoning-effort <level>", "set OpenAI reasoning.effort for this benchmark run (responses path)").option("--reasoning-summary <mode>", "set OpenAI reasoning.summary for this benchmark run (responses path)").option("--max-output-tokens <n>", "override OpenAI max_output_tokens for this benchmark run (responses path)").option("--observer-temperature <value>", "override observer temperature for this benchmark run").option("--transcript-budget <chars>", "override replay transcript budget in characters for this benchmark run");
2153
+ addDbOption(cmd);
2154
+ addJsonOption(cmd);
2155
+ cmd.action(async (opts) => {
2156
+ const benchmarkId = opts.benchmark?.trim() ?? "";
2157
+ const benchmark = getExtractionBenchmarkProfile(benchmarkId);
2158
+ if (!benchmark) throw new Error(`Unknown extraction benchmark: ${benchmarkId || opts.benchmark}`);
2159
+ const transcriptBudgetInput = opts.transcriptBudget?.trim() ?? "";
2160
+ const transcriptBudget = transcriptBudgetInput.length > 0 ? parseStrictPositiveId(transcriptBudgetInput) : null;
2161
+ if (transcriptBudgetInput.length > 0 && transcriptBudget === null) throw new Error(`Invalid transcript budget: ${transcriptBudgetInput || opts.transcriptBudget}`);
2162
+ const observerTemperatureInput = opts.observerTemperature?.trim() ?? "";
2163
+ let observerTemperature;
2164
+ if (observerTemperatureInput.length > 0) {
2165
+ const parsed = Number(observerTemperatureInput);
2166
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid observer temperature: ${observerTemperatureInput || opts.observerTemperature}`);
2167
+ observerTemperature = parsed;
2168
+ }
2169
+ const maxOutputTokensInput = opts.maxOutputTokens?.trim() ?? "";
2170
+ const maxOutputTokens = maxOutputTokensInput.length > 0 ? parseStrictPositiveId(maxOutputTokensInput) : null;
2171
+ if (maxOutputTokensInput.length > 0 && maxOutputTokens === null) throw new Error(`Invalid max output tokens: ${maxOutputTokensInput || opts.maxOutputTokens}`);
2172
+ const observerConfig = loadObserverConfig();
2173
+ const observerConfigWithOverrides = {
2174
+ ...observerConfig,
2175
+ observerProvider: opts.observerProvider?.trim() || observerConfig.observerProvider,
2176
+ observerModel: opts.observerModel?.trim() || observerConfig.observerModel,
2177
+ observerTemperature: observerTemperature ?? observerConfig.observerTemperature,
2178
+ observerOpenAIUseResponses: opts.openaiResponses === true,
2179
+ observerReasoningEffort: opts.reasoningEffort?.trim() || null,
2180
+ observerReasoningSummary: opts.reasoningSummary?.trim() || null,
2181
+ observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxTokens
2182
+ };
2183
+ const observer = new ObserverClient(observerConfigWithOverrides);
2184
+ const runs = [];
2185
+ for (const batch of benchmark.batches) {
2186
+ const scenarioId = batch.scenarioId ?? benchmark.scenarioId;
2187
+ const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
2188
+ batchId: batch.batchId,
2189
+ scenarioId,
2190
+ transcriptBudget: transcriptBudget ?? void 0
2191
+ }) : await replayBatchExtraction(resolveDbOpt(opts), observer, {
2192
+ batchId: batch.batchId,
2193
+ scenarioId,
2194
+ transcriptBudget: transcriptBudget ?? void 0
2195
+ });
2196
+ runs.push({
2197
+ batchId: batch.batchId,
2198
+ sessionId: batch.sessionId,
2199
+ label: batch.label,
2200
+ purpose: batch.purpose,
2201
+ complexity: batch.complexity,
2202
+ scenarioId,
2203
+ expectedTier: batch.expectedTier ?? null,
2204
+ analysis: {
2205
+ eventSpan: result.analysis.eventSpan,
2206
+ promptCount: result.analysis.promptCount,
2207
+ toolCount: result.analysis.toolCount,
2208
+ transcriptLength: result.analysis.transcriptLength
2209
+ },
2210
+ status: result.classification.status,
2211
+ reason: result.classification.reason,
2212
+ tier: result.observer.tier ?? "manual",
2213
+ provider: result.observer.provider,
2214
+ model: result.observer.model,
2215
+ openaiUseResponses: result.observer.openaiUseResponses,
2216
+ reasoningEffort: result.observer.reasoningEffort,
2217
+ reasoningSummary: result.observer.reasoningSummary,
2218
+ maxOutputTokens: result.observer.maxOutputTokens,
2219
+ temperature: result.observer.temperature,
2220
+ summaries: result.evaluation.counts.summaries,
2221
+ observations: result.evaluation.counts.observations,
2222
+ repairApplied: result.observer.repairApplied
2223
+ });
2224
+ }
2225
+ const summary = {
2226
+ total: runs.length,
2227
+ shapeQualityTotal: runs.filter((run) => run.purpose === "shape_quality").length,
2228
+ shapeQualityPasses: runs.filter((run) => run.purpose === "shape_quality" && run.status === "pass").length,
2229
+ shapeQualityFails: runs.filter((run) => run.purpose === "shape_quality" && run.status === "shape_fail").length,
2230
+ expectedTierTotal: runs.filter((run) => run.expectedTier != null).length,
2231
+ expectedTierMatches: runs.filter((run) => run.expectedTier != null && run.expectedTier === run.tier).length,
2232
+ robustnessNoOutput: runs.filter((run) => run.status === "observer_no_output").length
2233
+ };
2234
+ const uniqueObserverKeys = Array.from(new Set(runs.map((run) => `${run.provider}::${run.model}::${run.openaiUseResponses ? "responses" : "chat"}`)));
2235
+ const observerSummary = opts.observerTierRouting === true ? {
2236
+ provider: uniqueObserverKeys.length === 1 ? runs[0]?.provider ?? observer.provider : "mixed",
2237
+ model: uniqueObserverKeys.length === 1 ? runs[0]?.model ?? observer.model : "mixed",
2238
+ tierRouting: true,
2239
+ openaiUseResponses: uniqueObserverKeys.length === 1 ? runs[0]?.openaiUseResponses ?? observer.openaiUseResponses : null,
2240
+ reasoningEffort: uniqueObserverKeys.length === 1 ? runs[0]?.reasoningEffort ?? observer.reasoningEffort : "mixed",
2241
+ reasoningSummary: uniqueObserverKeys.length === 1 ? runs[0]?.reasoningSummary ?? observer.reasoningSummary : "mixed",
2242
+ maxOutputTokens: uniqueObserverKeys.length === 1 ? runs[0]?.maxOutputTokens ?? observer.maxOutputTokens : null,
2243
+ temperature: uniqueObserverKeys.length === 1 ? runs[0]?.temperature ?? observer.temperature : null,
2244
+ transcriptBudget: transcriptBudget ?? null,
2245
+ selectedObservers: uniqueObserverKeys
2246
+ } : {
2247
+ provider: observer.provider,
2248
+ model: observer.model,
2249
+ tierRouting: false,
2250
+ openaiUseResponses: observer.openaiUseResponses,
2251
+ reasoningEffort: observer.reasoningEffort,
2252
+ reasoningSummary: observer.reasoningSummary,
2253
+ maxOutputTokens: observer.maxOutputTokens,
2254
+ temperature: observer.temperature,
2255
+ transcriptBudget: transcriptBudget ?? null,
2256
+ selectedObservers: uniqueObserverKeys
2257
+ };
2258
+ const output = {
2259
+ benchmark: {
2260
+ id: benchmark.id,
2261
+ title: benchmark.title,
2262
+ scenarioId: benchmark.scenarioId
2263
+ },
2264
+ observer: observerSummary,
2265
+ summary,
2266
+ runs
2267
+ };
2268
+ if (opts.json) {
2269
+ console.log(JSON.stringify(output, null, 2));
2270
+ return;
2271
+ }
2272
+ p.intro("codemem memory extraction-benchmark");
2273
+ p.log.info([
2274
+ `Benchmark: ${benchmark.id} — ${benchmark.title}`,
2275
+ `Observer: ${observerSummary.provider}/${observerSummary.model}`,
2276
+ `Tier routing: ${opts.observerTierRouting === true ? "yes" : "no"}`,
2277
+ `OpenAI Responses: ${observerSummary.openaiUseResponses === null ? "mixed" : observerSummary.openaiUseResponses ? "yes" : "no"}`,
2278
+ `Reasoning effort: ${observerSummary.reasoningEffort ?? "none"}`,
2279
+ `Reasoning summary: ${observerSummary.reasoningSummary ?? "none"}`,
2280
+ `Max output tokens: ${observerSummary.maxOutputTokens ?? "mixed"}`,
2281
+ `Temperature: ${observerSummary.temperature ?? "mixed"}`,
2282
+ `Transcript budget override: ${transcriptBudget ?? "default"}`,
2283
+ `Shape-quality passes: ${summary.shapeQualityPasses}/${summary.shapeQualityTotal}`,
2284
+ `Shape-quality fails: ${summary.shapeQualityFails}`,
2285
+ `Expected-tier matches: ${summary.expectedTierMatches}/${summary.expectedTierTotal}`,
2286
+ `Observer no-output cases: ${summary.robustnessNoOutput}`
2287
+ ].join("\n"));
2288
+ for (const run of runs) p.log.message(` [${run.batchId}] ${run.status.padEnd(18)} ${run.complexity.padEnd(10)} tier=${run.tier.padEnd(6)} expected=${(run.expectedTier ?? "n/a").padEnd(6)} span=${String(run.analysis.eventSpan).padEnd(3)} prompts=${run.analysis.promptCount} tools=${String(run.analysis.toolCount).padEnd(2)} transcript=${run.analysis.transcriptLength} ${run.provider}/${run.model}${run.openaiUseResponses ? " [responses]" : ""} summaries=${run.summaries} observations=${run.observations} repair=${run.repairApplied ? "yes" : "no"} — ${run.label}`);
2289
+ p.outro("done");
2290
+ });
2291
+ return cmd;
2292
+ }
1860
2293
  function createMemoryRelinkReportCommand() {
1861
2294
  const cmd = new Command("relink-report").configureHelp(helpStyle).description("Analyze dry-run raw-event session relinking and compaction opportunities").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--limit <n>", "max groups to print", "25");
1862
2295
  addDbOption(cmd);
@@ -1935,18 +2368,86 @@ memoryCommand.addCommand(createRememberMemoryCommand());
1935
2368
  memoryCommand.addCommand(createInjectMemoryCommand());
1936
2369
  memoryCommand.addCommand(createMemoryRoleReportCommand());
1937
2370
  memoryCommand.addCommand(createMemoryRoleCompareCommand());
2371
+ memoryCommand.addCommand(createMemoryExtractionReportCommand());
2372
+ memoryCommand.addCommand(createMemoryExtractionReplayCommand());
2373
+ memoryCommand.addCommand(createMemoryExtractionBenchmarkCommand());
1938
2374
  memoryCommand.addCommand(createMemoryRelinkReportCommand());
1939
2375
  memoryCommand.addCommand(createMemoryRelinkPlanCommand());
1940
2376
  //#endregion
1941
2377
  //#region src/commands/pack.ts
1942
- var packCmd = new Command("pack").configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for").option("-n, --limit <n>", "max items", "10").option("--budget <tokens>", "token budget").option("--token-budget <tokens>", "token budget").option("--working-set-file <path>", "recently modified file path used as ranking hint", collectWorkingSetFile, []).option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects");
1943
- addDbOption(packCmd);
1944
- addJsonOption(packCmd);
1945
- var packCommand = packCmd.action(async (context, opts) => {
1946
- const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2378
+ function describeCandidate(candidate) {
2379
+ const scoreParts = [
2380
+ candidate.scores.combined_score != null ? `combined=${candidate.scores.combined_score.toFixed(2)}` : null,
2381
+ candidate.scores.base_score != null ? `base=${candidate.scores.base_score.toFixed(2)}` : null,
2382
+ candidate.scores.text_overlap > 0 ? `text=${candidate.scores.text_overlap}` : null,
2383
+ candidate.scores.tag_overlap > 0 ? `tag=${candidate.scores.tag_overlap}` : null,
2384
+ candidate.scores.working_set_overlap > 0 ? `working_set=${candidate.scores.working_set_overlap.toFixed(2)}` : null
2385
+ ].filter(Boolean).join(" ");
2386
+ const lines = [`${candidate.rank}. [${candidate.id}] (${candidate.kind}) ${candidate.title}`];
2387
+ if (candidate.section) lines.push(` - section: ${candidate.section}`);
2388
+ if (candidate.reasons.length > 0) lines.push(` - reasons: ${candidate.reasons.join(", ")}`);
2389
+ if (scoreParts) lines.push(` - scores: ${scoreParts}`);
2390
+ if (candidate.preview) lines.push(` - preview: ${candidate.preview}`);
2391
+ return lines;
2392
+ }
2393
+ function renderPackTrace(trace) {
2394
+ const workingSet = trace.inputs.working_set_files.length > 0 ? trace.inputs.working_set_files.join(", ") : "(none)";
2395
+ const lines = [
2396
+ "Pack trace",
2397
+ `- Query: ${trace.inputs.query}`,
2398
+ `- Project: ${trace.inputs.project ?? "(default)"}`,
2399
+ `- Working set: ${workingSet}`,
2400
+ `- Mode: ${trace.mode.selected}`,
2401
+ `- Mode reasons: ${trace.mode.reasons.join(", ") || "(none)"}`,
2402
+ `- Token budget: ${trace.inputs.token_budget ?? "(none)"}`,
2403
+ ""
2404
+ ];
2405
+ for (const disposition of [
2406
+ "selected",
2407
+ "dropped",
2408
+ "deduped",
2409
+ "trimmed"
2410
+ ]) {
2411
+ const group = trace.retrieval.candidates.filter((candidate) => candidate.disposition === disposition);
2412
+ if (group.length === 0) continue;
2413
+ lines.push(disposition.charAt(0).toUpperCase() + disposition.slice(1));
2414
+ for (const candidate of group) lines.push(...describeCandidate(candidate));
2415
+ lines.push("");
2416
+ }
2417
+ lines.push("Assembly");
2418
+ lines.push(`- deduped ids: ${trace.assembly.deduped_ids.join(", ") || "(none)"}`);
2419
+ lines.push(`- trimmed ids: ${trace.assembly.trimmed_ids.join(", ") || "(none)"}`);
2420
+ lines.push(`- trim reasons: ${trace.assembly.trim_reasons.join(", ") || "(none)"}`);
2421
+ lines.push(`- section counts: summary=${trace.output.section_counts.summary} timeline=${trace.output.section_counts.timeline} observations=${trace.output.section_counts.observations}`);
2422
+ lines.push(`- estimated tokens: ${trace.output.estimated_tokens}`);
2423
+ lines.push(`- truncated: ${trace.output.truncated ? "yes" : "no"}`);
2424
+ lines.push("");
2425
+ lines.push("Final pack");
2426
+ lines.push(trace.output.pack_text);
2427
+ return lines.join("\n");
2428
+ }
2429
+ async function withStore(opts, errorCode, run) {
2430
+ let store = null;
1947
2431
  try {
1948
- const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
1949
- const result = await store.buildMemoryPackAsync(context, limit, budget, filters);
2432
+ store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2433
+ await run(store);
2434
+ } catch (error) {
2435
+ const message = error instanceof Error ? error.message : String(error);
2436
+ const usageError = error instanceof PackUsageError;
2437
+ if (opts.json) {
2438
+ emitJsonError(usageError ? "usage_error" : errorCode, message, usageError ? 2 : 1);
2439
+ return;
2440
+ }
2441
+ p.log.error(message);
2442
+ process.exitCode = usageError ? 2 : 1;
2443
+ } finally {
2444
+ store?.close();
2445
+ }
2446
+ }
2447
+ async function packAction(context, opts) {
2448
+ await withStore(opts, "pack_failed", async (store) => {
2449
+ const { limit, budget, filters, renderOptions } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
2450
+ const result = await store.buildMemoryPackAsync(context, limit, budget, filters, renderOptions);
1950
2451
  if (opts.json) {
1951
2452
  console.log(JSON.stringify(result, null, 2));
1952
2453
  return;
@@ -1957,15 +2458,34 @@ var packCommand = packCmd.action(async (context, opts) => {
1957
2458
  p.outro("done");
1958
2459
  return;
1959
2460
  }
1960
- const m = result.metrics;
1961
- p.log.info(`${m.total_items} items, ~${m.pack_tokens} tokens` + (m.fallback_used ? " (fallback)" : "") + ` [fts:${m.sources.fts} sem:${m.sources.semantic} fuzzy:${m.sources.fuzzy}]`);
2461
+ const metrics = result.metrics;
2462
+ p.log.info(`${metrics.total_items} items, ~${metrics.pack_tokens} tokens` + (metrics.fallback_used ? " (fallback)" : "") + ` [fts:${metrics.sources.fts} sem:${metrics.sources.semantic} fuzzy:${metrics.sources.fuzzy}]`);
1962
2463
  for (const item of result.items) p.log.step(`#${item.id} ${item.kind} ${item.title}`);
1963
2464
  p.note(result.pack_text, "pack_text");
1964
2465
  p.outro("done");
1965
- } finally {
1966
- store.close();
1967
- }
1968
- });
2466
+ });
2467
+ }
2468
+ async function traceAction(context, opts) {
2469
+ await withStore(opts, "pack_trace_failed", async (store) => {
2470
+ const { limit, budget, filters, renderOptions } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
2471
+ const trace = await store.buildMemoryPackTraceAsync(context, limit, budget, filters, renderOptions);
2472
+ if (opts.json) {
2473
+ console.log(JSON.stringify(trace, null, 2));
2474
+ return;
2475
+ }
2476
+ console.log(renderPackTrace(trace));
2477
+ });
2478
+ }
2479
+ var packCmd = addPackRequestOptions(new Command("pack").enablePositionalOptions().configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for"));
2480
+ addDbOption(packCmd);
2481
+ addJsonOption(packCmd);
2482
+ packCmd.action(packAction);
2483
+ var traceCmd = addPackRequestOptions(new Command("trace").configureHelp(helpStyle).description("Trace retrieval and assembly for a memory pack").argument("<context>", "context string to trace"));
2484
+ addDbOption(traceCmd);
2485
+ addJsonOption(traceCmd);
2486
+ traceCmd.action(traceAction);
2487
+ packCmd.addCommand(traceCmd);
2488
+ var packCommand = packCmd;
1969
2489
  //#endregion
1970
2490
  //#region src/commands/recent.ts
1971
2491
  var cmd = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind");
@@ -2387,6 +2907,8 @@ async function startForegroundViewer(invocation) {
2387
2907
  dbPath: resolveDbPath(invocation.dbPath ?? void 0),
2388
2908
  signal: retentionAbort.signal
2389
2909
  });
2910
+ const vectorMigrationRunner = new VectorModelMigrationRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
2911
+ const dedupKeyBackfillRunner = new DedupKeyBackfillRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
2390
2912
  const syncRuntimeStatus = {
2391
2913
  phase: syncEnabled ? "starting" : "disabled",
2392
2914
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
@@ -2429,6 +2951,14 @@ async function startForegroundViewer(invocation) {
2429
2951
  p.log.success(`Listening on http://${info.address}:${info.port}`);
2430
2952
  p.log.info(`Database: ${preparedDb}`);
2431
2953
  p.log.step("Raw event sweeper started");
2954
+ if (!isEmbeddingDisabled()) {
2955
+ vectorMigrationRunner.start();
2956
+ p.log.step("Vector maintenance runner started");
2957
+ }
2958
+ if (hasPendingDedupKeyBackfill(store.db)) {
2959
+ dedupKeyBackfillRunner.start();
2960
+ p.log.step("Dedup-key maintenance runner started");
2961
+ }
2432
2962
  if (syncConfig.syncRetentionEnabled) {
2433
2963
  retentionRunner.start();
2434
2964
  p.log.step("Retention maintenance runner started");
@@ -2487,6 +3017,8 @@ async function startForegroundViewer(invocation) {
2487
3017
  syncAbort.abort();
2488
3018
  retentionAbort.abort();
2489
3019
  await sweeper.stop();
3020
+ await dedupKeyBackfillRunner.stop();
3021
+ await vectorMigrationRunner.stop();
2490
3022
  await retentionRunner.stop();
2491
3023
  await new Promise((resolve) => {
2492
3024
  let remaining = syncServer ? 2 : 1;
@@ -3810,7 +4342,7 @@ function getShellCompletionScript() {
3810
4342
  return completion.generateCompletionCode();
3811
4343
  }
3812
4344
  var program = new Command();
3813
- program.name("codemem").description("codemem — persistent memory for AI coding agents").option("--install-completion", "install shell completion").option("--show-completion", "show shell completion install guidance").version(VERSION).configureHelp(helpStyle);
4345
+ program.name("codemem").description("codemem — persistent memory for AI coding agents").enablePositionalOptions().option("--install-completion", "install shell completion").option("--show-completion", "show shell completion install guidance").version(VERSION).configureHelp(helpStyle);
3814
4346
  if (hasRootFlag("--setup-completion") || hasRootFlag("--install-completion")) {
3815
4347
  completion.setupShellInitFile();
3816
4348
  process.exit(0);