@joshuaswarren/openclaw-engram 8.3.12 → 8.3.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/dist/index.js CHANGED
@@ -120,6 +120,9 @@ function parseConfig(raw) {
120
120
  qmdEnabled: cfg.qmdEnabled !== false,
121
121
  qmdCollection: typeof cfg.qmdCollection === "string" ? cfg.qmdCollection : "openclaw-engram",
122
122
  qmdMaxResults: typeof cfg.qmdMaxResults === "number" ? cfg.qmdMaxResults : 8,
123
+ qmdColdTierEnabled: cfg.qmdColdTierEnabled === true,
124
+ qmdColdCollection: typeof cfg.qmdColdCollection === "string" && cfg.qmdColdCollection.length > 0 ? cfg.qmdColdCollection : "openclaw-engram-cold",
125
+ qmdColdMaxResults: typeof cfg.qmdColdMaxResults === "number" ? cfg.qmdColdMaxResults : 8,
123
126
  embeddingFallbackEnabled: cfg.embeddingFallbackEnabled !== false,
124
127
  embeddingFallbackProvider: cfg.embeddingFallbackProvider === "openai" ? "openai" : cfg.embeddingFallbackProvider === "local" ? "local" : "auto",
125
128
  qmdPath: typeof cfg.qmdPath === "string" && cfg.qmdPath.length > 0 ? cfg.qmdPath : void 0,
@@ -257,6 +260,7 @@ function parseConfig(raw) {
257
260
  qmdAutoEmbedEnabled: cfg.qmdAutoEmbedEnabled === true,
258
261
  qmdEmbedMinIntervalMs: typeof cfg.qmdEmbedMinIntervalMs === "number" ? cfg.qmdEmbedMinIntervalMs : 60 * 6e4,
259
262
  qmdUpdateTimeoutMs: typeof cfg.qmdUpdateTimeoutMs === "number" ? cfg.qmdUpdateTimeoutMs : 9e4,
263
+ qmdUpdateMinIntervalMs: typeof cfg.qmdUpdateMinIntervalMs === "number" ? cfg.qmdUpdateMinIntervalMs : 15 * 6e4,
260
264
  // Local LLM resilience
261
265
  localLlmRetry5xxCount: typeof cfg.localLlmRetry5xxCount === "number" ? cfg.localLlmRetry5xxCount : 1,
262
266
  localLlmRetryBackoffMs: typeof cfg.localLlmRetryBackoffMs === "number" ? cfg.localLlmRetryBackoffMs : 400,
@@ -277,6 +281,12 @@ function parseConfig(raw) {
277
281
  includeInRecallByDefault: p?.includeInRecallByDefault === true
278
282
  })).filter((p) => p.name.length > 0) : [],
279
283
  defaultRecallNamespaces: Array.isArray(cfg.defaultRecallNamespaces) ? ["self", "shared"].filter((x) => cfg.defaultRecallNamespaces.includes(x)) : ["self", "shared"],
284
+ cronRecallMode: cfg.cronRecallMode === "none" ? "none" : cfg.cronRecallMode === "allowlist" ? "allowlist" : "all",
285
+ cronRecallAllowlist: Array.isArray(cfg.cronRecallAllowlist) ? cfg.cronRecallAllowlist.filter((v) => typeof v === "string" && v.length > 0) : [],
286
+ cronRecallPolicyEnabled: cfg.cronRecallPolicyEnabled !== false,
287
+ cronRecallNormalizedQueryMaxChars: typeof cfg.cronRecallNormalizedQueryMaxChars === "number" ? cfg.cronRecallNormalizedQueryMaxChars : 480,
288
+ cronRecallInstructionHeavyTokenCap: typeof cfg.cronRecallInstructionHeavyTokenCap === "number" ? cfg.cronRecallInstructionHeavyTokenCap : 36,
289
+ cronConversationRecallMode: cfg.cronConversationRecallMode === "always" ? "always" : cfg.cronConversationRecallMode === "never" ? "never" : "auto",
280
290
  autoPromoteToSharedEnabled: cfg.autoPromoteToSharedEnabled === true,
281
291
  autoPromoteToSharedCategories: Array.isArray(cfg.autoPromoteToSharedCategories) ? cfg.autoPromoteToSharedCategories.filter((c) => c === "correction" || c === "decision" || c === "preference") : ["correction", "decision", "preference"],
282
292
  autoPromoteMinConfidenceTier: cfg.autoPromoteMinConfidenceTier === "explicit" ? "explicit" : cfg.autoPromoteMinConfidenceTier === "implied" ? "implied" : "explicit",
@@ -3490,6 +3500,7 @@ import os2 from "os";
3490
3500
  import path2 from "path";
3491
3501
  var QMD_TIMEOUT_MS = 3e4;
3492
3502
  var QMD_DAEMON_TIMEOUT_MS = 6e4;
3503
+ var QMD_PROBE_TIMEOUT_MS = 8e3;
3493
3504
  var QMD_UPDATE_BACKOFF_MS = 15 * 60 * 1e3;
3494
3505
  var QMD_EMBED_BACKOFF_MS = 60 * 60 * 1e3;
