agenr 0.12.0 → 0.12.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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.12.2] - 2026-03-22
4
+
5
+ ### Recall Scoping Fixes
6
+
7
+ - **Wildcard project passthrough.** `project: "*"` no longer gets silently dropped to `undefined` during recall request building. The wildcard marker now flows through the full stack to `hasWildcardProjectOverride` in `prepareRecallInputs`, enabling true cross-project recall.
8
+ - **Default project fallback for unscoped recall.** When an agent calls `agenr_recall` without an explicit `project`, the session's default project is now used as a `universal_first` hint — searching the default project first with null-project fallback. Previously, unscoped recall defaulted to null-project-only entries, silently returning near-empty results for project-heavy corpora.
9
+ - **Wildcard default when no project context.** When neither an explicit project nor a session default is available, unscoped recall now defaults to wildcard (`*`) cross-project search instead of null-project-only.
10
+
11
+ ### Browse Recall
12
+
13
+ - **Temporal proximity rebalancing.** Browse mode recall now prioritizes temporal proximity over importance with a diversity pass, better surfacing recent entries during temporal exploration.
14
+ - **Removed default 1d since window.** Browse mode no longer applies a default 1-day `since` window, allowing full temporal exploration of the corpus.
15
+
16
+ ### Update & Retire Improvements
17
+
18
+ - **Expiry changes via `agenr_update`.** The update tool now supports changing an entry's expiry tier (`core` → `permanent`, etc.) without retiring and re-creating it. Entry history (confirmations, recall count, created_at) is preserved.
19
+ - **Subject selectors for update and retire.** Both `agenr_update` and `agenr_retire` now accept `subject` as an alternative to `entry_id`. Subject matching is case-insensitive exact match; when multiple entries share a subject, the most recent is targeted.
20
+ - **Agent action replay.** Retire and update operations now support recall target hints for agent action replay workflows.
21
+
22
+ ### Maintenance
23
+
24
+ - **Vector integrity detection.** New `vector-integrity` maintain task detects and repairs drift between the vector shadow table and the entries table.
25
+
26
+ ## [0.12.1] - 2026-03-21
27
+
28
+ ### Post-Ingest Quality Fixes
29
+
30
+ - **Active-only FTS index.** FTS triggers and rebuild helpers now scope to active entries only (`retired = 0, superseded_by IS NULL`). Retired entries no longer occupy FTS slots or get reindexed during maintenance.
31
+ - **Active-only vector index.** The `idx_entries_embedding_shadow` partial index now excludes retired entries. Vector `top_k` queries no longer waste slots on retired embeddings that get post-filtered. Rebuild and health-check paths updated to match.
32
+ - **Schema regression test.** Fresh `init`/`reset` must create active-only trigger and index definitions — validated by new schema test.
33
+
34
+ ### Passthrough Dedup Fix
35
+
36
+ - **Normalized content hash dedup.** Passthrough now checks active `norm_content_hash` before insert, catching exact duplicates that survived because the previous `contentHash` included `source.file`. Identical content from different tool calls is now correctly deduplicated.
37
+ - **Within-batch norm-hash tracking.** Local batch dedup tracks both `contentHash` and `normContentHash`, preventing same-batch duplicates with different synthetic source files.
38
+ - **Granular skip tracking.** `stats.skipped` now counts by skipped candidates rather than matched set size.
39
+
40
+ ### Store Guidance
41
+
42
+ - **Future-session test.** Updated `agenr_store` tool description, Memory Doctrine (`system-context.ts`), and SKILL.md with concrete store/don't-store guidance. Agents are now instructed to apply the "future-session test" before storing: will a fresh session need this to act differently, or is this just logging what happened?
43
+ - **Importance calibration.** Added "importance is not recency" guidance — shipping events are 5-6, recurring operational hazards are 7-8.
44
+
3
45
  ## [0.12.0] - 2026-03-20
4
46
 
