@joshuaswarren/openclaw-engram 9.0.38 → 9.0.40

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/README.md CHANGED
@@ -40,6 +40,8 @@ AI agents forget everything between conversations. Engram fixes that.
40
40
  - **Red-team benchmark packs** — Engram's eval harness can now validate and count typed `memory-red-team` benchmark packs so poisoning-defense regression suites stay explicit and reviewable instead of hiding inside generic benchmark metadata.
41
41
  - **Cue-anchor index foundation** — Engram can now, when `harmonicRetrievalEnabled` and `abstractionAnchorsEnabled` are enabled, persist typed cue anchors for entities, files, tools, outcomes, constraints, and dates, inspect them with `openclaw engram cue-anchor-status`, and keep harmonic retrieval grounded in explicit abstraction-to-cue links before blending logic lands.
42
42
  - **Harmonic retrieval diagnostics** — Engram can now, when `harmonicRetrievalEnabled` is enabled, blend abstraction-node evidence with cue-anchor matches into a dedicated `Harmonic Retrieval` recall section and inspect those blended results with `openclaw engram harmonic-search`.
43
+ - **Verified episodic recall** — Engram can now, when `verifiedRecallEnabled` is enabled, inject a dedicated `Verified Episodes` recall section that reuses memory boxes but only surfaces boxes whose cited source memories still verify as non-archived episodes.
44
+ - **Semantic rule promotion** — Engram can now, when `semanticRulePromotionEnabled` is enabled, promote explicit `IF ... THEN ...` rules from verified episodic memories into durable `rule` memories with lineage, source-memory provenance, duplicate suppression, and the operator-facing `openclaw engram semantic-rule-promote` CLI.
43
45
  - **Zero-config start** — Install, add an API key, restart. Engram works out of the box with sensible defaults and progressively unlocks advanced features as you enable them.
44
46
 
45
47
  ## Quick Start
@@ -167,6 +169,8 @@ openclaw engram objective-state-status # Objective-state snapshot counts a
167
169
  openclaw engram causal-trajectory-status # Causal-trajectory record counts and latest stored chain
168
170
  openclaw engram trust-zone-status # Trust-zone record counts and latest stored record
169
171
  openclaw engram trust-zone-promote # Dry-run or apply a trust-zone promotion with provenance/corroboration enforcement
172
+ openclaw engram harmonic-search <query> # Preview blended harmonic retrieval matches
173
+ openclaw engram verified-recall-search <query> # Preview verified episodic recall matches
170
174
  openclaw engram conversation-index-health # Conversation index status
171
175
  openclaw engram graph-health # Entity graph status
172
176
  openclaw engram tier-status # Hot/cold tier metrics
@@ -203,6 +207,10 @@ Key settings:
203
207
  | `trustZoneRecallEnabled` | `false` | Inject prompt-relevant working and trusted trust-zone records into recall context |
204
208
  | `memoryPoisoningDefenseEnabled` | `false` | Enable deterministic provenance trust scoring and corroboration requirements for risky trusted promotions |
205
209
  | `memoryRedTeamBenchEnabled` | `false` | Enable typed memory red-team benchmark pack support and status accounting for poisoning-defense suites |
210
+ | `harmonicRetrievalEnabled` | `false` | Enable harmonic retrieval blending over abstraction nodes and cue anchors, including the dedicated recall section and `harmonic-search` diagnostics |
211
+ | `abstractionAnchorsEnabled` | `false` | Enable typed cue-anchor indexing for abstraction nodes and expose the anchor store through status tooling |
212
+ | `verifiedRecallEnabled` | `false` | Inject prompt-relevant memory boxes only when their cited source memories verify as non-archived episodes |
213
+ | `semanticRulePromotionEnabled` | `false` | Enable deterministic promotion of explicit `IF ... THEN ...` rules from verified episodic memories via `openclaw engram semantic-rule-promote` |
206
214
 
207
215
  Full reference: [Config Reference](docs/config-reference.md)
208
216
 