3495
3506
  var QMD_FALLBACK_PATHS = [
@@ -3702,6 +3713,7 @@ var QmdClient = class {
3702
3713
  this.maxResults = maxResults;
3703
3714
  this.slowLog = opts?.slowLog;
3704
3715
  this.updateTimeoutMs = opts?.updateTimeoutMs ?? 12e4;
3716
+ this.updateMinIntervalMs = Math.max(0, opts?.updateMinIntervalMs ?? 15 * 6e4);
3705
3717
  this.configuredQmdPath = opts?.qmdPath?.trim() ? opts.qmdPath.trim() : void 0;
3706
3718
  this.daemonRecheckIntervalMs = opts?.daemonRecheckIntervalMs ?? 6e4;
3707
3719
  if (opts?.daemonUrl) {
@@ -3711,7 +3723,10 @@ var QmdClient = class {
3711
3723
  available = null;
3712
3724
  lastUpdateFailAtMs = null;
3713
3725
  lastEmbedFailAtMs = null;
3726
+ lastUpdateRunAtMs = null;
3727
+ warnedGlobalUpdateBehavior = false;
3714
3728
  updateTimeoutMs;
3729
+ updateMinIntervalMs;
3715
3730
  slowLog;
3716
3731
  configuredQmdPath;
3717
3732
  qmdPathSource = "auto-path";
@@ -3770,7 +3785,7 @@ ${stderr}`.trim();
3770
3785
  };
3771
3786
  if (this.configuredQmdPath) {
3772
3787
  try {
3773
- const result = await runQmd(["--version"], 3e3, this.configuredQmdPath);
3788
+ const result = await runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, this.configuredQmdPath);
3774
3789
  this.available = true;
3775
3790
  this.qmdPath = this.configuredQmdPath;
3776
3791
  this.qmdPathSource = "configured";
@@ -3785,7 +3800,7 @@ ${stderr}`.trim();
3785
3800
  }
3786
3801
  }
3787
3802
  try {
3788
- const result = await runQmd(["--version"], 3e3, "qmd");
3803
+ const result = await runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, "qmd");
3789
3804
  this.available = true;
3790
3805
  this.qmdPath = "qmd";
3791
3806
  this.qmdPathSource = "auto-path";
@@ -3796,7 +3811,7 @@ ${stderr}`.trim();
3796
3811
  markProbeFailure(err);
3797
3812
  for (const fallbackPath of QMD_FALLBACK_PATHS) {
3798
3813
  try {
3799
- const result = await runQmd(["--version"], 3e3, fallbackPath);
3814
+ const result = await runQmd(["--version"], QMD_PROBE_TIMEOUT_MS, fallbackPath);
3800
3815
  this.available = true;
3801
3816
  this.qmdPath = fallbackPath;
3802
3817
  this.qmdPathSource = "auto-fallback";
@@ -3842,7 +3857,10 @@ ${stderr}`.trim();
3842
3857
  await this.maybeProbeDaemon();
3843
3858
  if (this.daemonAvailable) {
3844
3859
  const results = await this.searchViaDaemon(trimmed, col, n);
3845
- if (results !== null) return results;
3860
+ if (results !== null) {
3861
+ if (results.length > 0) return results;
3862
+ log.debug("QMD daemon search returned 0 results; falling back to subprocess query");
3863
+ }
3846
3864
  }
3847
3865
  return this.searchViaSubprocess(trimmed, col, n);
3848
3866
  }
@@ -3854,7 +3872,10 @@ ${stderr}`.trim();
3854
3872
  await this.maybeProbeDaemon();
3855
3873
  if (this.daemonAvailable) {
3856
3874
  const results = await this.searchViaDaemon(trimmed, void 0, n);
3857
- if (results !== null) return results;
3875
+ if (results !== null) {
3876
+ if (results.length > 0) return results;
3877
+ log.debug("QMD daemon global search returned 0 results; falling back to subprocess query");
3878
+ }
3858
3879
  }
3859
3880
  return this.searchGlobalViaSubprocess(trimmed, n);
3860
3881
  }
@@ -4076,17 +4097,28 @@ ${stderr}`.trim();
4076
4097
  }
