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 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.82;
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.82;
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 scan for undiscovered contradictions with `query_contradiction_candidates`, log confirmed pairs with `log_conflict`, and resolve them.
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. Start with the highest-priority pass that has work to do. If there are pending conflicts, start with contradictions. If none, check for dedup clusters. If none, start retirement.
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 (for example, no pending conflicts), skip calling `complete_pass` for it - just move to the next pass.
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: Proactive scanning is secondary to resolving known pending conflicts. If budget is tight after `query_conflicts`, skip the scan and note it in your `complete_pass` recommendations.
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/edge/openclaw/index.js"