agenr 0.7.13 → 0.7.14

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.14] - 2026-02-21
4
+
5
+ ### Added
6
+ - feat(recall): added `until` upper date bound to recall query filtering in CLI, MCP, and DB recall paths (`since` + `until` now define an inclusive window)
7
+
8
+ ### Changed
9
+ - fix(recall): recency decay now anchors to the `until` ceiling for historical windows while freshness boost remains anchored to real query time
10
+ - fix(recall): centralized `parseSinceToIso` in `src/utils/time.ts` and removed duplicate implementations from recall CLI and MCP server
11
+ - fix(recall): added inverted date-range validation - recall now returns a descriptive error when `since > until` instead of returning an empty list
12
+ - fix(recall): interim 3x candidate over-fetch under date bounds to improve in-window recall coverage until SQL-level date filtering is added
13
+ - fix(recall): corrupt `created_at` values are now safely excluded under date-bound filters instead of leaking invalid rows into filtered recall
14
+
3
15
  ## [0.7.13] - 2026-02-21
4
16
 
5
17
  ### Fixed
package/README.md CHANGED
@@ -161,7 +161,7 @@ agenr watch --platform openclaw --context ~/.agenr/CONTEXT.md
161
161
 
162
162
  ## How it works
163
163
 
164
- **Extract** - An LLM reads your transcripts and pulls out structured entries: facts, decisions, preferences, todos, relationships, events, lessons. Smart filtering removes noise (tool calls, file contents, boilerplate) before the LLM ever sees it.
164
+ **Extract** - An LLM reads your transcripts and pulls out structured entries: facts, decisions, preferences, todos, relationships, events, lessons. Smart filtering removes noise (tool calls, file contents, boilerplate) before the LLM ever sees it. For OpenClaw sessions, hedged or unverified agent claims are detected and capped at importance 5 with an `unverified` tag - so speculative assistant statements do not pollute your memory as facts.
165
165
 
166
166
  **Store** - Entries get embedded and compared against what's already in the database. Near-duplicates reinforce existing knowledge. New information gets inserted. Online dedup catches copies in real-time.
167
167
 
@@ -201,7 +201,7 @@ MCP settings.
201
201
  | `agenr ingest <paths...>` | Bulk-ingest files and directories |
202
202
  | `agenr extract <files...>` | Extract knowledge entries from text files |
203
203
  | `agenr store [files...]` | Store entries with semantic dedup |
204
- | `agenr recall [query]` | Semantic + memory-aware recall |
204
+ | `agenr recall [query]` | Semantic + memory-aware recall. Use `--since` / `--until` for date range queries (e.g. `--since 14d --until 7d` for entries from two weeks ago). |
205
205
  | `agenr retire [subject]` | Retire a stale entry (hidden, not deleted). Match by subject text or use --id <id> to target by entry ID. |
206
206
  | `agenr watch [file]` | Live-watch files/directories, auto-extract knowledge |
207
207
  | `agenr daemon install` | Install background watch daemon (macOS launchd) |
@@ -227,13 +227,12 @@ Deep dive: [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md)
227
227
 
228
228
  ## Status
229
229
 
230
- The core pipeline is stable and tested (797 tests). We use it daily managing
230
+ The core pipeline is stable and tested (841 tests). We use it daily managing
231
231
  thousands of knowledge entries across OpenClaw sessions.
232
232
 
233
233
  What works: extraction, storage, recall, MCP integration, online dedup, consolidation, smart filtering, live watching, daemon mode.
234
234
 
235
- What's next: Cursor live signals, Claude Code UserPromptSubmit adapter,
236
- transitive project dependencies.
235
+ What's next: GUI Management Console (browse, search, and curate your knowledge database visually), Cursor live signals, Claude Code UserPromptSubmit adapter, transitive project dependencies.
237
236
 
238
237
  ## Philosophy
239
238
 
@@ -171,6 +171,7 @@ declare function extractKnowledgeFromChunks(params: {
171
171
  chunks: TranscriptChunk[];
172
172
  client: LlmClient;
173
173
  verbose: boolean;
174
+ platform?: KnowledgePlatform | (string & {});
174
175
  noDedup?: boolean;
175
176
  interChunkDelayMs?: number;
176
177
  llmConcurrency?: number;
package/dist/cli-main.js CHANGED
@@ -3335,6 +3335,47 @@ function resolveEmbeddingApiKey(config, env = process.env) {
3335
3335
  );
3336
3336
  }
3337
3337
 
3338
+ // src/utils/time.ts
3339
+ var DURATION_PATTERN = /^(\d+)\s*([hdmy])$/i;
3340
+ var UNIT_TO_MILLISECONDS = {
3341
+ h: 1e3 * 60 * 60,
3342
+ d: 1e3 * 60 * 60 * 24,
3343
+ m: 1e3 * 60 * 60 * 24 * 30,
3344
+ y: 1e3 * 60 * 60 * 24 * 365
3345
+ };
3346
+ function parseSince(since, now = /* @__PURE__ */ new Date()) {
3347
+ if (!since) {
3348
+ return void 0;
3349
+ }
3350
+ const trimmed = since.trim();
3351
+ if (!trimmed) {
3352
+ return void 0;
3353
+ }
3354
+ const durationMatch = trimmed.match(DURATION_PATTERN);
3355
+ if (durationMatch) {
3356
+ const amount = Number(durationMatch[1]);
3357
+ const unit = durationMatch[2]?.toLowerCase();
3358
+ const multiplier = unit ? UNIT_TO_MILLISECONDS[unit] : void 0;
3359
+ if (!Number.isFinite(amount) || amount <= 0 || !multiplier) {
3360
+ throw new Error("Invalid since value");
3361
+ }
3362
+ return new Date(now.getTime() - amount * multiplier);
3363
+ }
3364
+ const parsed = new Date(trimmed);
3365
+ if (Number.isNaN(parsed.getTime())) {
3366
+ throw new Error("Invalid since value");
3367
+ }
3368
+ return parsed;
3369
+ }
3370
+ function parseSinceToIso(since, now = /* @__PURE__ */ new Date(), invalidMessage = "Invalid date value") {
3371
+ try {
3372
+ const parsed = parseSince(since, now);
3373
+ return parsed ? parsed.toISOString() : void 0;
3374
+ } catch {
3375
+ throw new Error(invalidMessage);
3376
+ }
3377
+ }
3378
+
3338
3379
  // src/db/session-start.ts
3339
3380
  var DEFAULT_SESSION_CANDIDATE_LIMIT = 500;
3340
3381
  var DEFAULT_CORE_CANDIDATE_LIMIT = 5e3;
@@ -3641,40 +3682,6 @@ function resolveScopeSet(scope) {
3641
3682
  }
3642
3683
  return /* @__PURE__ */ new Set(["private", "personal", "public"]);
3643
3684
  }
