agenr 0.9.15 → 0.9.16

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.16 - 2026-02-27
4
+
5
+ ### Added
6
+ - Added progress logging throughout the consolidate pipeline. Pairwise
7
+ similarity scan, rules phases, cluster processing, LLM dedup checks, and
8
+ LLM merge calls now report progress so users can see the system is working.
9
+ Phase-level progress logs are always shown (not gated behind `--verbose`).
10
+ - Added live cluster progress updates with ETA during consolidation phases so
11
+ long-running Phase 1, Phase 2, and Phase 3 work shows continuous terminal
12
+ activity.
13
+
3
14
  ## 0.9.14 - 2026-02-27
4
15
 
5
16
  ### Fixed
package/dist/cli-main.js CHANGED
@@ -3741,6 +3741,13 @@ function collapsePreview(text4, maxLength = 80) {
3741
3741
  }
3742
3742
  return `${collapsed.slice(0, maxLength - 3)}...`;
3743
3743
  }
3744
+ function formatSeconds(ms) {
3745
+ const seconds = Math.max(0, Math.round(ms / 1e3));
3746
+ return `${seconds}s`;
3747
+ }
3748
+ function formatCount(value) {
3749
+ return value.toLocaleString("en-US");
3750
+ }
3744
3751
  function forgettingScore(entry, now) {
3745
3752
  const ageDays2 = parseDaysBetween(now, entry.created_at);
3746
3753
  const recallCount = entry.recall_count ?? 0;
@@ -3932,6 +3939,13 @@ async function mergeNearExactDuplicates(db, options) {
3932
3939
  if (entries.length < 2) {
3933
3940
  return 0;
3934
3941
  }
3942
+ const totalComparisons = entries.length * (entries.length - 1) / 2;
3943
+ options.onLog(
3944
+ `[merge] Computing pairwise similarity for ${entries.length} entries (${formatCount(totalComparisons)} comparisons)...`
3945
+ );
3946
+ const scanStartedAt = Date.now();
3947
+ let checkedPairs = 0;
3948
+ let nextProgressPercent = 10;
3935
3949
  const entryById = new Map(entries.map((entry) => [entry.id, entry]));
3936
3950
  const unionFind = new UnionFind();
3937
3951
  for (const entry of entries) {
@@ -3941,6 +3955,7 @@ async function mergeNearExactDuplicates(db, options) {
3941
3955
  const entry = entries[i];
3942
3956
  for (let j = i + 1; j < entries.length; j += 1) {
3943
3957
  const candidate = entries[j];
3958
+ checkedPairs += 1;
3944
3959
  const similarity = cosineSim(entry.embedding, candidate.embedding);
3945
3960
  if (similarity <= MERGE_SIMILARITY_THRESHOLD) {
3946
3961
  continue;
@@ -3956,6 +3971,14 @@ async function mergeNearExactDuplicates(db, options) {
3956
3971
  }
3957
3972
  unionFind.union(entry.id, candidate.id);
3958
3973
  }
3974
+ while (nextProgressPercent <= 100 && i + 1 >= Math.ceil(entries.length * nextProgressPercent / 100)) {
3975
+ const elapsedMs = Date.now() - scanStartedAt;
3976
+ const remainingPairs = Math.max(totalComparisons - checkedPairs, 0);
3977
+ const etaMs = checkedPairs > 0 ? elapsedMs / checkedPairs * remainingPairs : 0;
3978
+ const etaSuffix = checkedPairs > 0 ? ` ~${formatSeconds(etaMs)} remaining` : "";
3979
+ options.onLog(`[merge] ...${nextProgressPercent}% (${formatCount(checkedPairs)} pairs checked)${etaSuffix}`);
3980
+ nextProgressPercent += 10;
3981
+ }
3959
3982
  }
3960
3983
  const groups = /* @__PURE__ */ new Map();
3961
3984
  for (const entry of entries) {
@@ -3964,6 +3987,9 @@ async function mergeNearExactDuplicates(db, options) {
3964
3987
  current.push(entry);
3965
3988
  groups.set(root, current);
3966
3989
  }
3990
+ const groupedCount = Array.from(groups.values()).filter((group) => group.length >= 2).length;
3991
+ const scanSeconds = ((Date.now() - scanStartedAt) / 1e3).toFixed(1);
3992
+ options.onLog(`[merge] Similarity scan complete: ${groupedCount} groups found in ${scanSeconds}s`);
3967
3993
  let mergedCount = 0;
3968
3994
  for (const rawGroup of groups.values()) {
3969
3995
  if (rawGroup.length < 2) {
@@ -4126,17 +4152,44 @@ async function consolidateRules(db, dbPath, options = {}) {
4126
4152
  let expiredCount = 0;
4127
4153
  let mergedCount = 0;
4128
4154
  let orphanedRelationsCleaned = 0;
4155
+ const mergePassCount = 1;
4156
+ const runRulePasses = async () => {
4157
+ for (let pass = 1; pass <= mergePassCount; pass += 1) {
4158
+ onLog("[rules] Pruning expired entries...");
4159
+ expiredCount += await expireDecayedEntries(db, now, {
4160
+ dryRun,
4161
+ verbose,
4162
+ onLog,
4163
+ platform,
4164
+ project,
4165
+ excludeProject
4166
+ });
4167
+ onLog(`[rules] Pass ${pass}: merging near-exact duplicates...`);
4168
+ mergedCount += await mergeNearExactDuplicates(db, {
4169
+ dryRun,
4170
+ verbose,
4171
+ onLog,
4172
+ platform,
4173
+ project,
4174
+ excludeProject
4175
+ });
4176
+ }
4177
+ };
4129
4178
  if (dryRun) {
4130
- expiredCount = await expireDecayedEntries(db, now, { dryRun, verbose, onLog, platform, project, excludeProject });
4131
- mergedCount = await mergeNearExactDuplicates(db, { dryRun, verbose, onLog, platform, project, excludeProject });
4132
- orphanedRelationsCleaned = skipOrphanCleanup ? 0 : await cleanOrphanedRelations(db, true);
4179
+ await runRulePasses();
4180
+ if (!skipOrphanCleanup) {
4181
+ onLog("[rules] Cleaning orphaned relations...");
4182
+ orphanedRelationsCleaned = await cleanOrphanedRelations(db, true);
4183
+ }
4133
4184
  } else {
4134
4185
  await db.execute("BEGIN");
4135
4186
  try {
4136
4187
  await ensureExpiredSentinel(db);
4137
- expiredCount = await expireDecayedEntries(db, now, { dryRun, verbose, onLog, platform, project, excludeProject });
4138
- mergedCount = await mergeNearExactDuplicates(db, { dryRun, verbose, onLog, platform, project, excludeProject });
4139
- orphanedRelationsCleaned = skipOrphanCleanup ? 0 : await cleanOrphanedRelations(db, false);
4188
+ await runRulePasses();
4189
+ if (!skipOrphanCleanup) {
4190
+ onLog("[rules] Cleaning orphaned relations...");
4191
+ orphanedRelationsCleaned = await cleanOrphanedRelations(db, false);
4192
+ }
4140
4193
  await db.execute("COMMIT");
4141
4194
  } catch (error) {
4142
4195
  try {
@@ -4421,6 +4474,7 @@ async function buildClusters(db, options = {}) {
4421
4474
  const entryById = new Map(candidates.map((entry) => [entry.id, entry]));
4422
4475
  const unionFind = new UnionFind();
4423
4476
  const looseUnionPairs = /* @__PURE__ */ new Set();
4477
+ const llmDedupQueue = [];
4424
4478
  let llmDedupCalls = 0;
4425
4479
  let llmDedupMatches = 0;
4426
4480
  for (const entry of candidates) {
@@ -4450,14 +4504,21 @@ async function buildClusters(db, options = {}) {
4450
4504
  if (!llmClient) {
4451
4505
  continue;
4452
4506
  }
4453
- llmDedupCalls += 1;
4454
- const isSame = await llmDedupCheck(llmClient, entry, candidate);
4455
- if (isSame) {
4456
- llmDedupMatches += 1;
4457
- looseUnionPairs.add(key);
4458
- unionFind.union(entry.id, candidate.id);
4459
- }
4507
+ llmDedupQueue.push({ entry, candidate, key });
4508
+ }
4509
+ }
4510
+ for (const pair of llmDedupQueue) {
4511
+ if (!llmClient) {
4512
+ break;
4513
+ }
4514
+ llmDedupCalls += 1;
4515
+ const isSame = await llmDedupCheck(llmClient, pair.entry, pair.candidate);
4516
+ if (isSame) {
4517
+ llmDedupMatches += 1;
4518
+ looseUnionPairs.add(pair.key);
4519
+ unionFind.union(pair.entry.id, pair.candidate.id);
4460
4520
  }
4521
+ onLog(`[dedup] Checked ${llmDedupCalls}/${llmDedupQueue.length} pairs (${llmDedupMatches} matched)`);
4461
4522
  }
4462
4523
  const groups = /* @__PURE__ */ new Map();
4463
4524
  for (const entry of candidates) {
@@ -7916,6 +7977,10 @@ async function mergeCluster(db, cluster, llmClient, apiKey, options = {}) {
7916
7977
  );
7917
7978
  }
7918
7979
  }
7980
+ const subjectPreview = truncateContent(cluster.entries[0]?.subject ?? "", 60);
7981
+ onLog(
7982
+ `[merge-llm] Merging cluster of ${cluster.entries.length} entries (subject: "${subjectPreview || "unknown"}")`
7983
+ );
7919
7984
  let mergeResult = null;
7920
7985
  try {
7921
7986
  const response = await runSimpleStream({
@@ -8376,6 +8441,41 @@ async function runFinalization(db, dryRun, onWarn, deps) {
8376
8441
  async function processPhaseClusters(params, deps) {
8377
8442
  const stats = defaultClusterStats();
8378
8443
  stats.clustersFound = params.clusters.length;
8444
+ const onLog = params.options.onLog ?? (() => void 0);
8445
+ const showLiveProgress = process.stderr.isTTY && params.options.verbose !== true;
8446
+ const liveLineWidth = 120;
8447
+ const phaseStartedAt = Date.now();
8448
+ const formatEta = (ms) => {
8449
+ const seconds = Math.max(0, Math.round(ms / 1e3));
8450
+ if (seconds < 60) {
8451
+ return `${seconds}s`;
8452
+ }
8453
+ const minutes = Math.floor(seconds / 60);
8454
+ const remainingSeconds = seconds % 60;
8455
+ return remainingSeconds === 0 ? `${minutes}m` : `${minutes}m ${remainingSeconds}s`;
8456
+ };
8457
+ const updateLiveProgress = (clusterIndex, totalClusters) => {
8458
+ if (!showLiveProgress) {
8459
+ return;
8460
+ }
8461
+ const completedClusters = clusterIndex - 1;
8462
+ let etaSuffix = "";
8463
+ if (completedClusters >= 2) {
8464
+ const elapsedMs = Date.now() - phaseStartedAt;
8465
+ const etaMs = (totalClusters - completedClusters) * (elapsedMs / completedClusters);
8466
+ etaSuffix = ` ~${formatEta(etaMs)} remaining`;
8467
+ }
8468
+ process.stderr.write(
8469
+ `\rPhase ${params.phase}: Processing cluster ${clusterIndex}/${totalClusters} (${params.type}) ...${etaSuffix}`
8470
+ );
8471
+ };
8472
+ const clearLiveProgress = () => {
8473
+ if (!showLiveProgress) {
8474
+ return;
8475
+ }
8476
+ process.stderr.write("\r");
8477
+ process.stderr.write(`${" ".repeat(liveLineWidth)}\r`);
8478
+ };
8379
8479
  const pending = [];
8380
8480
  for (let i = 0; i < params.clusters.length; i += 1) {
8381
8481
  const cluster = params.clusters[i];
@@ -8395,10 +8495,14 @@ async function processPhaseClusters(params, deps) {
8395
8495
  params.context.batchReached = true;
8396
8496
  break;
8397
8497
  }
8498
+ clearLiveProgress();
8499
+ const clusterNumber = stats.clustersProcessed + 1;
8500
+ onLog(`[phase ${params.phase}] Processing cluster ${clusterNumber}/${pending.length}...`);
8501
+ updateLiveProgress(clusterNumber, pending.length);
8398
8502
  const outcome = await deps.mergeClusterFn(params.db, item.cluster, params.llmClient, params.embeddingApiKey, {
8399
8503
  dryRun: params.options.dryRun,
8400
8504
  verbose: params.options.verbose,
8401
- onLog: params.options.verbose ? params.options.onLog : void 0
8505
+ onLog
8402
8506
  });
8403
8507
  stats.clustersProcessed += 1;
8404
8508
  stats.llmCalls += 1;
@@ -8425,6 +8529,7 @@ async function processPhaseClusters(params, deps) {
8425
8529
  break;
8426
8530
  }
8427
8531
  }
8532
+ clearLiveProgress();
8428
8533
  return stats;
8429
8534
  }
8430
8535
  async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiKey, options = {}, deps = {}) {
@@ -8577,7 +8682,7 @@ async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiK
8577
8682
  skipBackup: projectIndex > 0,
8578
8683
  backupPath: projectIndex > 0 ? sharedBackupPath : void 0,
8579
8684
  skipOrphanCleanup: projectIndex > 0,
8580
- onLog: options.verbose ? onLog : void 0
8685
+ onLog
8581
8686
  });
8582
8687
  if (projectIndex === 0) {
8583
8688
  sharedBackupPath = phase0Stats.backupPath;
@@ -8636,7 +8741,7 @@ async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiK
8636
8741
  looseThreshold: options.looseThreshold,
8637
8742
  idempotencyDays: options.idempotencyDays,
8638
8743
  verbose: options.verbose,
8639
- onLog: options.verbose ? onLog : void 0,
8744
+ onLog,
8640
8745
  onStats: (stats) => {
8641
8746
  phase1ClusterStats = stats;
8642
8747
  }
@@ -8671,7 +8776,7 @@ async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiK
8671
8776
  looseThreshold: options.looseThreshold,
8672
8777
  idempotencyDays: options.idempotencyDays,
8673
8778
  verbose: options.verbose,
8674
- onLog: options.verbose ? onLog : void 0,
8779
+ onLog,
8675
8780
  onStats: (stats) => {
8676
8781
  phase2ClusterStats = stats;
8677
8782
  }
@@ -8834,7 +8939,7 @@ async function runConsolidationOrchestrator(db, dbPath, llmClient, embeddingApiK
8834
8939
  looseThreshold: options.looseThreshold,
8835
8940
  idempotencyDays: 0,
8836
8941
  verbose: options.verbose,
8837
- onLog: options.verbose ? onLog : void 0,
8942
+ onLog,
8838
8943
  onStats: (stats) => {
8839
8944
  phase3ClusterStats = stats;
8840
8945
  }
@@ -15865,7 +15970,7 @@ function parsePositiveInt2(value, fallback, label) {
15865
15970
  function formatMetric(value) {
15866
15971
  return value.toFixed(2);
15867
15972
  }
15868
- function formatCount(value) {
15973
+ function formatCount2(value) {
15869
15974
  const rounded = Math.round(value);
15870
15975
  if (Math.abs(value - rounded) < 1e-9) {
15871
15976
  return String(rounded);
@@ -16294,7 +16399,7 @@ function renderSummary(result, rangesBySession) {
16294
16399
  for (const session of result.sessions) {
16295
16400
  const range = rangesBySession.get(session.session);
16296
16401
  const avgEntryCount = mean(session.runs.map((run) => run.total_entries));
16297
- const countLabel = range !== void 0 ? `${formatCount(avgEntryCount)}/${range.min}-${range.max}` : `${formatCount(avgEntryCount)}/-`;
16402
+ const countLabel = range !== void 0 ? `${formatCount2(avgEntryCount)}/${range.min}-${range.max}` : `${formatCount2(avgEntryCount)}/-`;
16298
16403
  const passCount = session.runs.filter((run) => run.pass).length;
16299
16404
  lines.push(
16300
16405
  `${session.session.padEnd(sessionWidth)} ${formatMetric(session.mean_recall).padEnd(6)} ${formatMetric(session.mean_partial_recall).padEnd(8)} ${formatMetric(session.mean_precision).padEnd(9)} ${formatMetric(session.mean_composite).padEnd(9)} ${countLabel.padEnd(9)} ${passCount}/${result.runs}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.9.15",
3
+ "version": "0.9.16",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/openclaw-plugin/index.js"