4077
4098
  async update() {
4078
4099
  if (this.available === false) return;
4100
+ if (this.lastUpdateRunAtMs && Date.now() - this.lastUpdateRunAtMs < this.updateMinIntervalMs) {
4101
+ log.debug("QMD update: suppressed due to min-interval gate");
4102
+ return;
4103
+ }
4079
4104
  if (this.lastUpdateFailAtMs && Date.now() - this.lastUpdateFailAtMs < QMD_UPDATE_BACKOFF_MS) {
4080
4105
  log.debug("QMD update: suppressed due to recent failures (backoff)");
4081
4106
  return;
4082
4107
  }
4083
4108
  try {
4109
+ if (!this.warnedGlobalUpdateBehavior) {
4110
+ this.warnedGlobalUpdateBehavior = true;
4111
+ log.warn(
4112
+ "QMD update runs globally across collections in current CLI versions; Engram now rate-limits update calls to reduce gateway load."
4113
+ );
4114
+ }
4084
4115
  const startedAtMs = Date.now();
4085
4116
  await runQmd(["update", "-c", this.collection], this.updateTimeoutMs, this.qmdPath);
4086
4117
  const durationMs = Date.now() - startedAtMs;
4087
4118
  if (this.slowLog?.enabled && durationMs >= this.slowLog.thresholdMs) {
4088
4119
  log.warn(`SLOW QMD update: durationMs=${durationMs}`);
4089
4120
  }
4121
+ this.lastUpdateRunAtMs = Date.now();
4090
4122
  log.debug("QMD update completed");
4091
4123
  } catch (err) {
4092
4124
  this.lastUpdateFailAtMs = Date.now();
@@ -5030,6 +5062,41 @@ ${sanitized.text}
5030
5062
  await readDir(this.correctionsDir);
5031
5063
  return memories;
5032
5064
  }
5065
+ /**
5066
+ * Read archived memory markdown files under archive/.
5067
+ * Used by long-term recall fallback when hot recall has no hits.
5068
+ */
5069
+ async readArchivedMemories() {
5070
+ const memories = [];
5071
+ const root = this.archiveDir;
5072
+ const readDir = async (dir) => {
5073
+ try {
5074
+ const entries = await readdir(dir, { withFileTypes: true });
5075
+ for (const entry of entries) {
5076
+ const fullPath = path4.join(dir, entry.name);
5077
+ if (entry.isDirectory()) {
5078
+ await readDir(fullPath);
5079
+ } else if (entry.name.endsWith(".md")) {
5080
+ try {
5081
+ const raw = await readFile2(fullPath, "utf-8");
5082
+ const parsed = parseFrontmatter(raw);
5083
+ if (parsed) {
5084
+ memories.push({
5085
+ path: fullPath,
5086
+ frontmatter: parsed.frontmatter,
5087
+ content: parsed.content
5088
+ });
5089
+ }
5090
+ } catch {
5091
+ }
5092
+ }
5093
+ }
5094
+ } catch {
5095
+ }
5096
+ };
5097
+ await readDir(root);
5098
+ return memories;
5099
+ }
5033
5100
  /** Read a single memory file by its absolute path. Returns null if unreadable. */
5034
5101
  async readMemoryByPath(filePath) {
5035
5102
  try {
@@ -8275,6 +8342,160 @@ function planRecallMode(prompt) {
8275
8342
  return "full";
8276
8343
  }
8277
8344
 
8345
+ // src/recall-query-policy.ts
8346
+ var DEFAULT_STOPWORDS = /* @__PURE__ */ new Set([
8347
+ "the",
8348
+ "and",
8349
+ "for",
8350
+ "with",
8351
+ "from",
8352
+ "that",
8353
+ "this",
8354
+ "your",
8355
+ "you",
8356
+ "are",
8357
+ "was",
8358
+ "were",
8359
+ "have",
8360
+ "has",
8361
+ "had",
8362
+ "not",
8363
+ "but",
8364
+ "its",
8365
+ "into",
8366
+ "only",
8367
+ "use",
8368
+ "run",
8369
+ "then",
8370
+ "when",
8371
+ "what",
8372
+ "where",
8373
+ "which",
8374
+ "will",
8375
+ "would",
8376
+ "should",
8377
+ "could",
8378
+ "goal",
8379
+ "output",
8380
+ "format",
8381
+ "rules",
8382
+ "section",
8383
+ "sections",
8384
+ "skip",
8385
+ "today",
8386
+ "yesterday",
8387
+ "return",
8388
+ "summary",
8389
+ "plain",
8390
+ "text",
8391
+ "before",
8392
+ "after",
8393
+ "time",
8394
+ "date",
8395
+ "daily",
8396
+ "cron",
8397
+ "agent",
8398
+ "mode",
8399
+ "data",
8400
+ "gathering",
8401
+ "context"
8402
+ ]);
8403
+ function collapseWhitespace(text) {
8404
+ return text.replace(/\s+/g, " ").trim();
8405
+ }
8406
+ function stripFilesystemLikePaths(text) {
8407
+ return text.replace(/(?:^|\s)(~\/[^\s)]+)(?=\s|$)/g, " ").replace(/(?:^|\s)(\/[A-Za-z0-9._\-\/]+)(?=\s|$)/g, " ").replace(/(?:^|\s)([A-Za-z]:\\[^\s)]+)(?=\s|$)/g, " ");
8408
+ }
8409
+ function isBulletOrNumberedLine(line) {
8410
+ if (line.startsWith("-") || line.startsWith("*")) {
8411
+ return true;
8412
+ }
8413
+ let i = 0;
8414
+ while (i < line.length) {
8415
+ const code = line.charCodeAt(i);
8416
+ if (code < 48 || code > 57) {
8417
+ break;
8418
+ }
8419
+ i += 1;
8420
+ }
8421
+ return i > 0 && i < line.length && line.charAt(i) === ".";
8422
+ }
8423
+ function scoreInstructionHeavyShape(prompt) {
8424
+ const lines = prompt.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
8425
+ const lineCount = lines.length;
8426
+ if (lineCount === 0) return 0;
8427
+ const headingLineCount = lines.filter(
8428
+ (line) => /^(goal|output format|tone rules|grounding rules|data gathering|date computation|crm context|follow-up|social|current time|return)\b/i.test(
8429
+ line
8430
+ ) || /^[A-Z][A-Z\s/-]{4,}:$/.test(line)
8431
+ ).length;
8432
+ const bulletLineCount = lines.filter((line) => isBulletOrNumberedLine(line)).length;
8433
+ const longLineCount = lines.filter((line) => line.length >= 180).length;
8434
+ const hasPathDensity = (prompt.match(/(?:~\/|\/Users\/|[A-Za-z]:\\)/g)?.length ?? 0) >= 2;
8435
+ const hasImperativeDensity = (prompt.match(/\b(run|extract|read|parse|determine|include|omit|skip)\b/gi)?.length ?? 0) >= 8;
8436
+ let score = 0;
8437
+ if (lineCount >= 24) score += 2;
8438
+ if (headingLineCount >= 4) score += 2;
8439
+ if (bulletLineCount >= 8) score += 1;
8440
+ if (longLineCount >= 3) score += 1;
8441
+ if (hasPathDensity) score += 1;
8442
+ if (hasImperativeDensity) score += 1;
8443
+ return score;
8444
+ }
8445
+ function classifyRecallPromptShape(prompt) {
8446
+ const score = scoreInstructionHeavyShape(prompt);
8447
+ return score >= 5 ? "instruction_heavy" : "standard";
8448
+ }
8449
+ function tokenizeForCompactQuery(text) {
8450
+ const raw = text.toLowerCase().replace(/[^a-z0-9\s:_-]+/g, " ").split(/\s+/).filter((token) => token.length >= 3);
8451
+ const deduped = [];
8452
+ const seen = /* @__PURE__ */ new Set();
8453
+ for (const token of raw) {
8454
+ if (DEFAULT_STOPWORDS.has(token)) continue;
8455
+ if (seen.has(token)) continue;
8456
+ seen.add(token);
8457
+ deduped.push(token);
8458
+ }
8459
+ return deduped;
8460
+ }
8461
+ function buildInstructionHeavyQuery(prompt, tokenCap, maxChars) {
8462
+ const cleaned = stripFilesystemLikePaths(prompt);
8463
+ const tokens = tokenizeForCompactQuery(cleaned).slice(0, Math.max(8, tokenCap));
8464
+ const joined = tokens.join(" ");
8465
+ const compact = collapseWhitespace(joined);
8466
+ if (compact.length <= maxChars) return compact;
8467
+ return compact.slice(0, maxChars).trim();
8468
+ }
8469
+ function buildStandardQuery(prompt, maxChars) {
8470
+ const trimmed = collapseWhitespace(prompt);
8471
+ if (trimmed.length <= maxChars) return trimmed;
8472
+ return trimmed.slice(0, maxChars).trim();
8473
+ }
8474
+ function buildRecallQueryPolicy(prompt, sessionKey, cfg) {
8475
+ const normalizedPrompt = collapseWhitespace(prompt);
8476
+ const isCron = (sessionKey ?? "").includes(":cron:");
8477
+ if (!cfg.cronRecallPolicyEnabled || !isCron) {
8478
+ return {
8479
+ promptShape: "standard",
8480
+ retrievalQuery: prompt,
8481
+ skipConversationRecall: false,
8482
+ retrievalBudgetMode: "full"
8483
+ };
8484
+ }
8485
+ const promptShape = classifyRecallPromptShape(prompt);
8486
+ const maxChars = Math.max(120, cfg.cronRecallNormalizedQueryMaxChars);
8487
+ const tokenCap = Math.max(8, cfg.cronRecallInstructionHeavyTokenCap);
8488
+ const retrievalQuery = promptShape === "instruction_heavy" ? buildInstructionHeavyQuery(prompt, tokenCap, maxChars) : buildStandardQuery(prompt, maxChars);
8489
+ const skipConversationRecall = cfg.cronConversationRecallMode === "never" ? true : cfg.cronConversationRecallMode === "always" ? false : promptShape === "instruction_heavy";
8490
+ const retrievalBudgetMode = promptShape === "instruction_heavy" ? "minimal" : "full";
8491
+ return {
8492
+ promptShape,
8493
+ retrievalQuery: retrievalQuery.length > 0 ? retrievalQuery : normalizedPrompt.slice(0, maxChars),
8494
+ skipConversationRecall,
8495
+ retrievalBudgetMode
8496
+ };
8497
+ }
8498
+
8278
8499
  // src/boxes.ts
8279
8500
  import { mkdir as mkdir10, writeFile as writeFile10, readFile as readFile11, readdir as readdir6 } from "fs/promises";
8280
8501
  import path13 from "path";
@@ -10214,6 +10435,9 @@ function filterRecallCandidates(candidates, options) {
10214
10435
  const scopedByNamespace = options.namespacesEnabled ? candidates.filter((r) => options.recallNamespaces.includes(options.resolveNamespace(r.path))) : candidates;
10215
10436
  return scopedByNamespace.filter((r) => !isArtifactMemoryPath(r.path)).slice(0, Math.max(0, options.limit));
10216
10437
  }
10438
+ function tokenizeRecallQuery(prompt) {
10439
+ return prompt.toLowerCase().split(/[^a-z0-9]+/i).map((t) => t.trim()).filter((t) => t.length >= 3);
10440
+ }
10217
10441
  function hasLifecycleMetadata(frontmatter) {
10218
10442
  return frontmatter.lifecycleState !== void 0 || frontmatter.verificationState !== void 0 || frontmatter.policyClass !== void 0 || frontmatter.lastValidatedAt !== void 0 || frontmatter.decayScore !== void 0 || frontmatter.heatScore !== void 0;
10219
10443
  }
@@ -10441,6 +10665,7 @@ var Orchestrator = class _Orchestrator {
10441
10665
  thresholdMs: config.slowLogThresholdMs
10442
10666
  },
10443
10667
  updateTimeoutMs: config.qmdUpdateTimeoutMs,
10668
+ updateMinIntervalMs: config.qmdUpdateMinIntervalMs,
10444
10669
  qmdPath: config.qmdPath,
10445
10670
  daemonUrl: config.qmdDaemonEnabled ? config.qmdDaemonUrl : void 0,
10446
10671
  daemonRecheckIntervalMs: config.qmdDaemonRecheckIntervalMs
@@ -10454,6 +10679,7 @@ var Orchestrator = class _Orchestrator {
10454
10679
  thresholdMs: config.slowLogThresholdMs
10455
10680
  },
10456
10681
  updateTimeoutMs: config.qmdUpdateTimeoutMs,
10682
+ updateMinIntervalMs: config.qmdUpdateMinIntervalMs,
10457
10683
  qmdPath: config.qmdPath,
10458
10684
  daemonUrl: config.qmdDaemonEnabled ? config.qmdDaemonUrl : void 0,
10459
10685
  daemonRecheckIntervalMs: config.qmdDaemonRecheckIntervalMs
@@ -10967,10 +11193,28 @@ var Orchestrator = class _Orchestrator {
10967
11193
  const maxFetchLimit = Math.min(800, Math.max(fetchLimit, qmdFetchLimit * 8));
10968
11194
  const MAX_ATTEMPTS = 4;
10969
11195
  let bestFiltered = [];
11196
+ let queryFallbackAttempted = false;
11197
+ const runQueryFallback = async () => {
11198
+ if (queryFallbackAttempted) return [];
11199
+ queryFallbackAttempted = true;
11200
+ const queryResults = await this.qmd.search(prompt, options.collection, fetchLimit);
11201
+ const filteredQueryResults = filterRecallCandidates(queryResults, {
11202
+ namespacesEnabled: options.namespacesEnabled,
11203
+ recallNamespaces: options.recallNamespaces,
11204
+ resolveNamespace: options.resolveNamespace,
11205
+ limit: fetchLimit
11206
+ });
11207
+ if (filteredQueryResults.length > 0) {
11208
+ log.debug(
11209
+ `QMD query fallback returned ${filteredQueryResults.length} candidates after hybrid underfilled`
11210
+ );
11211
+ }
11212
+ return filteredQueryResults;
11213
+ };
10970
11214
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
10971
11215
  const memoryResults = await this.qmd.hybridSearch(
10972
11216
  prompt,
10973
- void 0,
11217
+ options.collection,
10974
11218
  fetchLimit
10975
11219
  );
10976
11220
  const filteredResults = filterRecallCandidates(memoryResults, {
@@ -10986,17 +11230,27 @@ var Orchestrator = class _Orchestrator {
10986
11230
  bestFiltered = filteredResults;
10987
11231
  }
10988
11232
  if (memoryResults.length === 0) {
11233
+ const queryFallback = await runQueryFallback();
11234
+ if (queryFallback.length > 0) {
11235
+ return queryFallback.slice(0, qmdFetchLimit);
11236
+ }
10989
11237
  return filteredResults;
10990
11238
  }
10991
11239
  if (memoryResults.length < fetchLimit && filteredResults.length > 0) {
10992
11240
  return filteredResults;
10993
11241
  }
10994
11242
  if (fetchLimit >= maxFetchLimit) {
10995
- return filteredResults;
11243
+ break;
10996
11244
  }
10997
11245
  const growth = Math.max(20, Math.floor(fetchLimit / 2));
10998
11246
  fetchLimit = Math.min(maxFetchLimit, fetchLimit + growth);
10999
11247
  }
11248
+ if (bestFiltered.length === 0) {
11249
+ const queryFallback = await runQueryFallback();
11250
+ if (queryFallback.length > 0) {
11251
+ return queryFallback.slice(0, qmdFetchLimit);
11252
+ }
11253
+ }
11000
11254
  return bestFiltered;
11001
11255
  }
11002
11256
  async expandResultsViaGraph(options) {
@@ -11080,6 +11334,14 @@ var Orchestrator = class _Orchestrator {
11080
11334
  const recallStart = Date.now();
11081
11335
  const timings = {};
11082
11336
  const sections = [];
11337
+ const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
11338
+ cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
11339
+ cronRecallNormalizedQueryMaxChars: this.config.cronRecallNormalizedQueryMaxChars,
11340
+ cronRecallInstructionHeavyTokenCap: this.config.cronRecallInstructionHeavyTokenCap,
11341
+ cronConversationRecallMode: this.config.cronConversationRecallMode
11342
+ });
11343
+ const retrievalQuery = queryPolicy.retrievalQuery || prompt;
11344
+ timings.queryPolicy = `${queryPolicy.promptShape}/${queryPolicy.retrievalBudgetMode}${queryPolicy.skipConversationRecall ? "/skip-conv" : ""}`;
11083
11345
  const recallMode = resolveEffectiveRecallMode({
11084
11346
  plannerEnabled: this.config.recallPlannerEnabled,
11085
11347
  graphRecallEnabled: this.config.graphRecallEnabled,
@@ -11087,7 +11349,12 @@ var Orchestrator = class _Orchestrator {
11087
11349
  prompt
11088
11350
  });
11089
11351
  timings.recallPlan = recallMode;
11090
- const recallResultLimit = recallMode === "no_recall" ? 0 : recallMode === "minimal" ? Math.max(0, Math.min(this.config.qmdMaxResults, this.config.recallPlannerMaxQmdResultsMinimal)) : this.config.qmdMaxResults;
11352
+ const plannerRecallResultLimit = recallMode === "no_recall" ? 0 : recallMode === "minimal" ? Math.max(0, Math.min(this.config.qmdMaxResults, this.config.recallPlannerMaxQmdResultsMinimal)) : this.config.qmdMaxResults;
11353
+ const policyMinimalLimit = Math.max(
11354
+ 0,
11355
+ Math.min(this.config.qmdMaxResults, this.config.recallPlannerMaxQmdResultsMinimal)
11356
+ );
11357
+ const recallResultLimit = recallMode !== "no_recall" && queryPolicy.retrievalBudgetMode === "minimal" ? Math.min(plannerRecallResultLimit, policyMinimalLimit) : plannerRecallResultLimit;
11091
11358
  const recallHeadroom = this.config.verbatimArtifactsEnabled ? Math.max(12, this.config.verbatimArtifactsMaxRecall * 4) : 12;
11092
11359
  const computedFetchLimit = recallResultLimit === 0 ? 0 : Math.max(recallResultLimit, Math.min(200, recallResultLimit + recallHeadroom));
11093
11360
  const qmdFetchLimit = computedFetchLimit;
@@ -11154,7 +11421,11 @@ var Orchestrator = class _Orchestrator {
11154
11421
  timings.artifacts = "skip(limit=0)";
11155
11422
  return [];
11156
11423
  }
11157
- const results = await this.recallArtifactsAcrossNamespaces(prompt, recallNamespaces, targetCount);
11424
+ const results = await this.recallArtifactsAcrossNamespaces(
11425
+ retrievalQuery,
11426
+ recallNamespaces,
11427
+ targetCount
11428
+ );
11158
11429
  timings.artifacts = `${Date.now() - t0}ms`;
11159
11430
  return results;
11160
11431
  })();
@@ -11170,7 +11441,7 @@ var Orchestrator = class _Orchestrator {
11170
11441
  }
11171
11442
  const t0 = Date.now();
11172
11443
  const filteredResults = await this.fetchQmdMemoryResultsWithArtifactTopUp(
11173
- prompt,
11444
+ retrievalQuery,
11174
11445
  qmdFetchLimit,
11175
11446
  qmdHybridFetchLimit,
11176
11447
  {
@@ -11254,17 +11525,17 @@ ${tmtNode.summary}`);
11254
11525
  memoryResults = merged;
11255
11526
  await this.recordLastGraphRecallSnapshot({
11256
11527
  storage: profileStorage,
11257
- prompt,
11528
+ prompt: retrievalQuery,
11258
11529
  recallMode,
11259
11530
  recallNamespaces,
11260
11531
  seedPaths,
11261
11532
  expandedPaths
11262
11533
  });
11263
11534
  }
11264
- memoryResults = await this.boostSearchResults(memoryResults, recallNamespaces, prompt);
11535
+ memoryResults = await this.boostSearchResults(memoryResults, recallNamespaces, retrievalQuery);
11265
11536
  if (this.config.rerankEnabled && this.config.rerankProvider === "local") {
11266
11537
  const ranked = await rerankLocalOrNoop({
11267
- query: prompt,
11538
+ query: retrievalQuery,
11268
11539
  candidates: memoryResults.slice(0, this.config.rerankMaxCandidates).map((r) => ({
11269
11540
  id: r.path,
11270
11541
  snippet: r.snippet || r.path
@@ -11296,33 +11567,48 @@ ${tmtNode.summary}`);
11296
11567
  }
11297
11568
  memoryResults = memoryResults.slice(0, recallResultLimit);
11298
11569
  if (memoryResults.length > 0) {
11299
- const memoryIds = this.extractMemoryIdsFromResults(memoryResults);
11300
- this.trackMemoryAccess(memoryIds);
11301
- if (sessionKey) {
11302
- const unique = Array.from(new Set(memoryIds)).slice(0, 40);
11303
- this.lastRecall.record({ sessionKey, query: prompt, memoryIds: unique }).catch((err) => log.debug(`last recall record failed: ${err}`));
11304
- }
11305
- sections.push(this.formatQmdResults("Relevant Memories", memoryResults));
11570
+ this.publishRecallResults({
11571
+ title: "Relevant Memories",
11572
+ results: memoryResults,
11573
+ sections,
11574
+ retrievalQuery,
11575
+ sessionKey
11576
+ });
11306
11577
  } else {
11307
- const embeddingResults = await this.searchEmbeddingFallback(prompt, embeddingFetchLimit);
11578
+ const embeddingResults = await this.searchEmbeddingFallback(retrievalQuery, embeddingFetchLimit);
11308
11579
  const scopedCandidates = filterRecallCandidates(embeddingResults, {
11309
11580
  namespacesEnabled: this.config.namespacesEnabled,
11310
11581
  recallNamespaces,
11311
11582
  resolveNamespace: (p) => this.namespaceFromPath(p),
11312
11583
  limit: embeddingFetchLimit
11313
11584
  });
11314
- const scoped = (await this.boostSearchResults(scopedCandidates, recallNamespaces, prompt)).slice(
11585
+ const scoped = (await this.boostSearchResults(scopedCandidates, recallNamespaces, retrievalQuery)).slice(
11315
11586
  0,
11316
11587
  recallResultLimit
11317
11588
  );
11318
11589
  if (scoped.length > 0) {
11319
- const memoryIds = this.extractMemoryIdsFromResults(scoped);
11320
- this.trackMemoryAccess(memoryIds);
11321
- if (sessionKey) {
11322
- const unique = Array.from(new Set(memoryIds)).slice(0, 40);
11323
- this.lastRecall.record({ sessionKey, query: prompt, memoryIds: unique }).catch((err) => log.debug(`last recall record failed: ${err}`));
11590
+ this.publishRecallResults({
11591
+ title: "Relevant Memories",
11592
+ results: scoped,
11593
+ sections,
11594
+ retrievalQuery,
11595
+ sessionKey
11596
+ });
11597
+ } else {
11598
+ const longTerm = await this.applyColdFallbackPipeline({
11599
+ prompt: retrievalQuery,
11600
+ recallNamespaces,
11601
+ recallResultLimit
11602
+ });
11603
+ if (longTerm.length > 0) {
11604
+ this.publishRecallResults({
11605
+ title: "Long-Term Memories (Fallback)",
11606
+ results: longTerm,
11607
+ sections,
11608
+ retrievalQuery,
11609
+ sessionKey
11610
+ });
11324
11611
  }
11325
- sections.push(this.formatQmdResults("Relevant Memories", scoped));
11326
11612
  }
11327
11613
  }
11328
11614
  if (globalResults.length > 0) {
@@ -11345,25 +11631,26 @@ ${tmtNode.summary}`);
11345
11631
  );
11346
11632
  }
11347
11633
  } else if (recallResultLimit > 0 && (!this.config.qmdEnabled || !this.qmd.isAvailable())) {
11348
- const embeddingResults = await this.searchEmbeddingFallback(prompt, embeddingFetchLimit);
11634
+ const embeddingResults = await this.searchEmbeddingFallback(retrievalQuery, embeddingFetchLimit);
11349
11635
  const scopedCandidates = filterRecallCandidates(embeddingResults, {
11350
11636
  namespacesEnabled: this.config.namespacesEnabled,
11351
11637
  recallNamespaces,
11352
11638
  resolveNamespace: (p) => this.namespaceFromPath(p),
11353
11639
  limit: embeddingFetchLimit
11354
11640
  });
11355
- const scoped = (await this.boostSearchResults(scopedCandidates, recallNamespaces, prompt)).slice(
11356
- 0,
11357
- recallResultLimit
11358
- );
11641
+ const scoped = (await this.boostSearchResults(
11642
+ scopedCandidates,
11643
+ recallNamespaces,
11644
+ retrievalQuery
11645
+ )).slice(0, recallResultLimit);
11359
11646
  if (scoped.length > 0) {
11360
- const memoryIds = this.extractMemoryIdsFromResults(scoped);
11361
- this.trackMemoryAccess(memoryIds);
11362
- if (sessionKey) {
11363
- const unique = Array.from(new Set(memoryIds)).slice(0, 40);
11364
- this.lastRecall.record({ sessionKey, query: prompt, memoryIds: unique }).catch((err) => log.debug(`last recall record failed: ${err}`));
11365
- }
11366
- sections.push(this.formatQmdResults("Relevant Memories", scoped));
11647
+ this.publishRecallResults({
11648
+ title: "Relevant Memories",
11649
+ results: scoped,
11650
+ sections,
11651
+ retrievalQuery,
11652
+ sessionKey
11653
+ });
11367
11654
  } else {
11368
11655
  const memories = await this.readAllMemoriesForNamespaces(recallNamespaces);
11369
11656
  if (memories.length > 0) {
@@ -11382,14 +11669,51 @@ ${tmtNode.summary}`);
11382
11669
  snippet: m.content,
11383
11670
  score: 1 - i / Math.max(recentSorted.length, 1)
11384
11671
  }));