5
47
  ### Ingestion Overhaul
package/README.md CHANGED
@@ -162,7 +162,7 @@ agenr recall "package manager"
162
162
  tags: tooling, package-manager
163
163
  ```
164
164
 
165
- Recall supports date range queries (`--since 14d --until 7d`), temporal browse mode (`--browse --since 1d`), and around-date targeting (`--around 2026-02-15 --around-radius 14`) to rank entries by distance from a specific date.
165
+ Recall supports date range queries (`--since 14d --until 7d`), temporal browse mode (`--browse --since 1d`), and around-date targeting (`--around 2026-02-15 --around-radius 14`) to rank entries primarily by distance from a specific date with importance used as a secondary tiebreaker.
166
166
 
167
167
  ### Cross-session Handoff
168
168
 
@@ -257,7 +257,7 @@ This exposes four MCP tools: `agenr_recall`, `agenr_extract`, `agenr_retire`, an
257
257
  | `agenr store [files...]` | Store entries with semantic dedup |
258
258
  | `agenr recall [query]` | Semantic + memory-aware recall. Use `--since`/`--until` for date ranges, `--around` for target-date ranking, `--browse` for temporal mode. |
259
259
  | `agenr retire [subject]` | Retire a stale entry (hidden, not deleted). Match by subject or `--id`. |
260
- | `agenr update --id <id> --importance <n>` | Update an entry in place. Currently supports importance only. |
260
+ | `agenr update --id <id> [--importance <n>] [--expiry <level>]` | Update mutable entry metadata in place. Supports importance and expiry; pass at least one. |
261
261
  | `agenr watch [file]` | Live-watch files/directories, auto-extract knowledge |
262
262
  | `agenr watcher install` | Install background watch daemon (macOS launchd) |
263
263
  | `agenr watcher status` | Show daemon status (running/stopped, pid, watched file, recent logs) |
@@ -0,0 +1,36 @@
1
+ import {
2
+ computeMinhashSig,
3
+ computeNormContentHash,
4
+ findExistingContentHashes,
5
+ findExistingNormContentHashes,
6
+ hashEntrySourceContent,
7
+ insertEntry,
8
+ minhashSigToBuffer
9
+ } from "./chunk-UNB2GHB2.js";
10
+ import "./chunk-5645B45W.js";
11
+ import "./chunk-QDW77NBA.js";
12
+ import "./chunk-OZK32TEX.js";
13
+ import "./chunk-6HTA55NQ.js";
14
+ import {
15
+ composeEmbeddingText
16
+ } from "./chunk-RVPPKW4P.js";
17
+ import "./chunk-FIKYMSL6.js";
18
+ import "./chunk-7VFBBJVV.js";
19
+ import "./chunk-MLKGABMK.js";
20
+
21
+ // src/runtime/agent-store-passthrough-defaults.ts
22
+ function resolveAgentStorePassthroughDefaults() {
23
+ return {
24
+ computeMinhashSigFn: computeMinhashSig,
25
+ computeNormContentHashFn: computeNormContentHash,
26
+ minhashSigToBufferFn: minhashSigToBuffer,
27
+ hashEntrySourceContentFn: hashEntrySourceContent,
28
+ findExistingContentHashesFn: (db, hashes) => findExistingContentHashes(db, hashes),
29
+ findExistingNormContentHashesFn: (db, hashes) => findExistingNormContentHashes(db, hashes),
30
+ composeEmbeddingTextFn: composeEmbeddingText,
31
+ insertEntryFn: (db, entry, embedding, contentHash, normContentHash, minhashSig, config, options) => insertEntry(db, entry, embedding, contentHash, normContentHash, minhashSig, config, options)
32
+ };
33
+ }
34
+ export {
35
+ resolveAgentStorePassthroughDefaults
36
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  resolveBulkStoreDefaults
3
- } from "./chunk-DSOV3M3M.js";
4
- import "./chunk-Y4RTK5NN.js";
3
+ } from "./chunk-BR65GBJG.js";
4
+ import "./chunk-UNB2GHB2.js";
5
5
  import "./chunk-5645B45W.js";
6
6
  import "./chunk-QDW77NBA.js";
7
7
  import "./chunk-OZK32TEX.js";
@@ -5,7 +5,7 @@ import {
5
5
  hashEntrySourceContent,
6
6
  insertEntry,
7
7
  minhashSigToBuffer
8
- } from "./chunk-Y4RTK5NN.js";
8
+ } from "./chunk-UNB2GHB2.js";
9
9
  import {
10
10
  composeEmbeddingText
11
11
  } from "./chunk-RVPPKW4P.js";
@@ -0,0 +1,34 @@
1
+ // src/domain/watcher-demotion.ts
2
+ var WATCHER_DEMOTED_TAG = "watcher-demoted";
3
+ var DEFAULT_OPENCLAW_WATCHER_DEMOTION_IMPORTANCE_CAP = 4;
4
+ var DEFAULT_OPENCLAW_WATCHER_DEMOTION_TTL_DAYS = 30;
5
+ function clampInteger(value, floor, fallback) {
6
+ if (typeof value !== "number" || !Number.isFinite(value)) {
7
+ return fallback;
8
+ }
9
+ return Math.max(floor, Math.floor(value));
10
+ }
11
+ function resolveOpenClawWatcherDemotionConfig(config) {
12
+ const watcherConfig = config?.watcher?.openclawDemotion;
13
+ return {
14
+ enabled: watcherConfig?.enabled !== false,
15
+ importanceCap: Math.min(
16
+ 10,
17
+ clampInteger(
18
+ watcherConfig?.importanceCap,
19
+ 1,
20
+ DEFAULT_OPENCLAW_WATCHER_DEMOTION_IMPORTANCE_CAP
21
+ )
22
+ ),
23
+ ttlDays: clampInteger(
24
+ watcherConfig?.ttlDays,
25
+ 1,
26
+ DEFAULT_OPENCLAW_WATCHER_DEMOTION_TTL_DAYS
27
+ )
28
+ };
29
+ }
30
+
31
+ export {
32
+ WATCHER_DEMOTED_TAG,
33
+ resolveOpenClawWatcherDemotionConfig
34
+ };
@@ -29,7 +29,7 @@ import {
29
29
  recordEntrySupport,
30
30
  resolveEmbeddingForText,
31
31
  updateEntryForMerge
32
- } from "./chunk-Y4RTK5NN.js";
32
+ } from "./chunk-UNB2GHB2.js";
33
33
  import {
34
34
  applyEntryClassification,
35
35
  inferProjectFromTags,
@@ -47,7 +47,7 @@ import {
47
47
  createAppRuntime,
48
48
  readAppDbSession,
49
49
  resolveDefaultAppRuntimeDeps
50
- } from "./chunk-7IU43M5Q.js";
50
+ } from "./chunk-YFOFO2FC.js";
51
51
  import {
52
52
  mapStoredEntry,
53
53
  shapeRecallText
@@ -65,7 +65,7 @@ import {
65
65
  import {
66
66
  createLogger,
67
67
  walCheckpoint
68
- } from "./chunk-UBX652ZM.js";
68
+ } from "./chunk-HMBONTF3.js";
69
69
  import {
70
70
  DEFAULT_TASK_MODEL,
71
71
  resolveClaimExtractionBatchSize,
@@ -219,11 +219,11 @@ async function runSimpleStream(params) {
219
219
  continue;
220
220
  }
221
221
  if (event.type === "thinking_start") {
222
- logVerbose(params, "[thinking]");
222
+ continue;
223
223
  } else if (event.type === "thinking_delta") {
224
224
  params.onStreamDelta?.(event.delta, "thinking");
225
225
  } else if (event.type === "thinking_end") {
226
- logVerbose(params, "[/thinking]");
226
+ continue;
227
227
  } else if (event.type === "text_delta") {
228
228
  params.onStreamDelta?.(event.delta, "text");
229
229
  } else if (event.type === "toolcall_delta") {
@@ -948,6 +948,37 @@ async function buildEntityRegistry(db) {
948
948
  return buildEntityRegistryFromRows(rows);
949
949
  }
950
950
 
951
+ // src/utils/expiry.ts
952
+ var EXPIRY_SET = new Set(EXPIRY_LEVELS);
953
+ var EXPIRY_PRIORITY = {
954
+ temporary: 0,
955
+ permanent: 1,
956
+ core: 2
957
+ };
958
+ function normalizeExpiry(value) {
959
+ if (typeof value !== "string") {
960
+ return void 0;
961
+ }
962
+ const normalized = value.trim().toLowerCase();
963
+ if (!normalized) {
964
+ return void 0;
965
+ }
966
+ return EXPIRY_SET.has(normalized) ? normalized : void 0;
967
+ }
968
+ function coerceExpiry(value, fallback = "temporary") {
969
+ return normalizeExpiry(value) ?? fallback;
970
+ }
971
+ function resolveHigherExpiry(a, b) {
972
+ return EXPIRY_PRIORITY[a] >= EXPIRY_PRIORITY[b] ? a : b;
973
+ }
974
+ function resolveHighestExpiry(expiries) {
975
+ let highest = "temporary";
976
+ for (const expiry of expiries) {
977
+ highest = resolveHigherExpiry(highest, expiry);
978
+ }
979
+ return highest;
980
+ }
981
+
951
982
  // src/db/lockfile.ts
952
983
  import fs from "fs";
953
984
  import os from "os";
@@ -2301,37 +2332,6 @@ function evaluateConflictsForRejection(conflicts, newEntry, config) {
2301
2332
  };
2302
2333
  }
2303
2334
 
2304
- // src/utils/expiry.ts
2305
- var EXPIRY_SET = new Set(EXPIRY_LEVELS);
2306
- var EXPIRY_PRIORITY = {
2307
- temporary: 0,
2308
- permanent: 1,
2309
- core: 2
2310
- };
2311
- function normalizeExpiry(value) {
2312
- if (typeof value !== "string") {
2313
- return void 0;
2314
- }
2315
- const normalized = value.trim().toLowerCase();
2316
- if (!normalized) {
2317
- return void 0;
2318
- }
2319
- return EXPIRY_SET.has(normalized) ? normalized : void 0;
2320
- }
2321
- function coerceExpiry(value, fallback = "temporary") {
2322
- return normalizeExpiry(value) ?? fallback;
2323
- }
2324
- function resolveHigherExpiry(a, b) {
2325
- return EXPIRY_PRIORITY[a] >= EXPIRY_PRIORITY[b] ? a : b;
2326
- }
2327
- function resolveHighestExpiry(expiries) {
2328
- let highest = "temporary";
2329
- for (const expiry of expiries) {
2330
- highest = resolveHigherExpiry(highest, expiry);
2331
- }
2332
- return highest;
2333
- }
2334
-
2335
2335
  // src/memory/store/mutations.ts
2336
2336
  async function applyEntryMutation(repository, processed, embedFn, apiKey, cache, config, options) {
2337
2337
  const mutation = processed.mutation;
@@ -5598,6 +5598,47 @@ async function finalizeRecallResults(params) {
5598
5598
  return results;
5599
5599
  }
5600
5600
 
5601
+ // src/domain/recall/browse-selection.ts
5602
+ var DEFAULT_BROWSE_DIVERSITY_TARGET = 4;
5603
+ function normalizeEntryType(result) {
5604
+ const rawType = result.entry.type;
5605
+ return typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
5606
+ }
5607
+ function diversifyBrowseResults(results, limit, diversityTarget = DEFAULT_BROWSE_DIVERSITY_TARGET) {
5608
+ const normalizedLimit = Math.max(0, Math.floor(limit));
5609
+ if (normalizedLimit === 0 || results.length === 0) {
5610
+ return [];
5611
+ }
5612
+ const normalizedDiversityTarget = Math.max(0, Math.floor(diversityTarget));
5613
+ const seedTarget = Math.min(normalizedLimit, normalizedDiversityTarget);
5614
+ const selected = [];
5615
+ const selectedIds = /* @__PURE__ */ new Set();
5616
+ const admittedTypes = /* @__PURE__ */ new Set();
5617
+ for (const result of results) {
5618
+ if (selected.length >= seedTarget) {
5619
+ break;
5620
+ }
5621
+ const normalizedType = normalizeEntryType(result);
5622
+ if (!normalizedType || admittedTypes.has(normalizedType)) {
5623
+ continue;
5624
+ }
5625
+ selected.push(result);
5626
+ selectedIds.add(result.entry.id);
5627
+ admittedTypes.add(normalizedType);
5628
+ }
5629
+ for (const result of results) {
5630
+ if (selected.length >= normalizedLimit) {
5631
+ break;
5632
+ }
5633
+ if (selectedIds.has(result.entry.id)) {
5634
+ continue;
5635
+ }
5636
+ selected.push(result);
5637
+ selectedIds.add(result.entry.id);
5638
+ }
5639
+ return selected;
5640
+ }
5641
+
5601
5642
  // src/domain/recall/lexical.ts
5602
5643
  var STOP_TOKENS = /* @__PURE__ */ new Set([
5603
5644
  "a",
@@ -5754,6 +5795,8 @@ var DEFAULT_RECALL_SATURATION = 10;
5754
5795
  var DEFAULT_WARM_START_THRESHOLD = 3;
5755
5796
  var DEFAULT_SYNTHETIC_FLOOR = 0.1;
5756
5797
  var DEFAULT_AGENT_SOURCE_BONUS = 0.05;
5798
+ var BROWSE_RECENCY_WEIGHT = 0.8;
5799
+ var BROWSE_IMPORTANCE_WEIGHT = 0.2;
5757
5800
  var MISSING_RECALL_DAYS = 99999;
5758
5801
  var EMPTY_RECALL_METRICS = {
5759
5802
  totalCount: 0,
@@ -5878,7 +5921,9 @@ function browseRecencyFactor(entry, now, aroundDate, aroundRadius = DEFAULT_AROU
5878
5921
  return resolveRecallRecency(entry, now, aroundDate, aroundRadius);
5879
5922
  }
5880
5923
  function scoreBrowseEntry(entry, now, aroundDate, aroundRadius = DEFAULT_AROUND_RADIUS_DAYS) {
5881
- return clamp01((browseRecencyFactor(entry, now, aroundDate, aroundRadius) + importanceScore(entry.importance)) / 2);
5924
+ const recency2 = browseRecencyFactor(entry, now, aroundDate, aroundRadius);
5925
+ const importance = importanceScore(entry.importance);
5926
+ return clamp01(recency2 * BROWSE_RECENCY_WEIGHT + importance * BROWSE_IMPORTANCE_WEIGHT);
5882
5927
  }
5883
5928
  function scoreSessionEntry(entry, effectiveNow, freshnessNow, metricsMap, aroundDate, aroundRadius = DEFAULT_AROUND_RADIUS_DAYS, config) {
5884
5929
  return scoreEntryWithBreakdown(
@@ -6261,8 +6306,21 @@ async function fetchBrowseCandidates(params) {
6261
6306
  whereClauses.push("platform = ?");
6262
6307
  args.push(params.query.platform);
6263
6308
  }
6264
- args.push(params.limit);
6265
6309
  const whereClause = whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : "";
6310
+ const orderBy = params.aroundDate ? `
6311
+ ORDER BY
6312
+ ABS(julianday(created_at) - julianday(?)) ASC,
6313
+ importance DESC,
6314
+ created_at DESC
6315
+ ` : `
6316
+ ORDER BY
6317
+ created_at DESC,
6318
+ importance DESC
6319
+ `;
6320
+ if (params.aroundDate) {
6321
+ args.push(params.aroundDate.toISOString());
6322
+ }
6323
+ args.push(params.limit);
6266
6324
  const result = await params.db.execute({
6267
6325
  sql: `