3644
- function parseSince(since, now) {
3645
- if (!since) {
3646
- return void 0;
3647
- }
3648
- const trimmed = since.trim();
3649
- if (!trimmed) {
3650
- return void 0;
3651
- }
3652
- const durationMatch = trimmed.match(/^(\d+)\s*([hdy])$/i);
3653
- if (durationMatch) {
3654
- const amount = Number(durationMatch[1]);
3655
- const unit = durationMatch[2]?.toLowerCase();
3656
- if (!Number.isFinite(amount) || amount <= 0 || !unit) {
3657
- return void 0;
3658
- }
3659
- let multiplier = 0;
3660
- if (unit === "h") {
3661
- multiplier = 1e3 * 60 * 60;
3662
- } else if (unit === "d") {
3663
- multiplier = MILLISECONDS_PER_DAY;
3664
- } else if (unit === "y") {
3665
- multiplier = MILLISECONDS_PER_DAY * 365;
3666
- }
3667
- if (multiplier <= 0) {
3668
- return void 0;
3669
- }
3670
- return new Date(now.getTime() - amount * multiplier);
3671
- }
3672
- const parsed = new Date(trimmed);
3673
- if (Number.isNaN(parsed.getTime())) {
3674
- return void 0;
3675
- }
3676
- return parsed;
3677
- }
3678
3685
  function entryCreatedAfter(entry, cutoff) {
3679
3686
  if (!cutoff) {
3680
3687
  return true;
@@ -3685,10 +3692,23 @@ function entryCreatedAfter(entry, cutoff) {
3685
3692
  }
3686
3693
  return created.getTime() >= cutoff.getTime();
3687
3694
  }
3688
- function passesFilters(entry, query, cutoff, allowedScopes, normalizedTags, isSessionStart) {
3695
+ function entryCreatedBefore(entry, ceiling) {
3696
+ if (!ceiling) {
3697
+ return true;
3698
+ }
3699
+ const created = new Date(entry.created_at);
3700
+ if (Number.isNaN(created.getTime())) {
3701
+ return false;
3702
+ }
3703
+ return created.getTime() <= ceiling.getTime();
3704
+ }
3705
+ function passesFilters(entry, query, cutoff, ceiling, allowedScopes, normalizedTags, isSessionStart) {
3689
3706
  if (entry.superseded_by) {
3690
3707
  return false;
3691
3708
  }
3709
+ if (entry.retired) {
3710
+ return false;
3711
+ }
3692
3712
  if (isSessionStart && entry.suppressed_contexts?.includes("session-start")) {
3693
3713
  return false;
3694
3714
  }
@@ -3710,6 +3730,9 @@ function passesFilters(entry, query, cutoff, allowedScopes, normalizedTags, isSe
3710
3730
  if (!entryCreatedAfter(entry, cutoff)) {
3711
3731
  return false;
3712
3732
  }
3733
+ if (!entryCreatedBefore(entry, ceiling)) {
3734
+ return false;
3735
+ }
3713
3736
  const entryScope = entry.scope ?? "private";
3714
3737
  if (!allowedScopes.has(entryScope)) {
3715
3738
  return false;
@@ -3792,7 +3815,7 @@ function computeSpacingFactor(intervals, recallCount, createdAt, lastRecalledAt)
3792
3815
  }
3793
3816
  return Math.max(1, Math.log1p(maxGapDays + 1));
3794
3817
  }
3795
- function scoreEntryWithBreakdown(entry, vectorSim, ftsMatch, now) {
3818
+ function scoreEntryWithBreakdown(entry, vectorSim, ftsMatch, now, freshnessNow = now) {
3796
3819
  const daysOld = parseDaysBetween(now, entry.created_at);
3797
3820
  const daysSinceRecall = entry.last_recalled_at ? parseDaysBetween(now, entry.last_recalled_at) : daysOld;
3798
3821
  const rawVector = clamp01(vectorSim);
@@ -3807,7 +3830,7 @@ function scoreEntryWithBreakdown(entry, vectorSim, ftsMatch, now) {
3807
3830
  entry.last_recalled_at
3808
3831
  );
3809
3832
  const fts = ftsMatch ? 0.15 : 0;
3810
- const fresh = freshnessBoost(entry, now);
3833
+ const fresh = freshnessBoost(entry, freshnessNow);
3811
3834
  const spacedRecallBase = Math.min(recallBase * spacingFactor, 1);
3812
3835
  const memoryStrength = Math.min(Math.max(imp, spacedRecallBase) * fresh, 1);
3813
3836
  const todoPenalty = entry.type === "todo" ? todoStaleness(entry, now) : 1;
@@ -3893,6 +3916,7 @@ async function fetchVectorCandidates(db, queryEmbedding, limit, platform, projec
3893
3916
  CROSS JOIN entries AS e ON e.rowid = v.id
3894
3917
  WHERE e.embedding IS NOT NULL
3895
3918
  AND e.superseded_by IS NULL
3919
+ AND e.retired = 0
3896
3920
  ${projectSql.clause}
3897
3921
  ${platform ? "AND e.platform = ?" : ""}
3898
3922
  `,
@@ -3957,6 +3981,7 @@ async function fetchSessionCandidates(db, limit, context, platform, project, exc
3957
3981
  suppressed_contexts
3958
3982
  FROM entries
3959
3983
  WHERE superseded_by IS NULL
3984
+ AND retired = 0
3960
3985
  ${context === "session-start" ? `AND (suppressed_contexts IS NULL OR suppressed_contexts NOT LIKE '%"session-start"%')` : ""}
3961
3986
  ${projectSql.clause}
3962
3987
  ${platform ? "AND platform = ?" : ""}
@@ -3999,6 +4024,7 @@ async function runFts(db, text2, platform, project, excludeProject, projectStric
3999
4024
  JOIN entries AS e ON e.rowid = entries_fts.rowid
4000
4025
  WHERE entries_fts MATCH ?
4001
4026
  AND e.superseded_by IS NULL
4027
+ AND e.retired = 0
4002
4028
  ${projectSql.clause}
4003
4029
  ${platform ? "AND e.platform = ?" : ""}
4004
4030
  LIMIT 250
@@ -4027,8 +4053,8 @@ async function updateRecallMetadata(db, ids, now) {
4027
4053
  args: [now.toISOString(), epochSecs, ...ids]
4028
4054
  });
4029
4055
  }
4030
- function scoreSessionEntry(entry, now) {
4031
- return scoreEntryWithBreakdown(entry, 1, false, now);
4056
+ function scoreSessionEntry(entry, effectiveNow, freshnessNow) {
4057
+ return scoreEntryWithBreakdown(entry, 1, false, effectiveNow, freshnessNow);
4032
4058
  }
4033
4059
  async function recall(db, query, apiKey, options = {}) {
4034
4060
  const now = options.now ?? /* @__PURE__ */ new Date();
@@ -4046,8 +4072,28 @@ async function recall(db, query, apiKey, options = {}) {
4046
4072
  throw new Error("--no-boost requires query text.");
4047
4073
  }
4048
4074
  const normalizedTags = normalizeTags(query.tags);
4049
- const cutoff = parseSince(query.since, now);
4075
+ let cutoff;
4076
+ try {
4077
+ cutoff = parseSince(query.since, now);
4078
+ } catch (error) {
4079
+ const reason = error instanceof Error ? error.message : String(error);
4080
+ const sinceValue = query.since ?? "";
4081
+ throw new Error(`Invalid since value "${sinceValue}": ${reason}`);
4082
+ }
4083
+ let ceiling;
4084
+ try {
4085
+ ceiling = parseSince(query.until, now);
4086
+ } catch (error) {
4087
+ const reason = error instanceof Error ? error.message : String(error);
4088
+ throw new Error(`Invalid until value "${query.until ?? ""}": ${reason}`);
4089
+ }
4090
+ if (cutoff !== void 0 && ceiling !== void 0 && cutoff > ceiling) {
4091
+ throw new Error(
4092
+ `Invalid date range: since (${cutoff.toISOString()}) must be earlier than until (${ceiling.toISOString()}). since sets the lower bound, until the upper bound.`
4093
+ );
4094
+ }
4050
4095
  const allowedScopes = resolveScopeSet(query.scope);
4096
+ const hasDateBounds = cutoff !== void 0 || ceiling !== void 0;
4051
4097
  let candidates;
4052
4098
  let effectiveText = text2;
4053
4099
  if (text2) {
@@ -4058,19 +4104,21 @@ async function recall(db, query, apiKey, options = {}) {
4058
4104
  if (!queryEmbedding) {
4059
4105
  throw new Error("Embedding provider returned no vector for recall query.");
4060
4106
  }
4107
+ const vectorLimit = hasDateBounds ? (options.vectorCandidateLimit ?? DEFAULT_VECTOR_CANDIDATE_LIMIT) * 3 : options.vectorCandidateLimit ?? DEFAULT_VECTOR_CANDIDATE_LIMIT;
4061
4108
  candidates = await fetchVectorCandidates(
4062
4109
  db,
4063
4110
  queryEmbedding,
4064
- options.vectorCandidateLimit ?? DEFAULT_VECTOR_CANDIDATE_LIMIT,
4111
+ vectorLimit,
4065
4112
  platform,
4066
4113
  project,
4067
4114
  excludeProject,
4068
4115
  projectStrict
4069
4116
  );
4070
4117
  } else {
4118
+ const sessionLimit = hasDateBounds ? (options.sessionCandidateLimit ?? DEFAULT_SESSION_CANDIDATE_LIMIT) * 3 : options.sessionCandidateLimit ?? DEFAULT_SESSION_CANDIDATE_LIMIT;
4071
4119
  candidates = await fetchSessionCandidates(
4072
4120
  db,
4073
- options.sessionCandidateLimit ?? DEFAULT_SESSION_CANDIDATE_LIMIT,
4121
+ sessionLimit,
4074
4122
  context,
4075
4123
  platform,
4076
4124
  project,
@@ -4079,16 +4127,17 @@ async function recall(db, query, apiKey, options = {}) {
4079
4127
  );
4080
4128
  }
4081
4129
  const filtered = candidates.filter(
4082
- (candidate) => passesFilters(candidate.entry, query, cutoff, allowedScopes, normalizedTags, isSessionStart)
4130
+ (candidate) => passesFilters(candidate.entry, query, cutoff, ceiling, allowedScopes, normalizedTags, isSessionStart)
4083
4131
  );
4084
4132
  if (filtered.length === 0) {
4085
4133
  return [];
4086
4134
  }
4087
4135
  const ftsMatches = text2 && !query.noBoost ? await runFts(db, effectiveText, platform, project, excludeProject, projectStrict) : /* @__PURE__ */ new Set();
4136
+ const effectiveNow = ceiling ?? now;
4088
4137
  const scored = filtered.map((candidate) => {
4089
4138
  const ftsMatch = ftsMatches.has(candidate.entry.id);
4090
4139
  if (!text2) {
4091
- const sessionScore = scoreSessionEntry(candidate.entry, now);
4140
+ const sessionScore = scoreSessionEntry(candidate.entry, effectiveNow, now);
4092
4141
  return {
4093
4142
  entry: candidate.entry,
4094
4143
  score: sessionScore.score,
@@ -4112,7 +4161,7 @@ async function recall(db, query, apiKey, options = {}) {
4112
4161
  }
4113
4162
  };
4114
4163
  }
4115
- const detailed = scoreEntryWithBreakdown(candidate.entry, candidate.vectorSim, ftsMatch, now);
4164
+ const detailed = scoreEntryWithBreakdown(candidate.entry, candidate.vectorSim, ftsMatch, effectiveNow, now);
4116
4165
  return {
4117
4166
  entry: candidate.entry,
4118
4167
  score: detailed.score,
@@ -5849,6 +5898,7 @@ async function countActiveEntries(db, platform, project, excludeProject) {
5849
5898
  SELECT COUNT(*) AS count
5850
5899
  FROM entries
5851
5900
  WHERE superseded_by IS NULL
5901
+ AND retired = 0
5852
5902
  ${platform ? "AND platform = ?" : ""}
5853
5903
  ${projectSql.clause}
5854
5904
  `,
@@ -5890,6 +5940,7 @@ async function expireDecayedEntries(db, now, options) {
5890
5940
  SELECT id, content, expiry, created_at
5891
5941
  FROM entries
5892
5942
  WHERE superseded_by IS NULL
5943
+ AND retired = 0
5893
5944
  AND expiry = 'temporary'
5894
5945
  ${options.platform ? "AND platform = ?" : ""}
5895
5946
  ${projectSql.clause}
@@ -5952,6 +6003,7 @@ async function mergeNearExactDuplicates(db, options) {
5952
6003
  SELECT COUNT(*) AS count
5953
6004
  FROM entries
5954
6005
  WHERE superseded_by IS NULL
6006
+ AND retired = 0
5955
6007
  AND embedding IS NOT NULL
5956
6008
  ${options.platform ? "AND platform = ?" : ""}
5957
6009
  ${projectSql.clause}
@@ -5976,6 +6028,7 @@ async function mergeNearExactDuplicates(db, options) {
5976
6028
  SELECT id, type, subject, content, project, embedding, confirmations, recall_count, created_at
5977
6029
  FROM entries
5978
6030
  WHERE superseded_by IS NULL
6031
+ AND retired = 0
5979
6032
  AND embedding IS NOT NULL
5980
6033
  ${options.platform ? "AND platform = ?" : ""}
5981
6034
  ${projectSql.clause}
@@ -6362,6 +6415,7 @@ async function buildClusters(db, options = {}) {
6362
6415
  recall_count, created_at, merged_from, consolidated_at
6363
6416
  FROM entries
6364
6417
  WHERE superseded_by IS NULL
6418
+ AND retired = 0
6365
6419
  AND embedding IS NOT NULL
6366
6420
  ${platform ? "AND platform = ?" : ""}
6367
6421
  ${projectCondition}
@@ -9219,6 +9273,29 @@ PREFERENCE:
9219
9273
  "source_context": "User mentioned scheduling preference during calendar discussion -- scored 6 not 8 because no parallel session needs to act on this immediately; it is a low-urgency convenience preference"
9220
9274
  }
9221
9275
 
9276
+ // NOTE: These examples are drawn from OpenClaw transcripts (agent role labels, tool-verified claims). They provide soft cross-platform guidance for hedged-claim handling; mechanical enforcement (importance cap + unverified tag) is only applied for the openclaw platform via applyConfidenceCap().
9277
+ Example: hedged agent claim (correct handling):
9278
+ {
9279
+ "type": "fact",
9280
+ "subject": "agenr openclaw ingestion support",
9281
+ "content": "Agent expressed uncertainty about whether agenr supports OpenClaw session ingestion. This has not been tool-verified.",
9282
+ "importance": 5,
9283
+ "expiry": "temporary",
9284
+ "tags": ["agenr", "openclaw", "unverified"],
9285
+ "canonical_key": "agenr-openclaw-ingestion-support-uncertain"
9286
+ }
9287
+
9288
+ Example: tool-verified agent claim (correct handling):
9289
+ {
9290
+ "type": "fact",
9291
+ "subject": "agenr openclaw ingestion",
9292
+ "content": "agenr actively ingests OpenClaw sessions. Confirmed via exec output showing watch active on 3 files.",
9293
+ "importance": 7,
9294
+ "expiry": "temporary",
9295
+ "tags": ["agenr", "openclaw"],
9296
+ "canonical_key": "agenr-openclaw-ingestion-active"
9297
+ }
9298
+
9222
9299
  ### BORDERLINE \u2014 skip these
9223
9300
 
9224
9301
  SKIP: "The assistant read the config file and found the port was 3000."
@@ -9264,6 +9341,84 @@ WHY: Routine execution. No durable knowledge, decisions, or lessons.
9264
9341
  - source_context: one sentence, max 20 words.
9265
9342
  - tags: 1-4 lowercase descriptive tags.
9266
9343
  When related memories are injected before a chunk, they are reference material only. They do not lower the emission threshold.`;
9344
+ var OPENCLAW_CONFIDENCE_ADDENDUM = `
9345
+ ## Confidence Language (role-labeled transcripts)
9346
+
9347
+ Transcript lines are prefixed with [user] or [assistant] role labels.
9348
+ Apply confidence-language analysis to [assistant] statements only.
9349
+ Never apply this logic to [user] messages.
9350
+
9351
+ ### Verified agent statements - normal importance rules apply
9352
+
9353
+ An [assistant] statement is verified when it meets ANY of these conditions:
9354
+ - It immediately follows a line matching the pattern [called X: ...] such as
9355
+ [called exec: ...], [called web_search: ...], [called web_fetch: ...],
9356
+ [called Read: ...], [called write: ...], or any [called ...: ...] marker
9357
+ - It uses explicit verification language such as: "I confirmed", "I verified",
9358
+ "I checked and", "the output shows", "the test shows", "confirmed via",
9359
+ "the file shows", "I ran", "the result shows"
9360
+ - It is a direct summary of tool output returned in the same chunk
9361
+
9362
+ ### Hedged agent statements - cap importance at 5 and add "unverified" tag
9363
+
9364
+ An [assistant] statement is hedged when it uses speculative language WITHOUT
9365
+ any of the verification signals above. Hedging indicators:
9366
+ - Uncertainty: "I think", "I believe", "I assume", "I'm not sure", "probably",
9367
+ "likely", "I don't know if", "I imagine", "I suspect"
9368
+ - Conditional: "it might be", "it could be", "possibly", "maybe"
9369
+ - Unverified recall: "I recall that", "if I remember correctly",
9370
+ "I seem to recall", "I think we"
9371
+
9372
+ When an [assistant] factual claim is hedged and unverified:
9373
+ - Set importance to min(your assigned importance, 5)
9374
+ - Add the tag "unverified"
9375
+ - Apply all other normal extraction rules unchanged
9376
+
9377
+ EXCEPTION: Do NOT cap agent recommendations or opinions. "I think we should
9378
+ use pnpm" is a recommendation, not a factual claim. Only cap FACTUAL CLAIMS
9379
+ made with hedging language. Ask: is the agent asserting a fact about the
9380
+ world, or expressing a preference or suggestion?
9381
+
9382
+ Do NOT apply any cap to [assistant] lines that ARE the tool call markers
9383
+ themselves (lines like "[called exec: ls]"). These are structural metadata,
9384
+ not agent assertions.
9385
+
9386
+ ### Examples
9387
+
9388
+ HEDGED FACTUAL CLAIM - cap at 5, add "unverified":
9389
+ [m00010][assistant] I think agenr doesn't support OpenClaw ingestion yet.
9390
+ Result: importance capped at 5, tags include "unverified"
9391
+
9392
+ HEDGED FACTUAL CLAIM - cap at 5, add "unverified":
9393
+ [m00015][assistant] Probably the port is 3000 - I'm not 100% sure.
9394
+ Result: importance capped at 5, tags include "unverified"
9395
+
9396
+ VERIFIED after tool call - normal importance:
9397
+ [m00020][assistant] [called exec: agenr watch --list]
9398
+ [m00021][assistant] I confirmed via the output that OpenClaw ingestion is
9399
+ active on 3 files.
9400
+ Result: normal importance, no "unverified" tag
9401
+
9402
+ VERIFIED by explicit language - normal importance:
9403
+ [m00030][assistant] I verified in the source that the port is 4242.
9404
+ Result: normal importance
9405
+
9406
+ USER STATEMENT - never capped regardless of hedging language:
9407
+ [m00040][user] I'm not 100% sure, but I think our API key was rotated.
9408
+ Result: normal importance, no cap applied
9409
+
9410
+ RECOMMENDATION - not a factual claim, never capped:
9411
+ [m00050][assistant] I think we should switch to pnpm for consistency.
9412
+ Result: this is a suggestion, not a fact - do not apply the cap
9413
+ `;
9414
+ function buildExtractionSystemPrompt(platform) {
9415
+ if (platform === "openclaw") {
9416
+ return `${SYSTEM_PROMPT}
9417
+
9418
+ ${OPENCLAW_CONFIDENCE_ADDENDUM}`;
9419
+ }
9420
+ return SYSTEM_PROMPT;
9421
+ }
9267
9422
  var MAX_ATTEMPTS = 5;
9268
9423
  var DEFAULT_INTER_CHUNK_DELAY_MS = 150;
9269
9424
  var DEDUP_BATCH_SIZE = 50;
@@ -9525,6 +9680,12 @@ function validateEntry(entry) {
9525
9680
  }
9526
9681
  return null;
9527
9682
  }
9683
+ function applyConfidenceCap(entry, platform) {
9684
+ if (platform === "openclaw" && entry.tags.includes("unverified") && entry.importance > 5) {
9685
+ return { ...entry, importance: 5 };
9686
+ }
9687
+ return entry;
9688
+ }
9528
9689
  function selectStringField(record, ...keys) {
9529
9690
  for (const key of keys) {
9530
9691
  const value = record[key];
@@ -9990,7 +10151,7 @@ async function sleepMs2(ms) {
9990
10151
  async function extractChunkOnce(params) {
9991
10152
  const prompt = buildUserPrompt(params.chunk, params.related);
9992
10153
  const context = {
9993
- systemPrompt: SYSTEM_PROMPT,
10154
+ systemPrompt: params.systemPrompt ?? SYSTEM_PROMPT,
9994
10155
  messages: [
9995
10156
  {
9996
10157
  role: "user",
@@ -10029,6 +10190,7 @@ async function extractChunkOnce(params) {
10029
10190
  return { entries, warnings };
10030
10191
  }
10031
10192
  async function extractKnowledgeFromChunks(params) {
10193
+ const systemPrompt = buildExtractionSystemPrompt(params.platform);
10032
10194
  const warnings = [];
10033
10195
  const entries = [];
10034
10196
  let successfulChunks = 0;
@@ -10087,6 +10249,7 @@ async function extractKnowledgeFromChunks(params) {
10087
10249
  chunk,
10088
10250
  model: params.client.resolvedModel.model,
10089
10251
  apiKey: params.client.credentials.apiKey,
10252
+ systemPrompt,
10090
10253
  verbose: params.verbose,
10091
10254
  onVerbose: params.onVerbose,
10092
10255
  onStreamDelta: bufferStreamDeltas ? (delta, kind) => {
@@ -10126,15 +10289,32 @@ async function extractKnowledgeFromChunks(params) {
10126
10289
  );
10127
10290
  } else if (chunkResult) {
10128
10291
  dynamicDelay = Math.max(baseDelay, Math.floor(dynamicDelay * 0.9));
10292
+ const validatedChunkEntries = [];
10293
+ for (const entry of chunkResult.entries) {
10294
+ const capped = applyConfidenceCap(entry, params.platform);
10295
+ if (params.verbose && capped.importance !== entry.importance) {
10296
+ params.onVerbose?.(
10297
+ `[confidence-cap] lowered importance from ${entry.importance} to 5 (unverified tag)`
10298
+ );
10299
+ }
10300
+ const validationIssue = validateEntry(capped);
10301
+ if (validationIssue) {
10302
+ if (params.verbose) {
10303
+ params.onVerbose?.(`[entry-drop] ${validationIssue}`);
10304
+ }
10305
+ continue;
10306
+ }
10307
+ validatedChunkEntries.push(capped);
10308
+ }
10129
10309
  if (params.onChunkComplete) {
10130
10310
  await params.onChunkComplete({
10131
10311
  chunkIndex: chunk.chunk_index,
10132
10312
  totalChunks: params.chunks.length,
10133
- entries: chunkResult.entries,
10313
+ entries: validatedChunkEntries,
10134
10314
  warnings: chunkResult.warnings
10135
10315
  });
10136
10316
  } else {
10137
- entries.push(...chunkResult.entries);
10317
+ entries.push(...validatedChunkEntries);
10138
10318
  }
10139
10319
  }
10140
10320
  if (params.onStreamDelta) {
@@ -12887,6 +13067,7 @@ async function runIngestCommand(inputPaths, options, deps) {
12887
13067
  chunks: parsed.chunks,
12888
13068
  client,
12889
13069
  verbose: false,
13070
+ platform: platform ?? parsed.metadata?.platform ?? void 0,
12890
13071
  llmConcurrency,
12891
13072
  db: options.noPreFetch ? void 0 : db,
12892
13073
  embeddingApiKey: options.noPreFetch ? void 0 : embeddingApiKey ?? void 0,
@@ -13803,6 +13984,10 @@ var TOOL_DEFINITIONS = [
13803
13984
  type: "string",
13804
13985
  description: "Only entries newer than this (ISO date or relative, e.g. 7d, 1m)."
13805
13986
  },
13987
+ until: {
13988
+ type: "string",
13989
+ description: "Only entries created before this point in time (ISO date or relative, e.g. 7d = entries older than 7 days). Use with since for a date range: since sets the lower bound, until the upper bound."
13990
+ },
13806
13991
  threshold: {
13807
13992
  type: "number",
13808
13993
  description: "Minimum relevance score from 0.0 to 1.0.",
@@ -14047,42 +14232,6 @@ function parseCsvProjects(input) {
14047
14232
  }
14048
14233
  return parsed;
14049
14234
  }
14050
- function parseSinceToIso(since, now) {
14051
- if (!since) {
14052
- return void 0;
14053
- }
14054
- const trimmed = since.trim();
14055
- if (!trimmed) {
14056
- return void 0;
14057
- }
14058
- const durationMatch = trimmed.match(/^(\d+)\s*([hdmy])$/i);
14059
- if (durationMatch) {
14060
- const amount = Number(durationMatch[1]);
14061
- const unit = durationMatch[2]?.toLowerCase();
14062
- if (!Number.isFinite(amount) || amount <= 0 || !unit) {
14063
- throw new RpcError(JSON_RPC_INVALID_PARAMS, "Invalid since value");
14064
- }
14065
- let multiplier = 0;
14066
- if (unit === "h") {
14067
- multiplier = 1e3 * 60 * 60;
14068
- } else if (unit === "d") {
14069
- multiplier = 1e3 * 60 * 60 * 24;
14070
- } else if (unit === "m") {
14071
- multiplier = 1e3 * 60 * 60 * 24 * 30;
14072
- } else if (unit === "y") {
14073
- multiplier = 1e3 * 60 * 60 * 24 * 365;
14074
- }
14075
- if (multiplier <= 0) {
14076
- throw new RpcError(JSON_RPC_INVALID_PARAMS, "Invalid since value");
14077
- }
14078
- return new Date(now.getTime() - amount * multiplier).toISOString();
14079
- }
14080
- const parsedDate = new Date(trimmed);
14081
- if (Number.isNaN(parsedDate.getTime())) {
14082
- throw new RpcError(JSON_RPC_INVALID_PARAMS, "Invalid since value");
14083
- }
14084
- return parsedDate.toISOString();
14085
- }
14086
14235
  function normalizeTags3(value) {
14087
14236
  if (!Array.isArray(value)) {
14088
14237
  return [];
@@ -14359,7 +14508,22 @@ function createMcpServer(options = {}, deps = {}) {
14359
14508
  const threshold = parseThreshold(args.threshold);
14360
14509
  const now = resolvedDeps.nowFn();
14361
14510
  const types = typeof args.types === "string" && args.types.trim().length > 0 ? parseCsvTypes(args.types) : void 0;
14362
- const since = typeof args.since === "string" && args.since.trim().length > 0 ? parseSinceToIso(args.since, now) : void 0;
14511
+ let since;
14512
+ if (typeof args.since === "string" && args.since.trim().length > 0) {
14513
+ try {
14514
+ since = parseSinceToIso(args.since, now);
14515
+ } catch {
14516
+ throw new RpcError(JSON_RPC_INVALID_PARAMS, "Invalid since value");
14517
+ }
14518
+ }
14519
+ let until;
14520
+ if (typeof args.until === "string" && args.until.trim().length > 0) {
14521
+ try {
14522
+ until = parseSinceToIso(args.until, now);
14523
+ } catch {
14524
+ throw new RpcError(JSON_RPC_INVALID_PARAMS, "Invalid until value");
14525
+ }
14526
+ }
14363
14527
  const platformRaw = typeof args.platform === "string" ? args.platform.trim() : "";
14364
14528
  const platform = platformRaw ? normalizeKnowledgePlatform(platformRaw) : null;
14365
14529
  if (platformRaw && !platform) {
@@ -14397,6 +14561,7 @@ function createMcpServer(options = {}, deps = {}) {
14397
14561
  limit,
14398
14562
  types,
14399
14563
  since,
14564
+ until,
14400
14565
  platform: platform ?? void 0,
14401
14566
  project,
14402
14567
  projectStrict: projectStrict ? true : void 0,
@@ -14417,6 +14582,7 @@ function createMcpServer(options = {}, deps = {}) {
14417
14582
  limit,
14418
14583
  types,
14419
14584
  since,
14585
+ until,
14420
14586
  platform: platform ?? void 0,
14421
14587
  project,
14422
14588
  projectStrict: projectStrict ? true : void 0
@@ -14807,40 +14973,6 @@ function parseCsv(input) {
14807
14973
  )
14808
14974
  );
14809
14975
  }
14810
- function parseSinceToIso2(since, now = /* @__PURE__ */ new Date()) {
14811
- if (!since) {
14812
- return void 0;
14813
- }
14814
- const trimmed = since.trim();
14815
- if (!trimmed) {
14816
- return void 0;
14817
- }
14818
- const duration = trimmed.match(/^(\d+)\s*([hdy])$/i);
14819
- if (duration) {
14820
- const amount = Number(duration[1]);
14821
- const unit = duration[2]?.toLowerCase();
14822
- if (!Number.isFinite(amount) || amount <= 0 || !unit) {
14823
- throw new Error("Invalid --since duration. Use 1h, 7d, 30d, or 1y.");
14824
- }
14825
- let millis = 0;
14826
- if (unit === "h") {
14827
- millis = amount * 60 * 60 * 1e3;
14828
- } else if (unit === "d") {
14829
- millis = amount * 24 * 60 * 60 * 1e3;
14830
- } else if (unit === "y") {
14831
- millis = amount * 365 * 24 * 60 * 60 * 1e3;
14832
- }
14833
- if (millis <= 0) {
14834
- throw new Error("Invalid --since duration. Use 1h, 7d, 30d, or 1y.");
14835
- }
14836
- return new Date(now.getTime() - millis).toISOString();
14837
- }
14838
- const parsed = new Date(trimmed);
14839
- if (Number.isNaN(parsed.getTime())) {
14840
- throw new Error("Invalid --since value. Use 1h, 7d, 30d, 1y, or an ISO date.");
14841
- }
14842
- return parsed.toISOString();
14843
- }
14844
14976
  function stripEmbedding(result) {
14845
14977
  const { embedding, ...entryWithoutEmbedding } = result.entry;
14846
14978
  return {
@@ -15009,7 +15141,8 @@ async function runRecallCommand(queryInput, options, deps) {
15009
15141
  const project = parsedProject.length > 0 ? parsedProject : void 0;
15010
15142
  const excludeProject = parsedExcludeProject.length > 0 ? parsedExcludeProject : void 0;
15011
15143
  const projectStrict = options.strict === true && Boolean(project && project.length > 0);
15012
- const sinceIso = parseSinceToIso2(options.since, now);
15144
+ const sinceIso = parseSinceToIso(options.since, now, "Invalid --since value. Use 1h, 7d, 1m, 1y, or an ISO date.");
15145
+ const untilIso = parseSinceToIso(options.until, now, "Invalid --until value. Use 1h, 7d, 1m, 1y, or an ISO date.");
15013
15146
  const queryForRecall = {
15014
15147
  text: queryText ? shapeRecallText(queryText, context) : void 0,
15015
15148
  limit,
@@ -15017,6 +15150,7 @@ async function runRecallCommand(queryInput, options, deps) {
15017
15150
  tags: tags.length > 0 ? tags : void 0,
15018
15151
  minImportance,
15019
15152
  since: sinceIso,
15153
+ until: untilIso,
15020
15154
  expiry,
15021
15155
  scope: scope ?? "private",
15022
15156
  platform,
@@ -16091,6 +16225,7 @@ async function runWatcher(options, deps) {
16091
16225
  chunks: parsed.chunks,
16092
16226
  client,
16093
16227
  verbose: options.verbose,
16228
+ platform: currentPlatform && currentPlatform !== "mtime" ? normalizeKnowledgePlatform(currentPlatform) ?? void 0 : void 0,
16094
16229
  db: options.noPreFetch ? void 0 : db ?? void 0,
16095
16230
  embeddingApiKey: options.noPreFetch ? void 0 : embeddingApiKey ?? void 0,
16096
16231
  noPreFetch: options.noPreFetch === true,
@@ -17456,13 +17591,14 @@ function createProgram() {
17456
17591
  process.exitCode = result.exitCode;
17457
17592
  }
17458
17593
  );
17459
- program.command("recall").description("Recall knowledge from the local database").argument("[query]", "Natural language query").option("--limit <n>", "Maximum number of results", parseIntOption, 10).option("--type <types>", "Filter by comma-separated entry types").option("--tags <tags>", "Filter by comma-separated tags").option("--min-importance <n>", "Minimum importance: 1-10").option("--since <duration>", "Filter by recency (1h, 7d, 30d, 1y) or ISO timestamp").option("--expiry <level>", "Filter by expiry: core|permanent|temporary").option("--platform <name>", "Filter by platform: openclaw, claude-code, codex").option("--project <name>", "Filter by project (repeatable)", (val, prev) => [...prev, val], []).option("--exclude-project <name>", "Exclude entries from project (repeatable)", (val, prev) => [...prev, val], []).option("--strict", "Exclude NULL-project entries from results", false).option("--json", "Output JSON", false).option("--db <path>", "Database path override").option("--budget <tokens>", "Approximate token budget", parseIntOption).option("--context <mode>", "Context mode: default|session-start|topic:<query>", "default").option("--scope <level>", "Visibility scope: private|personal|public", "private").option("--no-boost", "Disable scoring boosts and use raw vector similarity", false).option("--no-update", "Do not increment recall metadata", false).action(async (query, opts) => {
17594
+ program.command("recall").description("Recall knowledge from the local database").argument("[query]", "Natural language query").option("--limit <n>", "Maximum number of results", parseIntOption, 10).option("--type <types>", "Filter by comma-separated entry types").option("--tags <tags>", "Filter by comma-separated tags").option("--min-importance <n>", "Minimum importance: 1-10").option("--since <duration>", "Filter by recency (1h, 7d, 30d, 1y) or ISO timestamp").option("--until <date>", "Only entries at or before this time (ISO date or relative, e.g. 7d, 1m)").option("--expiry <level>", "Filter by expiry: core|permanent|temporary").option("--platform <name>", "Filter by platform: openclaw, claude-code, codex").option("--project <name>", "Filter by project (repeatable)", (val, prev) => [...prev, val], []).option("--exclude-project <name>", "Exclude entries from project (repeatable)", (val, prev) => [...prev, val], []).option("--strict", "Exclude NULL-project entries from results", false).option("--json", "Output JSON", false).option("--db <path>", "Database path override").option("--budget <tokens>", "Approximate token budget", parseIntOption).option("--context <mode>", "Context mode: default|session-start|topic:<query>", "default").option("--scope <level>", "Visibility scope: private|personal|public", "private").option("--no-boost", "Disable scoring boosts and use raw vector similarity", false).option("--no-update", "Do not increment recall metadata", false).action(async (query, opts) => {
17460
17595
  const result = await runRecallCommand(query, {
17461
17596
  limit: opts.limit,
17462
17597
  type: opts.type,
17463
17598
  tags: opts.tags,
17464
17599
  minImportance: opts.minImportance,
17465
17600
  since: opts.since,
17601
+ until: opts.until,
17466
17602
  expiry: opts.expiry,
17467
17603
  platform: opts.platform,
17468
17604
  project: opts.project,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenr",
3
- "version": "0.7.13",
3
+ "version": "0.7.14",
4
4
  "openclaw": {
5
5
  "extensions": [
6
6
  "dist/openclaw-plugin/index.js"