agenr 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/cli-main.js +90 -4
- package/dist/modules/surgeon/adapters/prompts/passes/auto.md +16 -4
- package/dist/modules/surgeon/adapters/prompts/passes/contradictions.md +3 -1
- package/dist/modules/surgeon/adapters/prompts/passes/dedup.md +8 -0
- package/dist/modules/surgeon/adapters/prompts/passes/retirement.md +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.2] - 2026-03-23
|
|
4
|
+
|
|
5
|
+
### Surgeon
|
|
6
|
+
|
|
7
|
+
- **Continuation loop prevents early exit.** If the surgeon model stops without calling `complete_pass` and has >10% budget remaining, a continuation prompt is injected to push it back to work. Up to 3 nudges before allowing exit. Eliminates the "surgeon quits at 1% budget" problem.
|
|
8
|
+
- **Lowered default dedup similarity threshold from 0.82 to 0.60.** The threshold controls candidate surfacing, not merge execution — the surgeon agent still makes every merge decision. Lower threshold surfaces more candidates for review on large corpora.
|
|
9
|
+
- **`reset` parameter for `query_dedup_clusters` and `query_contradiction_candidates`.** Query parameters are no longer permanently frozen after the first call. Pass `reset: true` to clear cached clusters and rebuild at a new threshold. Lets the surgeon start wide and narrow if noisy.
|
|
10
|
+
- **Strengthened auto sweep prompts.** Contradictions phase always runs proactive scan (no more skipping when pending conflicts = 0). Budget discipline section added — surgeon must keep working while budget remains. Retirement throughput expectations: 500+ candidates on a 3K corpus, not 100.
|
|
11
|
+
- **Dedup threshold guidance in prompts.** Surgeon is told the default is deliberately low, and can raise it via reset if too noisy.
|
|
12
|
+
|
|
3
13
|
## [0.13.1] - 2026-03-23
|
|
4
14
|
|
|
5
15
|
### MCP Server
|
package/dist/cli-main.js
CHANGED
|
@@ -20772,7 +20772,7 @@ function validateClusterWithSupport(group, maxSize, diameterFloor, supportGraph)
|
|
|
20772
20772
|
}
|
|
20773
20773
|
|
|
20774
20774
|
// src/modules/surgeon/application/clustering/cluster.ts
|
|
20775
|
-
var DEFAULT_SIMILARITY_THRESHOLD2 = 0.
|
|
20775
|
+
var DEFAULT_SIMILARITY_THRESHOLD2 = 0.6;
|
|
20776
20776
|
var CROSS_TYPE_SUBJECT_THRESHOLD = 0.89;
|
|
20777
20777
|
var DEFAULT_MIN_CLUSTER = 2;
|
|
20778
20778
|
var DEFAULT_MAX_CLUSTER_SIZE2 = 12;
|
|
@@ -23402,6 +23402,10 @@ var QUERY_CONTRADICTION_CANDIDATES_SCHEMA = Type4.Object({
|
|
|
23402
23402
|
default: false,
|
|
23403
23403
|
description: "If true, only return pairs sharing a subject_key. If false, also find semantically similar cross-subject pairs."
|
|
23404
23404
|
})),
|
|
23405
|
+
reset: Type4.Optional(Type4.Boolean({
|
|
23406
|
+
default: false,
|
|
23407
|
+
description: "If true, clear the cached contradiction scan for this run and rebuild it with the current scan settings."
|
|
23408
|
+
})),
|
|
23405
23409
|
limit: Type4.Optional(Type4.Integer({ minimum: 1, maximum: 50, default: 20 })),
|
|
23406
23410
|
offset: Type4.Optional(Type4.Integer({ minimum: 0 }))
|
|
23407
23411
|
});
|
|
@@ -23441,9 +23445,12 @@ function createQueryContradictionCandidatesTool(deps) {
|
|
|
23441
23445
|
return {
|
|
23442
23446
|
name: "query_contradiction_candidates",
|
|
23443
23447
|
label: "Query contradiction candidates",
|
|
23444
|
-
description: "Scan the active corpus for potential undiscovered contradictions. Finds pairs of entries that are semantically similar or share structured claim predicates but assert different things. Pairs already in conflict_log are marked but still returned. Use inspect_entry to evaluate promising pairs, then resolve_conflict to fix confirmed contradictions or flag_for_review for ambiguous cases.",
|
|
23448
|
+
description: "Scan the active corpus for potential undiscovered contradictions. Finds pairs of entries that are semantically similar or share structured claim predicates but assert different things. Pairs already in conflict_log are marked but still returned. Use inspect_entry to evaluate promising pairs, then resolve_conflict to fix confirmed contradictions or flag_for_review for ambiguous cases. If you need to rebuild the scan with different thresholds, call with reset=true.",
|
|
23445
23449
|
parameters: QUERY_CONTRADICTION_CANDIDATES_SCHEMA,
|
|
23446
23450
|
async execute(_toolCallId, params) {
|
|
23451
|
+
if (params.reset === true) {
|
|
23452
|
+
cached = null;
|
|
23453
|
+
}
|
|
23447
23454
|
const query = buildQuery(params, deps);
|
|
23448
23455
|
const offset = normalizeOffset2(params.offset);
|
|
23449
23456
|
const limit = normalizeLimit2(params.limit);
|
|
@@ -23750,7 +23757,7 @@ function createResolveConflictTool(deps) {
|
|
|
23750
23757
|
import { Type as Type7 } from "@sinclair/typebox";
|
|
23751
23758
|
|
|
23752
23759
|
// src/modules/surgeon/application/dedup-clusters.ts
|
|
23753
|
-
var DEFAULT_SIM_THRESHOLD = 0.
|
|
23760
|
+
var DEFAULT_SIM_THRESHOLD = 0.6;
|
|
23754
23761
|
var CLUSTER_PREVIEW_MAX_CHARS = 200;
|
|
23755
23762
|
var UNSCOPED_PROJECT_LABEL = "(unscoped)";
|
|
23756
23763
|
var DAY_MS3 = 24 * 60 * 60 * 1e3;
|
|
@@ -23868,6 +23875,11 @@ function getCachedEligibleDedupClusters(cache) {
|
|
|
23868
23875
|
clusters: cache.eligibleClusters
|
|
23869
23876
|
};
|
|
23870
23877
|
}
|
|
23878
|
+
function resetDedupClusterCache(cache) {
|
|
23879
|
+
cache.rawClusters = null;
|
|
23880
|
+
cache.eligibleClusters = null;
|
|
23881
|
+
cache.frozenQuery = null;
|
|
23882
|
+
}
|
|
23871
23883
|
async function loadEligibleDedupClusters(db, input) {
|
|
23872
23884
|
const cached = getCachedEligibleDedupClusters(input.cache);
|
|
23873
23885
|
if (cached) {
|
|
@@ -24031,6 +24043,10 @@ var QUERY_DEDUP_CLUSTERS_SCHEMA = Type8.Object({
|
|
|
24031
24043
|
project: Type8.Optional(Type8.String()),
|
|
24032
24044
|
type: Type8.Optional(Type8.String()),
|
|
24033
24045
|
sim_threshold: Type8.Optional(Type8.Number({ minimum: 0.5, maximum: 1 })),
|
|
24046
|
+
reset: Type8.Optional(Type8.Boolean({
|
|
24047
|
+
default: false,
|
|
24048
|
+
description: "If true, clear the cached cluster scan for this run and rebuild it with the current filters so you can adjust thresholds mid-run."
|
|
24049
|
+
})),
|
|
24034
24050
|
limit: Type8.Optional(Type8.Integer({ minimum: 1, maximum: 20 })),
|
|
24035
24051
|
offset: Type8.Optional(Type8.Integer({ minimum: 0 }))
|
|
24036
24052
|
});
|
|
@@ -24050,12 +24066,15 @@ function createQueryDedupClustersTool(deps) {
|
|
|
24050
24066
|
return {
|
|
24051
24067
|
name: "query_dedup_clusters",
|
|
24052
24068
|
label: "Query dedup clusters",
|
|
24053
|
-
description: "Retrieve clusters of potentially duplicate entries. Each cluster groups entries with high embedding similarity or identical structured claims. Returns cluster summaries with entry previews. Use offset for pagination.",
|
|
24069
|
+
description: "Retrieve clusters of potentially duplicate entries. Each cluster groups entries with high embedding similarity or identical structured claims. Returns cluster summaries with entry previews. Use offset for pagination. If you need to rebuild the candidate set with a different threshold or scope, call with reset=true.",
|
|
24054
24070
|
parameters: QUERY_DEDUP_CLUSTERS_SCHEMA,
|
|
24055
24071
|
async execute(_toolCallId, params) {
|
|
24056
24072
|
if (!deps.clusterCache) {
|
|
24057
24073
|
throw new Error("Dedup cluster cache is unavailable for this run.");
|
|
24058
24074
|
}
|
|
24075
|
+
if (params.reset === true) {
|
|
24076
|
+
resetDedupClusterCache(deps.clusterCache);
|
|
24077
|
+
}
|
|
24059
24078
|
const query = normalizeDedupClusterQuery(
|
|
24060
24079
|
{
|
|
24061
24080
|
project: params.project,
|
|
@@ -24927,6 +24946,9 @@ async function captureBrainHealthSnapshot(db) {
|
|
|
24927
24946
|
// src/modules/surgeon/application/workflow.ts
|
|
24928
24947
|
var USER_ABORT_ERROR = "Run aborted by user (SIGINT).";
|
|
24929
24948
|
var USER_ABORT_SUMMARY = "Run aborted by user.";
|
|
24949
|
+
var MAX_CONTINUATION_ATTEMPTS = 3;
|
|
24950
|
+
var LOW_BUDGET_FRACTION = 0.1;
|
|
24951
|
+
var SHALLOW_RUN_WARNING_BUDGET_USED_FRACTION = 0.5;
|
|
24930
24952
|
function resolveRunBudget(options, config) {
|
|
24931
24953
|
if (Number.isFinite(options.budget) && options.budget > 0) {
|
|
24932
24954
|
return Math.floor(options.budget);
|
|
@@ -25112,6 +25134,7 @@ function buildInitialUserPrompt(options, stats, tokenBudget, dedupClusterCount,
|
|
|
25112
25134
|
`Last surgeon run: ${stats.lastRun ? `${stats.lastRun.passType} ${stats.lastRun.status} at ${stats.lastRun.startedAt}` : "none"}.`,
|
|
25113
25135
|
`Your budget is ${tokenBudget} tokens for the entire sweep.`,
|
|
25114
25136
|
"Work through passes in priority order: contradictions -> dedup -> retirement.",
|
|
25137
|
+
"Always run the proactive contradiction scan before dedup, even when pending conflicts start at 0.",
|
|
25115
25138
|
"Call complete_pass with the pass_type for each phase transition, and complete_pass with pass_type='auto' when the full sweep is done."
|
|
25116
25139
|
].join(" ");
|
|
25117
25140
|
}
|
|
@@ -25128,6 +25151,31 @@ function buildInitialUserPrompt(options, stats, tokenBudget, dedupClusterCount,
|
|
|
25128
25151
|
"Work conservatively and use complete_pass when you are done."
|
|
25129
25152
|
].join(" ");
|
|
25130
25153
|
}
|
|
25154
|
+
function buildContinuationPrompt(options, input) {
|
|
25155
|
+
const lines = [
|
|
25156
|
+
`You stopped without calling complete_pass and still have ${input.remainingTokens} tokens and about $${input.remainingCostUsd.toFixed(2)} of run budget remaining.`
|
|
25157
|
+
];
|
|
25158
|
+
if (options.pass === "auto") {
|
|
25159
|
+
lines.push(
|
|
25160
|
+
"Continue the auto sweep. If contradictions are not fully scanned, resume there first. Otherwise continue with the next unfinished phase in order: contradictions, dedup, retirement."
|
|
25161
|
+
);
|
|
25162
|
+
lines.push(
|
|
25163
|
+
"Do not call complete_pass with pass_type='auto' until the full sweep is genuinely done."
|
|
25164
|
+
);
|
|
25165
|
+
} else {
|
|
25166
|
+
lines.push(`Continue the ${options.pass} pass.`);
|
|
25167
|
+
lines.push(
|
|
25168
|
+
"Do not call complete_pass until candidates are genuinely exhausted or budget is low."
|
|
25169
|
+
);
|
|
25170
|
+
}
|
|
25171
|
+
lines.push("Keep paginating and evaluating candidates.");
|
|
25172
|
+
lines.push("A healthy-looking batch or a few blocked candidates are not reasons to stop.");
|
|
25173
|
+
lines.push(
|
|
25174
|
+
"If contradiction or dedup scans feel too narrow or too noisy, call the query tool again with reset=true and adjusted thresholds."
|
|
25175
|
+
);
|
|
25176
|
+
lines.push(`This is continuation attempt ${input.attempt} of ${MAX_CONTINUATION_ATTEMPTS}.`);
|
|
25177
|
+
return lines.join(" ");
|
|
25178
|
+
}
|
|
25131
25179
|
function buildStoredSummary(passType, summary, phaseCompletions, snapshots) {
|
|
25132
25180
|
if (!summary) {
|
|
25133
25181
|
return null;
|
|
@@ -25299,6 +25347,7 @@ async function runSurgeon(options, deps) {
|
|
|
25299
25347
|
};
|
|
25300
25348
|
let terminalStatus = null;
|
|
25301
25349
|
let terminalError = null;
|
|
25350
|
+
let continuationAttempts = 0;
|
|
25302
25351
|
async function finalizeRun(status, error, summaryOverride) {
|
|
25303
25352
|
if (beforeSnapshot && !afterSnapshot) {
|
|
25304
25353
|
afterSnapshot = await captureBrainHealthSnapshot(deps.db);
|
|
@@ -25479,6 +25528,30 @@ async function runSurgeon(options, deps) {
|
|
|
25479
25528
|
convertToLlm,
|
|
25480
25529
|
toolExecution: "sequential",
|
|
25481
25530
|
getApiKey: deps.getApiKey,
|
|
25531
|
+
getFollowUpMessages: async () => {
|
|
25532
|
+
if (completionState.isComplete || signal?.aborted || budgetTracker.isExhausted() || budgetTracker.isCostCapExceeded() || continuationAttempts >= MAX_CONTINUATION_ATTEMPTS) {
|
|
25533
|
+
return [];
|
|
25534
|
+
}
|
|
25535
|
+
const remaining = budgetTracker.remaining();
|
|
25536
|
+
const tokenRemainingFraction = tokenBudget > 0 ? remaining.tokens / tokenBudget : 0;
|
|
25537
|
+
const costRemainingFraction = runCostCap > 0 ? remaining.costUsd / runCostCap : 0;
|
|
25538
|
+
if (tokenRemainingFraction < LOW_BUDGET_FRACTION || costRemainingFraction < LOW_BUDGET_FRACTION) {
|
|
25539
|
+
return [];
|
|
25540
|
+
}
|
|
25541
|
+
continuationAttempts += 1;
|
|
25542
|
+
log21.warn(
|
|
25543
|
+
`Surgeon stopped without completing. Injecting continuation prompt (${continuationAttempts}/${MAX_CONTINUATION_ATTEMPTS}) with ${remaining.tokens} tokens and $${remaining.costUsd.toFixed(2)} remaining.`
|
|
25544
|
+
);
|
|
25545
|
+
return [{
|
|
25546
|
+
role: "user",
|
|
25547
|
+
content: buildContinuationPrompt(options, {
|
|
25548
|
+
remainingTokens: remaining.tokens,
|
|
25549
|
+
remainingCostUsd: remaining.costUsd,
|
|
25550
|
+
attempt: continuationAttempts
|
|
25551
|
+
}),
|
|
25552
|
+
timestamp: Date.now()
|
|
25553
|
+
}];
|
|
25554
|
+
},
|
|
25482
25555
|
beforeToolCall: async (context) => {
|
|
25483
25556
|
registerUsage(context.assistantMessage);
|
|
25484
25557
|
if (signal?.aborted) {
|
|
@@ -25580,6 +25653,19 @@ async function runSurgeon(options, deps) {
|
|
|
25580
25653
|
summarizeCompletion(completionState.summary, completionState.passCompletions) ?? USER_ABORT_SUMMARY
|
|
25581
25654
|
);
|
|
25582
25655
|
}
|
|
25656
|
+
const totals = budgetTracker.totals();
|
|
25657
|
+
const budgetUsedPct = Math.min(
|
|
25658
|
+
1,
|
|
25659
|
+
Math.max(
|
|
25660
|
+
runCostCap > 0 ? totals.costUsd / runCostCap : 1,
|
|
25661
|
+
tokenBudget > 0 ? (totals.inputTokens + totals.outputTokens) / tokenBudget : 1
|
|
25662
|
+
)
|
|
25663
|
+
);
|
|
25664
|
+
if (!completionState.isComplete && !budgetTracker.isExhausted() && !budgetTracker.isCostCapExceeded() && budgetUsedPct < SHALLOW_RUN_WARNING_BUDGET_USED_FRACTION) {
|
|
25665
|
+
log21.warn(
|
|
25666
|
+
`Surgeon ended without calling complete_pass and left ${((1 - budgetUsedPct) * 100).toFixed(0)}% of the run budget unused. The run may have quit early. Re-run with --verbose to inspect the trace.`
|
|
25667
|
+
);
|
|
25668
|
+
}
|
|
25583
25669
|
const finalStatus = completionState.summary ? terminalStatus && terminalStatus !== "failed" ? terminalStatus : "completed" : terminalStatus ?? "completed";
|
|
25584
25670
|
return finalizeRun(finalStatus, terminalError);
|
|
25585
25671
|
} catch (error) {
|
|
@@ -6,15 +6,15 @@ You have access to ALL surgeon tools across ALL pass types. Your job is to work
|
|
|
6
6
|
|
|
7
7
|
Work through passes in this priority order:
|
|
8
8
|
|
|
9
|
-
1. **Contradictions** - Resolve pending conflicts first. Active inconsistencies degrade corpus trust. Use `query_conflicts` and `resolve_conflict`.
|
|
10
|
-
Then
|
|
9
|
+
1. **Contradictions** - Always start here. Resolve pending conflicts first. Active inconsistencies degrade corpus trust. Use `query_conflicts` and `resolve_conflict`.
|
|
10
|
+
Then run a proactive scan with `query_contradiction_candidates`, even if `query_conflicts` returned zero pending conflicts. Log confirmed pairs with `log_conflict` and resolve them.
|
|
11
11
|
2. **Dedup** - Merge duplicate entries next. Duplicates waste recall bandwidth and confuse retrieval. Use `query_dedup_clusters` and `merge_cluster`.
|
|
12
12
|
3. **Retirement** - Clean up stale entries last. Use `query_candidates` and `retire_entry`. After standard candidates, scan for supersession chains with `query_supersession_candidates`.
|
|
13
13
|
|
|
14
14
|
## Workflow
|
|
15
15
|
|
|
16
16
|
1. Call `get_health_stats` to orient.
|
|
17
|
-
2.
|
|
17
|
+
2. **Always start with contradictions.** First resolve pending conflicts via `query_conflicts`. Then, regardless of whether there were pending conflicts, run a proactive scan using `query_contradiction_candidates` to find undiscovered contradictions. Only move to dedup after both pending conflicts are resolved and the proactive scan is complete, or the contradictions budget allocation is genuinely spent.
|
|
18
18
|
3. Work through that pass's candidates using the same methodology described in its individual pass instructions.
|
|
19
19
|
4. When a pass is complete (candidates exhausted or no more productive work), call `complete_pass` with `pass_type` set to the pass you just finished (for example, `"contradictions"`).
|
|
20
20
|
5. After completing a pass, move to the next priority pass. You do not need to call `get_health_stats` again - check for work by calling the next pass's query tool directly.
|
|
@@ -40,6 +40,18 @@ Rough budget allocation guideline (not rigid):
|
|
|
40
40
|
|
|
41
41
|
If a pass has no work, its budget share rolls into the next pass.
|
|
42
42
|
|
|
43
|
+
## Budget Discipline
|
|
44
|
+
|
|
45
|
+
**Do not stop early.** Your budget exists to be used. If you have remaining budget and there are still candidates to evaluate, keep working. Finishing a sweep at 1% of budget on a corpus of thousands of entries means you barely looked.
|
|
46
|
+
|
|
47
|
+
Concrete rules:
|
|
48
|
+
|
|
49
|
+
- After each `complete_pass` for a phase transition, check your remaining budget. If significant budget remains, the next phase should use it.
|
|
50
|
+
- For retirement: page through all candidates until `query_candidates` returns empty or budget is genuinely low, meaning less than 10% remains. Seeing a batch of healthy entries is not a reason to stop. The next batch may contain stale entries.
|
|
51
|
+
- For contradictions: the proactive scan is mandatory in auto mode. Zero pending conflicts from `query_conflicts` is not enough to move on.
|
|
52
|
+
- For dedup: if the phase produces very few clusters, note that observation. A corpus of thousands of entries typically has many more than a handful of near-duplicate candidates.
|
|
53
|
+
- If you reach `complete_pass` with `pass_type = "auto"` after using less than 50% of your budget, reconsider. Go back and do deeper evaluation: inspect more dedup clusters, reset the contradiction or dedup query with wider thresholds if needed, or run a broader retirement sweep.
|
|
54
|
+
|
|
43
55
|
## Complete Pass Calls
|
|
44
56
|
|
|
45
57
|
Call `complete_pass` once per phase transition:
|
|
@@ -49,6 +61,6 @@ Call `complete_pass` once per phase transition:
|
|
|
49
61
|
- After finishing retirement work: `complete_pass` with `pass_type = "retirement"`
|
|
50
62
|
- After all passes are done: `complete_pass` with `pass_type = "auto"` (this is the final one)
|
|
51
63
|
|
|
52
|
-
If a pass has zero work
|
|
64
|
+
If a pass has zero actionable work after running its required discovery steps, you may move to the next pass without calling `complete_pass` for that phase.
|
|
53
65
|
|
|
54
66
|
The final `complete_pass` with `pass_type = "auto"` should include observations and recommendations spanning all passes you worked through.
|
|
@@ -42,6 +42,8 @@ Keep working until conflicts are exhausted or budget is low. Call `complete_pass
|
|
|
42
42
|
|
|
43
43
|
After resolving pending conflicts from `query_conflicts`, scan for undiscovered contradictions using `query_contradiction_candidates`. This finds pairs of active entries that the ingestion pipeline never compared - entries from different sessions, with different subject normalization, or from different project scopes.
|
|
44
44
|
|
|
45
|
+
**Do not skip proactive scanning.** Even if `query_conflicts` returned zero pending conflicts, the proactive scan via `query_contradiction_candidates` is mandatory. Undiscovered contradictions are the most dangerous kind because they silently degrade corpus quality without appearing in the pending conflict queue.
|
|
46
|
+
|
|
45
47
|
For each candidate pair:
|
|
46
48
|
|
|
47
49
|
1. **Check if already known** - If `existingConflictLogId` is present, the conflict is already logged. Skip it or inspect the entries if you need more context.
|
|
@@ -57,7 +59,7 @@ For each candidate pair:
|
|
|
57
59
|
|
|
58
60
|
**Prioritize claim divergence pairs** (strategy `"claim_divergence"`) over embedding similarity pairs. Claim divergence means two entries share the same subject and predicate but assert different objects - these are usually real conflicts. Embedding similarity pairs need more careful evaluation.
|
|
59
61
|
|
|
60
|
-
Budget note:
|
|
62
|
+
Budget note: Known pending conflicts still take priority, but proactive scanning is part of completing this pass. Only stop before the scan is exhausted when budget is genuinely low. If that happens, note the incomplete scan in your `complete_pass` recommendations.
|
|
61
63
|
|
|
62
64
|
## Resolution Quality Rules
|
|
63
65
|
|
|
@@ -31,6 +31,14 @@ For each cluster from `query_dedup_clusters`:
|
|
|
31
31
|
- Preserve the strongest form. Prefer the most specific, complete, and well-supported version of the knowledge.
|
|
32
32
|
- Treat recently consolidated entries with extra caution. If `merged_from > 0` and `consolidated_at` is recent, inspect before merging again.
|
|
33
33
|
|
|
34
|
+
## Threshold Guidance
|
|
35
|
+
|
|
36
|
+
The similarity threshold controls which entry pairs are surfaced as candidates for your review. It does not control whether entries actually get merged. Merging always requires your decision after reading the entries.
|
|
37
|
+
|
|
38
|
+
The default threshold is deliberately low (`0.60`) so the candidate net is wide. That will surface some clear duplicates, some borderline cases, and some related-but-distinct entries. That is expected. Your job is to evaluate each cluster and decide: merge, flag, or skip.
|
|
39
|
+
|
|
40
|
+
If the current threshold is too noisy or too narrow, call `query_dedup_clusters` with `reset = true` and a different `sim_threshold`. Reset clears the cached cluster set for this run and rebuilds it with your new parameters. Start wide, then tighten only if the candidate stream is mostly noise.
|
|
41
|
+
|
|
34
42
|
## Working Through Clusters
|
|
35
43
|
|
|
36
44
|
You will receive clusters in batches.
|
|
@@ -37,6 +37,10 @@ An empty `query_candidates` result means there are no more candidates matching t
|
|
|
37
37
|
|
|
38
38
|
A productive pass works through hundreds of candidates, not dozens.
|
|
39
39
|
|
|
40
|
+
**Budget awareness:** If your budget allows examining more candidates, you must keep paginating. Do not call `complete_pass` while `query_candidates` is still returning candidates and budget is available. A batch where most entries are healthy is normal in a healthy corpus. That does not mean the pass is done. Keep going. The stale entries are mixed throughout the candidate pool.
|
|
41
|
+
|
|
42
|
+
Expected throughput: On a corpus of around 3000 entries, a retirement pass with adequate budget should evaluate at least 500 candidates and often more. If you stop at 100, you probably have not done enough.
|
|
43
|
+
|
|
40
44
|
## Type-Specific Heuristics
|
|
41
45
|
|
|
42
46
|
Different entry types have different retirement profiles:
|