6268
6326
  SELECT
@@ -6270,10 +6328,10 @@ async function fetchBrowseCandidates(params) {
6270
6328
  FROM entries
6271
6329
  ${whereClause}
6272
6330
  -- SQL pre-sort is a best-effort approximation only.
6273
- -- Final order is determined by scoreBrowseEntry() (importance * recency decay)
6274
- -- which re-sorts post-fetch. The over-fetch buffer (limit*3, min 50)
6331
+ -- Final order is determined by recency-first browse scoring and a post-score
6332
+ -- diversity pass. The over-fetch buffer (limit*3, min 50)
6275
6333
  -- ensures the final top-N are present in the candidate pool.
6276
- ORDER BY importance DESC, created_at DESC
6334
+ ${orderBy}
6277
6335
  LIMIT ?
6278
6336
  `,
6279
6337
  args
@@ -6499,6 +6557,7 @@ async function recallBrowseMode(params) {
6499
6557
  query: browseQuery,
6500
6558
  limit: browseLimit,
6501
6559
  now: params.now,
6560
+ aroundDate: params.prepared.temporal.aroundDate,
6502
6561
  projectFilter: params.prepared.scope.resolution.primaryFilter
6503
6562
  });
6504
6563
  const filtered = browseCandidates.filter(
@@ -6519,7 +6578,11 @@ async function recallBrowseMode(params) {
6519
6578
  aroundRadiusDays: params.prepared.temporal.aroundRadiusDays
6520
6579
  });
6521
6580
  scored.sort((left, right) => right.score - left.score);
6522
- return scored.slice(0, requestedLimit);
6581
+ const results = diversifyBrowseResults(scored, requestedLimit, DEFAULT_BROWSE_DIVERSITY_TARGET);
6582
+ if (params.trace) {
6583
+ params.trace.finalResultIds = results.map((result) => result.entry.subject ?? result.entry.id);
6584
+ }
6585
+ return results;
6523
6586
  }
6524
6587
  async function retrieveCandidates(params) {
6525
6588
  if (!params.prepared.retrievalSearchText) {
@@ -6924,11 +6987,12 @@ function resolvePreparedRecallScope(query, normalizedQuery) {
6924
6987
  const explicitWildcardProject = hasWildcardProjectOverride(query.project);
6925
6988
  const explicitProjects = explicitWildcardProject ? [] : parseProjectList(query.project);
6926
6989
  const explicitExcludedProjects = parseProjectList(query.excludeProject);
6927
- const inferredProjectHints = deriveRecallProjectScopeHints({
6990
+ const inferredProjectHints = explicitWildcardProject ? [] : deriveRecallProjectScopeHints({
6928
6991
  queryText: normalizedQuery?.searchText ?? query.text ?? "",
6929
6992
  explicitProjects
6930
6993
  });
6931
- const projectHints = explicitProjects.length > 0 ? explicitProjects : inferredProjectHints;
6994
+ const callerProjectHints = explicitProjects.length > 0 || explicitWildcardProject ? [] : parseProjectList(query.projectHints).filter((projectHint) => projectHint !== "*");
6995
+ const projectHints = explicitProjects.length > 0 ? explicitProjects : Array.from(/* @__PURE__ */ new Set([...inferredProjectHints, ...callerProjectHints]));
6932
6996
  const intent = projectHints.length > 0 ? "project" : normalizedQuery?.scopeIntent ?? "ambiguous";
6933
6997
  if (query.universalOnly === true) {
6934
6998
  return {