11385
- const recent = (await this.boostSearchResults(recentAsResults, recallNamespaces, prompt, preloadedMap)).sort((a, b) => b.score - a.score).slice(0, recallResultLimit);
11386
- const memoryIds = recent.map((r) => r.docid).filter(Boolean);
11387
- this.trackMemoryAccess(memoryIds);
11388
- if (sessionKey) {
11389
- const unique = Array.from(new Set(memoryIds)).slice(0, 40);
11390
- this.lastRecall.record({ sessionKey, query: prompt, memoryIds: unique }).catch((err) => log.debug(`last recall record failed: ${err}`));
11672
+ const recent = (await this.boostSearchResults(
11673
+ recentAsResults,
11674
+ recallNamespaces,
11675
+ retrievalQuery,
11676
+ preloadedMap
11677
+ )).sort((a, b) => b.score - a.score).slice(0, recallResultLimit);
11678
+ if (recent.length > 0) {
11679
+ this.publishRecallResults({
11680
+ title: "Recent Memories",
11681
+ results: recent,
11682
+ sections,
11683
+ retrievalQuery,
11684
+ sessionKey
11685
+ });
11686
+ } else {
11687
+ const longTerm = await this.applyColdFallbackPipeline({
11688
+ prompt: retrievalQuery,
11689
+ recallNamespaces,
11690
+ recallResultLimit
11691
+ });
11692
+ if (longTerm.length > 0) {
11693
+ this.publishRecallResults({
11694
+ title: "Long-Term Memories (Fallback)",
11695
+ results: longTerm,
11696
+ sections,
11697
+ retrievalQuery,
11698
+ sessionKey
11699
+ });
11700
+ }
11701
+ }
11702
+ } else {
11703
+ const longTerm = await this.applyColdFallbackPipeline({
11704
+ prompt: retrievalQuery,
11705
+ recallNamespaces,
11706
+ recallResultLimit
11707
+ });
11708
+ if (longTerm.length > 0) {
11709
+ this.publishRecallResults({
11710
+ title: "Long-Term Memories (Fallback)",
11711
+ results: longTerm,
11712
+ sections,
11713
+ retrievalQuery,
11714
+ sessionKey
11715
+ });
11391
11716
  }
11392
- sections.push(this.formatQmdResults("Recent Memories", recent));
11393
11717
  }