package/dist/index.js CHANGED
@@ -304,6 +304,8 @@ function parseConfig(raw) {
304
304
  memoryRedTeamBenchEnabled: cfg.memoryRedTeamBenchEnabled === true,
305
305
  harmonicRetrievalEnabled: cfg.harmonicRetrievalEnabled === true,
306
306
  abstractionAnchorsEnabled: cfg.abstractionAnchorsEnabled === true,
307
+ verifiedRecallEnabled: cfg.verifiedRecallEnabled === true,
308
+ semanticRulePromotionEnabled: cfg.semanticRulePromotionEnabled === true,
307
309
  abstractionNodeStoreDir: typeof cfg.abstractionNodeStoreDir === "string" && cfg.abstractionNodeStoreDir.trim().length > 0 ? cfg.abstractionNodeStoreDir.trim() : path.join(memoryDir, "state", "abstraction-nodes"),
308
310
  // Local LLM Provider (v2.1)
309
311
  localLlmEnabled: cfg.localLlmEnabled === true || cfg.localLlmEnabled === "true",
@@ -587,6 +589,12 @@ function buildDefaultRecallPipeline(cfg) {
587
589
  maxResults: 3,
588
590
  maxChars: 2200
589
591
  },
592
+ {
593
+ id: "verified-episodes",
594
+ enabled: cfg.verifiedRecallEnabled === true,
595
+ maxResults: 3,
596
+ maxChars: 1800
597
+ },
590
598
  {
591
599
  id: "memories",
592
600
  enabled: true,
@@ -15336,6 +15344,92 @@ async function searchHarmonicRetrieval(options) {
15336
15344
  ).slice(0, options.maxResults);
15337
15345
  }
15338
15346
 
15347
+ // src/verified-recall.ts
15348
+ function createReadOnlyBoxBuilder(memoryDir) {
15349
+ return new BoxBuilder(memoryDir, {
15350
+ memoryBoxesEnabled: true,
15351
+ traceWeaverEnabled: false,
15352
+ boxTopicShiftThreshold: 0.35,
15353
+ boxTimeGapMs: 30 * 60 * 1e3,
15354
+ boxMaxMemories: 50,
15355
+ traceWeaverLookbackDays: 7,
15356
+ traceWeaverOverlapThreshold: 0.4
15357
+ });
15358
+ }
15359
+ function scoreVerifiedEpisodeCandidate(box, verifiedMemories, queryTokens) {
15360
+ const matchedFields = /* @__PURE__ */ new Set();
15361
+ let score = 0;
15362
+ const topicMatches = countRecallTokenOverlap(queryTokens, box.topics.join(" "));
15363
+ if (topicMatches > 0) {
15364
+ score += topicMatches * 3;
15365
+ matchedFields.add("topics");
15366
+ }
15367
+ const goalMatches = countRecallTokenOverlap(queryTokens, box.goal);
15368
+ if (goalMatches > 0) {
15369
+ score += goalMatches * 4;
15370
+ matchedFields.add("goal");
15371
+ }
15372
+ const toolMatches = countRecallTokenOverlap(queryTokens, box.toolsUsed?.join(" "));
15373
+ if (toolMatches > 0) {
15374
+ score += toolMatches * 2;
15375
+ matchedFields.add("toolsUsed");
15376
+ }
15377
+ let episodeContentMatches = 0;
15378
+ for (const memory of verifiedMemories) {
15379
+ episodeContentMatches += countRecallTokenOverlap(queryTokens, memory.content);
15380
+ }
15381
+ if (episodeContentMatches > 0) {
15382
+ score += episodeContentMatches * 4;
15383
+ matchedFields.add("episodeContent");
15384
+ }
15385
+ return { score, matchedFields };
15386
+ }
15387
+ function resolveVerifiedEpisodeMemoriesFromMap(memoryById, memoryIds) {
15388
+ const verified = [];
15389
+ for (const memoryId of memoryIds) {
15390
+ try {
15391
+ const memory = memoryById.get(memoryId);
15392
+ if (!memory) continue;
15393
+ if (memory.frontmatter.status === "archived") continue;
15394
+ if (memory.frontmatter.memoryKind !== "episode") continue;
15395
+ verified.push(memory);
15396
+ } catch {
15397
+ }
15398
+ }
15399
+ return verified;
15400
+ }
15401
+ async function searchVerifiedEpisodes(options) {
15402
+ const queryTokens = new Set(normalizeRecallTokens(options.query, ["what", "which"]));
15403
+ if (queryTokens.size === 0 || options.maxResults <= 0) return [];
15404
+ const storage = new StorageManager(options.memoryDir);
15405
+ const verifiedMemoryById = new Map(
15406
+ (await storage.readAllMemories()).filter((memory) => memory.frontmatter.status !== "archived").filter((memory) => memory.frontmatter.memoryKind === "episode").map((memory) => [memory.frontmatter.id, memory])
15407
+ );
15408
+ const boxes = await createReadOnlyBoxBuilder(options.memoryDir).readRecentBoxes(Math.max(1, Math.floor(options.boxRecallDays ?? 3))).catch(() => []);
15409
+ const candidates = [];
15410
+ for (const box of boxes) {
15411
+ const verifiedMemories = resolveVerifiedEpisodeMemoriesFromMap(verifiedMemoryById, box.memoryIds);
15412
+ if (verifiedMemories.length === 0) continue;
15413
+ const { score, matchedFields } = scoreVerifiedEpisodeCandidate(box, verifiedMemories, queryTokens);
15414
+ if (score <= 0) continue;
15415
+ candidates.push({
15416
+ box,
15417
+ score,
15418
+ matchedFields,
15419
+ verifiedMemories
15420
+ });
15421
+ }
15422
+ return candidates.map((candidate) => ({
15423
+ box: candidate.box,
15424
+ score: candidate.score,
15425
+ verifiedEpisodeCount: candidate.verifiedMemories.length,
15426
+ verifiedMemoryIds: candidate.verifiedMemories.map((memory) => memory.frontmatter.id),
15427
+ matchedFields: [...candidate.matchedFields].sort()
15428
+ })).sort(
15429
+ (left, right) => right.score - left.score || right.verifiedEpisodeCount - left.verifiedEpisodeCount || right.box.sealedAt.localeCompare(left.box.sealedAt)
15430
+ ).slice(0, options.maxResults);
15431
+ }
15432
+
15339
15433
  // src/replay/types.ts
15340
15434
  var VALID_SOURCES = /* @__PURE__ */ new Set(["openclaw", "claude", "chatgpt"]);
15341
15435
  var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
@@ -19120,6 +19214,26 @@ ${r.snippet.trim()}
19120
19214
  timings.harmonicRetrieval = `${Date.now() - t0}ms`;
19121
19215
  return results.length > 0 ? this.formatHarmonicRetrievalResults(results) : null;
19122
19216
  })();
