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 +11 -0
- package/dist/cli-main.js +125 -20
- package/package.json +1 -1
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
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
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
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
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
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ? `${
|
|
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}`
|