11394
11718
  }
11395
11719
  if (isDisagreementPrompt(prompt)) {
@@ -11465,13 +11789,13 @@ ${formatted}`);
11465
11789
  }
11466
11790
  timings.summaries = `${Date.now() - summariesT0}ms`;
11467
11791
  const convT0 = Date.now();
11468
- if (this.config.conversationIndexEnabled && this.conversationQmd && this.conversationQmd.isAvailable()) {
11792
+ if (this.config.conversationIndexEnabled && !queryPolicy.skipConversationRecall && this.conversationQmd && this.conversationQmd.isAvailable()) {
11469
11793
  const startedAtMs = Date.now();
11470
11794
  const timeoutMs = Math.max(200, this.config.conversationRecallTimeoutMs);
11471
11795
  const topK = Math.max(1, this.config.conversationRecallTopK);
11472
11796
  const maxChars = Math.max(400, this.config.conversationRecallMaxChars);
11473
11797
  const results = await Promise.race([
11474
- this.conversationQmd.search(prompt, void 0, topK),
11798
+ this.conversationQmd.search(retrievalQuery, void 0, topK),
11475
11799
  new Promise((resolve) => setTimeout(() => resolve([]), timeoutMs))
11476
11800
  ]).catch(() => []);
11477
11801
  const durationMs = Date.now() - startedAtMs;
@@ -12614,6 +12938,15 @@ ${snippet}`;
12614
12938
 
12615
12939
  ${lines.join("\n\n")}`;
12616
12940
  }
12941
+ publishRecallResults(options) {
12942
+ const memoryIds = this.extractMemoryIdsFromResults(options.results);
12943
+ this.trackMemoryAccess(memoryIds);
12944
+ if (options.sessionKey) {
12945
+ const unique = Array.from(new Set(memoryIds)).slice(0, 40);
12946
+ this.lastRecall.record({ sessionKey: options.sessionKey, query: options.retrievalQuery, memoryIds: unique }).catch((err) => log.debug(`last recall record failed: ${err}`));
12947
+ }
12948
+ options.sections.push(this.formatQmdResults(options.title, options.results));
12949
+ }
12617
12950
  async searchEmbeddingFallback(query, limit) {
12618
12951
  if (!this.config.embeddingFallbackEnabled) return [];
12619
12952
  if (!await this.embeddingFallback.isAvailable()) return [];
@@ -12633,6 +12966,123 @@ ${lines.join("\n\n")}`;
12633
12966
  }