19217
+ const verifiedRecallPromise = (async () => {
19218
+ const t0 = Date.now();
19219
+ if (!this.config.verifiedRecallEnabled || !this.isRecallSectionEnabled("verified-episodes", this.config.verifiedRecallEnabled === true)) {
19220
+ timings.verifiedRecall = "skip";
19221
+ return null;
19222
+ }
19223
+ const maxResults = this.getRecallSectionNumber("verified-episodes", "maxResults") ?? 3;
19224
+ if (maxResults <= 0) {
19225
+ timings.verifiedRecall = "skip(limit=0)";
19226
+ return null;
19227
+ }
19228
+ const results = await searchVerifiedEpisodes({
19229
+ memoryDir: this.config.memoryDir,
19230
+ query: retrievalQuery,
19231
+ maxResults,
19232
+ boxRecallDays: this.config.boxRecallDays
19233
+ });
19234
+ timings.verifiedRecall = `${Date.now() - t0}ms`;
19235
+ return results.length > 0 ? this.formatVerifiedEpisodeResults(results) : null;
19236
+ })();
19123
19237
  const qmdPromise = (async () => {
19124
19238
  if (recallResultLimit <= 0) {
19125
19239
  timings.qmd = "skip(limit=0)";
@@ -19327,6 +19441,7 @@ ${formatted}`;
19327
19441
  causalTrajectorySection,
19328
19442
  trustZoneSection,
19329
19443
  harmonicRetrievalSection,
19444
+ verifiedRecallSection,
19330
19445
  qmdResult,
19331
19446
  transcriptSection,
19332
19447
  compactionSection,
@@ -19343,6 +19458,7 @@ ${formatted}`;
19343
19458
  causalTrajectoryPromise,
19344
19459
  trustZonePromise,
19345
19460
  harmonicRetrievalPromise,
19461
+ verifiedRecallPromise,
19346
19462
  qmdPromise,
19347
19463
  transcriptPromise,
19348
19464
  compactionPromise,
@@ -19409,6 +19525,9 @@ ${tmtNode.summary}`);
19409
19525
  if (harmonicRetrievalSection) {
19410
19526
  this.appendRecallSection(sectionBuckets, "harmonic-retrieval", harmonicRetrievalSection);
19411
19527
  }
19528
+ if (verifiedRecallSection) {
19529
+ this.appendRecallSection(sectionBuckets, "verified-episodes", verifiedRecallSection);
19530
+ }
19412
19531
  if (qmdResult) {
19413
19532
  const t0 = Date.now();
19414
19533
  const { memoryResultsLists, globalResults } = qmdResult;
@@ -21609,6 +21728,29 @@ ${details.join("\n")}`;
21609
21728
  });
21610
21729
  return `## Harmonic Retrieval
21611
21730
 
21731
+ ${lines.join("\n\n")}`;
21732
+ }
21733
+ formatVerifiedEpisodeResults(results) {
21734
+ const lines = results.map(({ box, verifiedEpisodeCount, matchedFields }, index) => {
21735
+ const header = [
21736
+ `[${index + 1}] ${box.sealedAt.replace("T", " ").slice(0, 16)}`,
21737
+ box.traceId ? `trace:${box.traceId.slice(0, 12)}` : "trace:none"
21738
+ ].join(" | ");
21739
+ const details = [
21740
+ box.goal ?? `topics: ${box.topics.join(", ")}`,
21741
+ `verified episodes: ${verifiedEpisodeCount}`
21742
+ ];
21743
+ if (box.toolsUsed && box.toolsUsed.length > 0) {
21744
+ details.push(`tools: ${box.toolsUsed.join(", ")}`);
21745
+ }
21746
+ if (matchedFields.length > 0) {
21747
+ details.push(`matched: ${matchedFields.join(", ")}`);
21748
+ }
21749
+ return `${header}
21750
+ ${details.join("\n")}`;
21751
+ });
21752
+ return `## Verified Episodes
21753
+
21612
21754
  ${lines.join("\n\n")}`;
21613
21755
  }