12634
12967
  return results;
12635
12968
  }
12969
+ /**
12970
+ * Long-term fallback retrieval.
12971
+ * Searches archived memories only, and is invoked only when hot recall returns zero hits.
12972
+ */
12973
+ async searchLongTermArchiveFallback(prompt, recallNamespaces, limit) {
12974
+ const cappedLimit = Math.max(0, limit);
12975
+ if (cappedLimit === 0) return [];
12976
+ const tokens = Array.from(new Set(tokenizeRecallQuery(prompt)));
12977
+ if (tokens.length === 0) return [];
12978
+ const archivedMemories = await this.readArchivedMemoriesForNamespaces(recallNamespaces);
12979
+ if (archivedMemories.length === 0) return [];
12980
+ const scored = [];
12981
+ for (const memory of archivedMemories) {
12982
+ const haystack = [
12983
+ memory.content,
12984
+ memory.frontmatter.category,
12985
+ ...memory.frontmatter.tags ?? []
12986
+ ].join(" ").toLowerCase();
12987
+ let hits = 0;
12988
+ for (const token of tokens) {
12989
+ if (haystack.includes(token)) hits += 1;
12990
+ }
12991
+ if (hits === 0) continue;
12992
+ const normalized = hits / tokens.length;
12993
+ scored.push({
12994
+ docid: memory.frontmatter.id,
12995
+ path: memory.path,
12996
+ score: normalized,
12997
+ snippet: memory.content.slice(0, 400).replace(/\n/g, " ")
12998
+ });
12999
+ }
13000
+ return scored.sort((a, b) => b.score - a.score).slice(0, cappedLimit);
13001
+ }
13002
+ async applyColdFallbackPipeline(options) {
13003
+ const coldQmdEnabled = this.config.qmdColdTierEnabled === true;
13004
+ const coldCollection = this.config.qmdColdCollection ?? "openclaw-engram-cold";
13005
+ const coldMaxResults = this.config.qmdColdMaxResults ?? this.config.qmdMaxResults;
13006
+ let longTerm = [];
13007
+ if (coldQmdEnabled && this.config.qmdEnabled && this.qmd.isAvailable()) {
13008
+ const coldFetchLimit = Math.max(
13009
+ 0,
13010
+ Math.min(options.recallResultLimit, Math.max(0, coldMaxResults))
13011
+ );
13012
+ if (coldFetchLimit > 0) {
13013
+ const coldHybridLimit = computeQmdHybridFetchLimit(
13014
+ coldFetchLimit,
13015
+ false,
13016
+ 0
13017
+ );
13018
+ longTerm = await this.fetchQmdMemoryResultsWithArtifactTopUp(
13019
+ options.prompt,
13020
+ coldFetchLimit,
13021
+ coldHybridLimit,
13022
+ {
13023
+ namespacesEnabled: this.config.namespacesEnabled,
13024
+ recallNamespaces: options.recallNamespaces,
13025
+ resolveNamespace: (p) => this.namespaceFromPath(p),
13026
+ collection: coldCollection
13027
+ }
13028
+ );
13029
+ if (longTerm.length > 0) {
13030
+ log.debug(`cold-tier recall source=cold-qmd collection=${coldCollection} hits=${longTerm.length}`);
13031
+ }
13032
+ }
13033
+ }
13034
+ if (longTerm.length === 0) {
13035
+ longTerm = await this.searchLongTermArchiveFallback(
13036
+ options.prompt,
13037
+ options.recallNamespaces,
13038
+ options.recallResultLimit
13039
+ );
13040
+ if (longTerm.length > 0) {
13041
+ log.debug("cold-tier recall source=archive-scan");
13042
+ }
13043
+ }
13044
+ if (longTerm.length === 0) return [];
13045
+ let results = await this.boostSearchResults(
13046
+ longTerm,
13047
+ options.recallNamespaces,
13048
+ options.prompt,
13049
+ void 0,
13050
+ { allowLifecycleFiltered: true }
13051
+ );
13052
+ if (this.config.rerankEnabled && this.config.rerankProvider === "local") {
13053
+ const ranked = await rerankLocalOrNoop({
13054
+ query: options.prompt,
13055
+ candidates: results.slice(0, this.config.rerankMaxCandidates).map((r) => ({
13056
+ id: r.path,
13057
+ snippet: r.snippet || r.path
13058
+ })),
13059
+ local: this.localLlm,
13060
+ enabled: true,
13061
+ timeoutMs: this.config.rerankTimeoutMs,
13062
+ maxCandidates: this.config.rerankMaxCandidates,
13063
+ cache: this.rerankCache,
13064
+ cacheEnabled: this.config.rerankCacheEnabled,
13065
+ cacheTtlMs: this.config.rerankCacheTtlMs
13066
+ });
13067
+ if (ranked && ranked.length > 0) {
13068
+ const byPath = new Map(results.map((r) => [r.path, r]));
13069
+ const reordered = [];
13070
+ for (const p of ranked) {
13071
+ const it = byPath.get(p);
13072
+ if (it) reordered.push(it);
13073
+ }
13074
+ const rankedSet = new Set(ranked);
13075
+ for (const r of results) {
13076
+ if (!rankedSet.has(r.path)) reordered.push(r);
13077
+ }
13078
+ results = reordered;
13079
+ }
13080
+ }
13081
+ if (this.config.rerankEnabled && this.config.rerankProvider === "cloud") {
13082
+ log.debug("rerankProvider=cloud is reserved/experimental in v2.2.0; skipping rerank");
13083
+ }
13084
+ return results.slice(0, options.recallResultLimit);
13085
+ }
12636
13086
  // ---------------------------------------------------------------------------
12637
13087
  // Access Tracking (Phase 1A)
12638
13088
  // ---------------------------------------------------------------------------
@@ -12701,7 +13151,7 @@ ${lines.join("\n\n")}`;
12701
13151
  * Apply recency, access count, and importance boosting to QMD search results.
12702
13152
  * Returns re-ranked results.
12703
13153
  */
12704
- async boostSearchResults(results, _recallNamespaces, prompt, preloadedMemoryMap) {
13154
+ async boostSearchResults(results, _recallNamespaces, prompt, preloadedMemoryMap, options) {
12705
13155
  if (results.length === 0) return results;
12706
13156
  const now = Date.now();
12707
13157
  const memoryByPath = preloadedMemoryMap ? new Map(preloadedMemoryMap) : /* @__PURE__ */ new Map();
@@ -12749,7 +13199,7 @@ ${lines.join("\n\n")}`;
12749
13199
  const memory = memoryByPath.get(r.path);
12750
13200
  let score = r.score;
12751
13201
  if (memory) {
12752
- if (shouldFilterLifecycleRecallCandidate(memory.frontmatter, {
13202
+ if (options?.allowLifecycleFiltered !== true && shouldFilterLifecycleRecallCandidate(memory.frontmatter, {
12753
13203
  lifecyclePolicyEnabled: this.config.lifecyclePolicyEnabled,
12754
13204
  lifecycleFilterStaleEnabled: this.config.lifecycleFilterStaleEnabled
12755
13205
  })) {
@@ -12945,6 +13395,16 @@ ${lines.join("\n\n")}`;
12945
13395
  );
12946
13396
  return lists.flat();
12947
13397
  }
13398
+ async readArchivedMemoriesForNamespaces(namespaces) {
13399
+ const uniq = Array.from(new Set(namespaces.filter(Boolean)));
13400
+ const lists = await Promise.all(
13401
+ uniq.map(async (ns) => {
13402
+ const sm = await this.storageRouter.storageFor(ns);
13403
+ return sm.readArchivedMemories();
13404
+ })
13405
+ );
13406
+ return lists.flat();
13407
+ }
12948
13408
  };
12949
13409
 
12950
13410
  // src/tools.ts
@@ -14483,6 +14943,7 @@ function registerCli(api, orchestrator) {
14483
14943
  console.log("Missing query. Usage: openclaw engram search <query>");
14484
14944
  return;
14485
14945
  }
14946
+ await orchestrator.qmd.probe();
14486
14947
  if (orchestrator.qmd.isAvailable()) {
14487
14948
  const results = await orchestrator.qmd.search(
14488
14949
  query,
@@ -14511,12 +14972,18 @@ function registerCli(api, orchestrator) {
14511
14972
  const matches = memories.filter(
14512
14973
  (m) => m.content.toLowerCase().includes(lowerQuery) || m.frontmatter.tags.some((t) => t.includes(lowerQuery))
14513
14974
  );
14975
+ const qmdStatus = orchestrator.qmd.debugStatus();
14514
14976
  if (matches.length === 0) {
14515
- console.log(`No results for: "${query}" (QMD not available, using text search)`);
14977
+ console.log(
14978
+ `No results for: "${query}" (QMD unavailable in this CLI process; text search fallback).`
14979
+ );
14980
+ console.log(`QMD status: ${qmdStatus}`);
14516
14981
  return;
14517
14982
  }
14518
14983
  console.log(`
14519
- === Text Search: "${query}" (${matches.length} results) ===
14984
+ === Text Search Fallback: "${query}" (${matches.length} results) ===
14985
+ `);
14986
+ console.log(`QMD status: ${qmdStatus}
14520
14987
  `);
14521
14988
  for (const m of matches.slice(0, maxResults)) {
14522
14989
  console.log(` [${m.frontmatter.category}] ${m.content.slice(0, 120)}`);
@@ -14932,6 +15399,7 @@ import { readFile as readFile23, writeFile as writeFile21 } from "fs/promises";
14932
15399
  import { readFileSync as readFileSync4 } from "fs";
14933
15400
  import path34 from "path";
14934
15401
  import os5 from "os";
15402
+ var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
14935
15403
  function loadPluginConfigFromFile() {
14936
15404
  try {
14937
15405
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
@@ -14946,6 +15414,24 @@ function loadPluginConfigFromFile() {
14946
15414
  return void 0;
14947
15415
  }
14948
15416
  }
15417
+ function wildcardToRegExp(pattern) {
15418
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
15419
+ return new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
15420
+ }
15421
+ function shouldSkipRecallForSession(sessionKey, cfg) {
15422
+ const isCron = sessionKey.includes(":cron:");
15423
+ if (!isCron) return false;
15424
+ if (cfg.cronRecallMode === "none") return true;
15425
+ if (cfg.cronRecallMode === "all") return false;
15426
+ if (cfg.cronRecallAllowlist.length === 0) return true;
15427
+ return !cfg.cronRecallAllowlist.some((pattern) => {
15428
+ try {
15429
+ return wildcardToRegExp(pattern).test(sessionKey);
15430
+ } catch {
15431
+ return false;
15432
+ }
15433
+ });
15434
+ }
14949
15435
  var index_default = {
14950
15436
  id: "openclaw-engram",
14951
15437
  name: "Engram (Local Memory)",
@@ -14965,6 +15451,11 @@ var index_default = {
14965
15451
  log.info(
14966
15452
  `initialized (debug=${cfg.debug}, qmdEnabled=${cfg.qmdEnabled}, transcriptEnabled=${cfg.transcriptEnabled}, hourlySummariesEnabled=${cfg.hourlySummariesEnabled}, localLlmEnabled=${cfg.localLlmEnabled})`
14967
15453
  );
15454
+ if (globalThis[ENGRAM_REGISTERED_GUARD] === true) {
15455
+ log.warn("register called more than once; skipping duplicate hook/tool registration");
15456
+ return;
15457
+ }
15458
+ globalThis[ENGRAM_REGISTERED_GUARD] = true;
14968
15459
  const existing = globalThis.__openclawEngramOrchestrator;
14969
15460
  const orchestrator = existing?.recall ? existing : new Orchestrator(cfg);
14970
15461
  globalThis.__openclawEngramOrchestrator = orchestrator;
@@ -14978,6 +15469,24 @@ var index_default = {
14978
15469
  if (!prompt || prompt.length < 5) return;
14979
15470
  const sessionKey = ctx?.sessionKey ?? "default";
14980
15471
  log.debug(`before_agent_start: sessionKey=${sessionKey}, promptLen=${prompt.length}`);
15472
+ log.debug(
15473
+ `before_agent_start: cronRecallMode=${cfg.cronRecallMode}, allowlistCount=${cfg.cronRecallAllowlist.length}`
15474
+ );
15475
+ if (sessionKey.includes(":cron:") && cfg.cronRecallMode === "allowlist") {
15476
+ const matchedPattern = cfg.cronRecallAllowlist.find((pattern) => {
15477
+ const re = wildcardToRegExp(pattern);
15478
+ return re.test(sessionKey);
15479
+ });
15480
+ log.debug(
15481
+ `before_agent_start: cron allowlist match=${matchedPattern ? "yes" : "no"} pattern=${matchedPattern ?? "none"}`
15482
+ );
15483
+ }
15484
+ if (shouldSkipRecallForSession(sessionKey, cfg)) {
15485
+ log.debug(
15486
+ `before_agent_start: skip recall for cron session ${sessionKey} (mode=${cfg.cronRecallMode})`
15487
+ );
15488
+ return;
15489
+ }
14981
15490
  try {
14982
15491
  await orchestrator.maybeRunFileHygiene().catch(() => void 0);
14983
15492
  const context = await orchestrator.recall(prompt, sessionKey);
@@ -15159,6 +15668,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
15159
15668
  log.info("engram memory system ready");
15160
15669
  },
15161
15670
  stop: () => {
15671
+ globalThis[ENGRAM_REGISTERED_GUARD] = false;
15162
15672
  log.info("stopped");
15163
15673
  }
15164
15674
  });