21614
21756
  summarizeIdentityText(raw, maxLines, maxChars) {
@@ -27106,6 +27248,126 @@ async function runCompatChecks(options) {
27106
27248
  };
27107
27249
  }
27108
27250
 
27251
+ // src/semantic-rule-promotion.ts
27252
+ function normalizeRuleWhitespace(value) {
27253
+ return value.replace(/\s+/g, " ").trim();
27254
+ }
27255
+ function stripTrailingClausePunctuation(value) {
27256
+ return value.replace(/[,:;]+$/g, "").trim();
27257
+ }
27258
+ function canonicalizeRuleContent(value) {
27259
+ return extractExplicitIfThenRule(value) ?? normalizeRuleWhitespace(value);
27260
+ }
27261
+ function canonicalizeRuleKey(value) {
27262
+ return canonicalizeRuleContent(value).toLowerCase();
27263
+ }
27264
+ function extractExplicitIfThenRule(content) {
27265
+ const match = content.match(/\bif\b([\s\S]+?)\bthen\b([\s\S]+?)(?:[.!?](?:\s|$)|$)/i);
27266
+ if (!match) return null;
27267
+ const condition = stripTrailingClausePunctuation(normalizeRuleWhitespace(match[1] ?? ""));
27268
+ const outcome = stripTrailingClausePunctuation(normalizeRuleWhitespace(match[2] ?? ""));
27269
+ if (condition.length === 0 || outcome.length === 0) return null;
27270
+ return `IF ${condition} THEN ${outcome}.`;
27271
+ }
27272
+ function promotionConfidence(memory) {
27273
+ const base = Number.isFinite(memory.frontmatter.confidence) ? memory.frontmatter.confidence : 0.8;
27274
+ return Math.max(0.6, Math.min(0.98, base));
27275
+ }
27276
+ function promotionTags(memory) {
27277
+ return Array.from(/* @__PURE__ */ new Set([...memory.frontmatter.tags ?? [], "semantic-rule", "promoted-rule"]));
27278
+ }
27279
+ function buildSupportLinks(sourceMemoryId, confidence) {
27280
+ return [
27281
+ {
27282
+ targetId: sourceMemoryId,
27283
+ linkType: "supports",
27284
+ strength: confidence,
27285
+ reason: "Promoted from verified episodic memory"
27286
+ }
27287
+ ];
27288
+ }
27289
+ async function promoteSemanticRuleFromMemory(options) {
27290
+ const report = {
27291
+ enabled: options.enabled,
27292
+ dryRun: options.dryRun === true,
27293
+ promoted: [],
27294
+ skipped: []
27295
+ };
27296
+ if (!options.enabled) {
27297
+ report.skipped.push({
27298
+ sourceMemoryId: options.sourceMemoryId,
27299
+ reason: "disabled"
27300
+ });
27301
+ return report;
27302
+ }
27303
+ const storage = new StorageManager(options.memoryDir);
27304
+ const sourceMemory = await storage.getMemoryById(options.sourceMemoryId);
27305
+ if (!sourceMemory) {
27306
+ report.skipped.push({
27307
+ sourceMemoryId: options.sourceMemoryId,
27308
+ reason: "source-memory-missing"
27309
+ });
27310
+ return report;
27311
+ }
27312
+ if (sourceMemory.frontmatter.status === "archived" || sourceMemory.frontmatter.memoryKind !== "episode") {
27313
+ report.skipped.push({
27314
+ sourceMemoryId: options.sourceMemoryId,
27315
+ reason: "source-memory-not-episode"
27316
+ });
27317
+ return report;
27318
+ }
27319
+ const content = extractExplicitIfThenRule(sourceMemory.content);
27320
+ if (!content) {
27321
+ report.skipped.push({
27322
+ sourceMemoryId: options.sourceMemoryId,
27323
+ reason: "no-explicit-rule"
27324
+ });
27325
+ return report;
27326
+ }
27327
+ const ruleKey = canonicalizeRuleKey(content);
27328
+ const existingRule = (await storage.readAllMemories()).find(
27329
+ (memory) => memory.frontmatter.category === "rule" && memory.frontmatter.status !== "archived" && canonicalizeRuleKey(memory.content) === ruleKey
27330
+ );
27331
+ if (existingRule) {
27332
+ report.skipped.push({
27333
+ sourceMemoryId: options.sourceMemoryId,
27334
+ reason: "duplicate-rule",
27335
+ existingRuleId: existingRule.frontmatter.id
27336
+ });
27337
+ return report;
27338
+ }
27339
+ const confidence = promotionConfidence(sourceMemory);
27340
+ const candidateBase = {
27341
+ sourceMemoryId: options.sourceMemoryId,
27342
+ content,
27343
+ confidence,
27344
+ tags: promotionTags(sourceMemory),
27345
+ memoryKind: "note",
27346
+ lineage: [options.sourceMemoryId]
27347
+ };
27348
+ if (options.dryRun === true) {
27349
+ report.promoted.push({
27350
+ id: `dry-run:${options.sourceMemoryId}`,
27351
+ ...candidateBase
27352
+ });
27353
+ return report;
27354
+ }
27355
+ const id = await storage.writeMemory("rule", content, {
27356
+ confidence,
27357
+ tags: candidateBase.tags,
27358
+ source: "semantic-rule-promotion",
27359
+ lineage: candidateBase.lineage,
27360
+ sourceMemoryId: options.sourceMemoryId,
27361
+ memoryKind: "note",
27362
+ links: buildSupportLinks(options.sourceMemoryId, confidence)
27363
+ });
27364
+ report.promoted.push({
27365
+ id,
27366
+ ...candidateBase
27367
+ });
27368
+ return report;
27369
+ }
27370
+
27109
27371
  // src/cli.ts
27110
27372
  function rankCandidateForKeep(a, b) {
27111
27373
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -27342,6 +27604,23 @@ async function runHarmonicSearchCliCommand(options) {
27342
27604
  anchorsEnabled: options.abstractionAnchorsEnabled
27343
27605
  });
27344
27606
  }
27607
+ async function runVerifiedRecallSearchCliCommand(options) {
27608
+ if (!options.verifiedRecallEnabled) return [];
27609
+ return searchVerifiedEpisodes({
27610
+ memoryDir: options.memoryDir,
27611
+ query: options.query,
27612
+ maxResults: Math.max(1, Math.floor(options.maxResults ?? 3)),
27613
+ boxRecallDays: options.boxRecallDays
27614
+ });
27615
+ }
27616
+ async function runSemanticRulePromoteCliCommand(options) {
27617
+ return promoteSemanticRuleFromMemory({
27618
+ memoryDir: options.memoryDir,
27619
+ enabled: options.semanticRulePromotionEnabled,
27620
+ sourceMemoryId: options.sourceMemoryId,
27621
+ dryRun: options.dryRun
27622
+ });
27623
+ }
27345
27624
  async function runTrustZonePromoteCliCommand(options) {
27346
27625
  const result = await promoteTrustZoneRecord({
27347
27626
  memoryDir: options.memoryDir,
@@ -28587,6 +28866,31 @@ function registerCli(api, orchestrator) {
28587
28866
  console.log(JSON.stringify(result, null, 2));
28588
28867
  console.log("OK");
28589
28868
  });
28869
+ cmd.command("verified-recall-search").description("Preview verified episodic recall over recent memory boxes").argument("<query>", "Prompt-like query to evaluate against verified episodic recall").option("--max-results <count>", "Maximum number of verified episodic results to return", "3").action(async (...args) => {
28870
+ const query = typeof args[0] === "string" ? args[0] : "";
28871
+ const options = args[1] ?? {};
28872
+ const maxResults = typeof options.maxResults === "string" ? Number.parseInt(options.maxResults, 10) : 3;
28873
+ const results = await runVerifiedRecallSearchCliCommand({
28874
+ memoryDir: orchestrator.config.memoryDir,
28875
+ verifiedRecallEnabled: orchestrator.config.verifiedRecallEnabled,
28876
+ query,
28877
+ maxResults: Number.isFinite(maxResults) ? maxResults : 3,
28878
+ boxRecallDays: orchestrator.config.boxRecallDays
28879
+ });
28880
+ console.log(JSON.stringify(results, null, 2));
28881
+ console.log("OK");
28882
+ });
28883
+ cmd.command("semantic-rule-promote").description("Promote an explicit IF/THEN rule from a verified episodic memory").requiredOption("--memory-id <memoryId>", "Verified episodic memory id to promote from").option("--dry-run", "Preview the promoted semantic rule without writing it").action(async (...args) => {
28884
+ const options = args[0] ?? {};
28885
+ const result = await runSemanticRulePromoteCliCommand({
28886
+ memoryDir: orchestrator.config.memoryDir,
28887
+ semanticRulePromotionEnabled: orchestrator.config.semanticRulePromotionEnabled,
28888
+ sourceMemoryId: String(options.memoryId ?? ""),
28889
+ dryRun: options.dryRun === true
28890
+ });
28891
+ console.log(JSON.stringify(result, null, 2));
28892
+ console.log("OK");
28893
+ });
28590
28894
  cmd.command("conversation-index-health").description("Show conversation index backend health and index stats").action(async () => {
28591
28895
  const health = await runConversationIndexHealthCliCommand(orchestrator);
28592
28896
  console.log(JSON.stringify(health, null, 2));