@joshuaswarren/openclaw-engram 9.0.35 → 9.0.37

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
@@ -38,6 +38,7 @@ AI agents forget everything between conversations. Engram fixes that.
38
38
  - **Trust-zone recall** — Engram can now, when `trustZoneRecallEnabled` is enabled, inject prompt-relevant `working` and `trusted` trust-zone records into recall context as a separate `Trust Zones` section while keeping `quarantine` material out of recall by default.
39
39
  - **Poisoning-defense corroboration** — Engram can now, when `memoryPoisoningDefenseEnabled` is enabled, score trust-zone provenance deterministically and require independent non-quarantine corroboration before risky `working -> trusted` promotions succeed.
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
+ - **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.
41
42
  - **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.
42
43
 
43
44
  ## Quick Start
package/dist/index.js CHANGED
@@ -302,6 +302,9 @@ function parseConfig(raw) {
302
302
  trustZoneRecallEnabled: cfg.trustZoneRecallEnabled === true,
303
303
  memoryPoisoningDefenseEnabled: cfg.memoryPoisoningDefenseEnabled === true,
304
304
  memoryRedTeamBenchEnabled: cfg.memoryRedTeamBenchEnabled === true,
305
+ harmonicRetrievalEnabled: cfg.harmonicRetrievalEnabled === true,
306
+ abstractionAnchorsEnabled: cfg.abstractionAnchorsEnabled === true,
307
+ abstractionNodeStoreDir: typeof cfg.abstractionNodeStoreDir === "string" && cfg.abstractionNodeStoreDir.trim().length > 0 ? cfg.abstractionNodeStoreDir.trim() : path.join(memoryDir, "state", "abstraction-nodes"),
305
308
  // Local LLM Provider (v2.1)
306
309
  localLlmEnabled: cfg.localLlmEnabled === true || cfg.localLlmEnabled === "true",
307
310
  // default: false
@@ -24220,7 +24223,7 @@ promotionCandidates: ${res.promotionCandidateCount}`
24220
24223
  }
24221
24224
 
24222
24225
  // src/cli.ts
24223
- import path54 from "path";
24226
+ import path56 from "path";
24224
24227
  import { access as access3, readFile as readFile37, readdir as readdir24, unlink as unlink7 } from "fs/promises";
24225
24228
  import { createHash as createHash10 } from "crypto";
24226
24229
 
@@ -25101,8 +25104,8 @@ function gatherCandidates(input, warnings) {
25101
25104
  const record = rec;
25102
25105
  const content = typeof record.content === "string" ? record.content : null;
25103
25106
  if (!content) continue;
25104
- const path56 = typeof record.path === "string" ? record.path : "";
25105
- if (!path56.startsWith("transcripts/") && !path56.includes("/transcripts/")) continue;
25107
+ const path58 = typeof record.path === "string" ? record.path : "";
25108
+ if (!path58.startsWith("transcripts/") && !path58.includes("/transcripts/")) continue;
25106
25109
  rows.push(...parseJsonl(content, warnings));
25107
25110
  }
25108
25111
  return rows;
@@ -26736,6 +26739,171 @@ async function runCompatChecks(options) {
26736
26739
  };
26737
26740
  }
26738
26741
 
26742
+ // src/abstraction-nodes.ts
26743
+ import path54 from "path";
26744
+ import { mkdir as mkdir36, writeFile as writeFile33 } from "fs/promises";
26745
+ function validateKind2(raw) {
26746
+ const value = assertString2(raw, "kind");
26747
+ if (!["episode", "topic", "project", "workflow", "constraint"].includes(value)) {
26748
+ throw new Error("kind must be one of episode|topic|project|workflow|constraint");
26749
+ }
26750
+ return value;
26751
+ }
26752
+ function validateLevel(raw) {
26753
+ const value = assertString2(raw, "abstractionLevel");
26754
+ if (!["micro", "meso", "macro"].includes(value)) {
26755
+ throw new Error("abstractionLevel must be one of micro|meso|macro");
26756
+ }
26757
+ return value;
26758
+ }
26759
+ function resolveAbstractionNodeStoreDir(memoryDir, overrideDir) {
26760
+ if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
26761
+ return overrideDir.trim();
26762
+ }
26763
+ return path54.join(memoryDir, "state", "abstraction-nodes");
26764
+ }
26765
+ function validateAbstractionNode(raw) {
26766
+ if (!isRecord2(raw)) throw new Error("abstraction node must be an object");
26767
+ if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
26768
+ return {
26769
+ schemaVersion: 1,
26770
+ nodeId: assertSafePathSegment(assertString2(raw.nodeId, "nodeId"), "nodeId"),
26771
+ recordedAt: assertIsoRecordedAt(assertString2(raw.recordedAt, "recordedAt")),
26772
+ sessionKey: assertString2(raw.sessionKey, "sessionKey"),
26773
+ kind: validateKind2(raw.kind),
26774
+ abstractionLevel: validateLevel(raw.abstractionLevel),
26775
+ title: assertString2(raw.title, "title"),
26776
+ summary: assertString2(raw.summary, "summary"),
26777
+ sourceMemoryIds: optionalStringArray2(raw.sourceMemoryIds, "sourceMemoryIds"),
26778
+ entityRefs: optionalStringArray2(raw.entityRefs, "entityRefs"),
26779
+ tags: optionalStringArray2(raw.tags, "tags"),
26780
+ metadata: validateStringRecord(raw.metadata, "metadata")
26781
+ };
26782
+ }
26783
+ async function getAbstractionNodeStoreStatus(options) {
26784
+ const rootDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);
26785
+ const nodesDir = path54.join(rootDir, "nodes");
26786
+ const files = await listJsonFiles(nodesDir);
26787
+ const nodes = [];
26788
+ const invalidNodes = [];
26789
+ for (const filePath of files) {
26790
+ try {
26791
+ nodes.push(validateAbstractionNode(await readJsonFile(filePath)));
26792
+ } catch (error) {
26793
+ invalidNodes.push({
26794
+ path: filePath,
26795
+ error: error instanceof Error ? error.message : String(error)
26796
+ });
26797
+ }
26798
+ }
26799
+ nodes.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
26800
+ const byKind = {};
26801
+ const byLevel = {};
26802
+ for (const node of nodes) {
26803
+ byKind[node.kind] = (byKind[node.kind] ?? 0) + 1;
26804
+ byLevel[node.abstractionLevel] = (byLevel[node.abstractionLevel] ?? 0) + 1;
26805
+ }
26806
+ return {
26807
+ enabled: options.enabled,
26808
+ anchorsEnabled: options.anchorsEnabled,
26809
+ rootDir,
26810
+ nodesDir,
26811
+ nodes: {
26812
+ total: files.length,
26813
+ valid: nodes.length,
26814
+ invalid: invalidNodes.length,
26815
+ byKind,
26816
+ byLevel,
26817
+ latestNodeId: nodes[0]?.nodeId,
26818
+ latestRecordedAt: nodes[0]?.recordedAt,
26819
+ latestSessionKey: nodes[0]?.sessionKey
26820
+ },
26821
+ latestNode: nodes[0],
26822
+ invalidNodes
26823
+ };
26824
+ }
26825
+
26826
+ // src/cue-anchors.ts
26827
+ import path55 from "path";
26828
+ import { mkdir as mkdir37, writeFile as writeFile34 } from "fs/promises";
26829
+ function validateAnchorType(raw) {
26830
+ const value = assertString2(raw, "anchorType");
26831
+ if (!["entity", "file", "tool", "outcome", "constraint", "date"].includes(value)) {
26832
+ throw new Error("anchorType must be one of entity|file|tool|outcome|constraint|date");
26833
+ }
26834
+ return value;
26835
+ }
26836
+ function validateNodeRefs(raw) {
26837
+ const nodeRefs = optionalStringArray2(raw, "nodeRefs");
26838
+ if (!nodeRefs || nodeRefs.length === 0) {
26839
+ throw new Error("nodeRefs must contain at least one node reference");
26840
+ }
26841
+ return nodeRefs.map((nodeRef, index) => assertSafePathSegment(nodeRef, `nodeRefs[${index}]`));
26842
+ }
26843
+ function resolveCueAnchorStoreDir(abstractionNodeStoreDir, overrideDir) {
26844
+ if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
26845
+ return overrideDir.trim();
26846
+ }
26847
+ return path55.join(abstractionNodeStoreDir, "anchors");
26848
+ }
26849
+ function validateCueAnchor(raw) {
26850
+ if (!isRecord2(raw)) throw new Error("cue anchor must be an object");
26851
+ if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
26852
+ return {
26853
+ schemaVersion: 1,
26854
+ anchorId: assertSafePathSegment(assertString2(raw.anchorId, "anchorId"), "anchorId"),
26855
+ anchorType: validateAnchorType(raw.anchorType),
26856
+ anchorValue: assertString2(raw.anchorValue, "anchorValue"),
26857
+ normalizedCue: assertString2(raw.normalizedCue, "normalizedCue"),
26858
+ recordedAt: assertIsoRecordedAt(assertString2(raw.recordedAt, "recordedAt")),
26859
+ sessionKey: assertString2(raw.sessionKey, "sessionKey"),
26860
+ nodeRefs: validateNodeRefs(raw.nodeRefs),
26861
+ tags: optionalStringArray2(raw.tags, "tags"),
26862
+ metadata: validateStringRecord(raw.metadata, "metadata")
26863
+ };
26864
+ }
26865
+ async function getCueAnchorStoreStatus(options) {
26866
+ const abstractionNodeStoreDir = options.abstractionNodeStoreDir?.trim().length ? options.abstractionNodeStoreDir.trim() : path55.join(options.memoryDir, "state", "abstraction-nodes");
26867
+ const rootDir = resolveCueAnchorStoreDir(abstractionNodeStoreDir, options.cueAnchorStoreDir);
26868
+ const files = await listJsonFiles(rootDir);
26869
+ const anchors = [];
26870
+ const invalidAnchors = [];
26871
+ for (const filePath of files) {
26872
+ try {
26873
+ anchors.push(validateCueAnchor(await readJsonFile(filePath)));
26874
+ } catch (error) {
26875
+ invalidAnchors.push({
26876
+ path: filePath,
26877
+ error: error instanceof Error ? error.message : String(error)
26878
+ });
26879
+ }
26880
+ }
26881
+ anchors.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
26882
+ const byType = {};
26883
+ let totalNodeRefs = 0;
26884
+ for (const anchor of anchors) {
26885
+ byType[anchor.anchorType] = (byType[anchor.anchorType] ?? 0) + 1;
26886
+ totalNodeRefs += anchor.nodeRefs.length;
26887
+ }
26888
+ return {
26889
+ enabled: options.enabled,
26890
+ anchorsEnabled: options.anchorsEnabled,
26891
+ rootDir,
26892
+ anchors: {
26893
+ total: files.length,
26894
+ valid: anchors.length,
26895
+ invalid: invalidAnchors.length,
26896
+ byType,
26897
+ totalNodeRefs,
26898
+ latestAnchorId: anchors[0]?.anchorId,
26899
+ latestRecordedAt: anchors[0]?.recordedAt,
26900
+ latestSessionKey: anchors[0]?.sessionKey
26901
+ },
26902
+ latestAnchor: anchors[0],
26903
+ invalidAnchors
26904
+ };
26905
+ }
26906
+
26739
26907
  // src/cli.ts
26740
26908
  function rankCandidateForKeep(a, b) {
26741
26909
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -26945,6 +27113,22 @@ async function runTrustZoneStatusCliCommand(options) {
26945
27113
  poisoningDefenseEnabled: options.memoryPoisoningDefenseEnabled
26946
27114
  });
26947
27115
  }
27116
+ async function runAbstractionNodeStatusCliCommand(options) {
27117
+ return getAbstractionNodeStoreStatus({
27118
+ memoryDir: options.memoryDir,
27119
+ abstractionNodeStoreDir: options.abstractionNodeStoreDir,
27120
+ enabled: options.harmonicRetrievalEnabled,
27121
+ anchorsEnabled: options.abstractionAnchorsEnabled
27122
+ });
27123
+ }
27124
+ async function runCueAnchorStatusCliCommand(options) {
27125
+ return getCueAnchorStoreStatus({
27126
+ memoryDir: options.memoryDir,
27127
+ abstractionNodeStoreDir: options.abstractionNodeStoreDir,
27128
+ enabled: options.harmonicRetrievalEnabled,
27129
+ anchorsEnabled: options.abstractionAnchorsEnabled
27130
+ });
27131
+ }
26948
27132
  async function runTrustZonePromoteCliCommand(options) {
26949
27133
  const result = await promoteTrustZoneRecord({
26950
27134
  memoryDir: options.memoryDir,
@@ -27191,7 +27375,7 @@ function policyVersionForValues(values, config) {
27191
27375
  return createHash10("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
27192
27376
  }
27193
27377
  async function readRuntimePolicySnapshot2(config, fileName) {
27194
- const filePath = path54.join(config.memoryDir, "state", fileName);
27378
+ const filePath = path56.join(config.memoryDir, "state", fileName);
27195
27379
  const snapshot = await readRuntimePolicySnapshot(filePath, {
27196
27380
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
27197
27381
  });
@@ -27775,14 +27959,14 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
27775
27959
  const ns = (namespace ?? "").trim();
27776
27960
  if (!ns) return orchestrator.config.memoryDir;
27777
27961
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
27778
- const candidate = path54.join(orchestrator.config.memoryDir, "namespaces", ns);
27962
+ const candidate = path56.join(orchestrator.config.memoryDir, "namespaces", ns);
27779
27963
  if (ns === orchestrator.config.defaultNamespace) {
27780
27964
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
27781
27965
  }
27782
27966
  return candidate;
27783
27967
  }
27784
27968
  async function readAllMemoryFiles(memoryDir) {
27785
- const roots = [path54.join(memoryDir, "facts"), path54.join(memoryDir, "corrections")];
27969
+ const roots = [path56.join(memoryDir, "facts"), path56.join(memoryDir, "corrections")];
27786
27970
  const out = [];
27787
27971
  const walk = async (dir) => {
27788
27972
  let entries;
@@ -27793,7 +27977,7 @@ async function readAllMemoryFiles(memoryDir) {
27793
27977
  }
27794
27978
  for (const entry of entries) {
27795
27979
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
27796
- const fullPath = path54.join(dir, entryName);
27980
+ const fullPath = path56.join(dir, entryName);
27797
27981
  if (entry.isDirectory()) {
27798
27982
  await walk(fullPath);
27799
27983
  continue;
@@ -28136,6 +28320,26 @@ function registerCli(api, orchestrator) {
28136
28320
  console.log(JSON.stringify(status, null, 2));
28137
28321
  console.log("OK");
28138
28322
  });
28323
+ cmd.command("abstraction-node-status").description("Show abstraction-node store status, abstraction counts, and latest stored node").action(async () => {
28324
+ const status = await runAbstractionNodeStatusCliCommand({
28325
+ memoryDir: orchestrator.config.memoryDir,
28326
+ abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
28327
+ harmonicRetrievalEnabled: orchestrator.config.harmonicRetrievalEnabled,
28328
+ abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled
28329
+ });
28330
+ console.log(JSON.stringify(status, null, 2));
28331
+ console.log("OK");
28332
+ });
28333
+ cmd.command("cue-anchor-status").description("Show cue-anchor index status, anchor counts, and the latest stored cue anchor").action(async () => {
28334
+ const status = await runCueAnchorStatusCliCommand({
28335
+ memoryDir: orchestrator.config.memoryDir,
28336
+ abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
28337
+ harmonicRetrievalEnabled: orchestrator.config.harmonicRetrievalEnabled,
28338
+ abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled
28339
+ });
28340
+ console.log(JSON.stringify(status, null, 2));
28341
+ console.log("OK");
28342
+ });
28139
28343
  cmd.command("trust-zone-promote").description("Dry-run or apply a trust-zone promotion with provenance enforcement").requiredOption("--record-id <recordId>", "Source trust-zone record id").requiredOption("--target-zone <targetZone>", "Promotion target zone (working|trusted)").requiredOption("--reason <reason>", "Human-readable promotion reason").option("--recorded-at <isoTimestamp>", "Promotion timestamp (defaults to now)").option("--summary <summary>", "Optional replacement summary for the promoted record").option("--dry-run", "Show the promotion plan without writing the promoted record").action(async (...args) => {
28140
28344
  const options = args[0] ?? {};
28141
28345
  const result = await runTrustZonePromoteCliCommand({
@@ -28803,7 +29007,7 @@ function registerCli(api, orchestrator) {
28803
29007
  }
28804
29008
  });
28805
29009
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
28806
- const workspaceDir = path54.join(process.env.HOME ?? "~", ".openclaw", "workspace");
29010
+ const workspaceDir = path56.join(process.env.HOME ?? "~", ".openclaw", "workspace");
28807
29011
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
28808
29012
  if (!identity) {
28809
29013
  console.log("No identity file found.");
@@ -29026,8 +29230,8 @@ function registerCli(api, orchestrator) {
29026
29230
  const options = args[0] ?? {};
29027
29231
  const threadId = options.thread;
29028
29232
  const top = parseInt(options.top ?? "10", 10);
29029
- const memoryDir = path54.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
29030
- const threading = new ThreadingManager(path54.join(memoryDir, "threads"));
29233
+ const memoryDir = path56.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
29234
+ const threading = new ThreadingManager(path56.join(memoryDir, "threads"));
29031
29235
  if (threadId) {
29032
29236
  const thread = await threading.loadThread(threadId);
29033
29237
  if (!thread) {
@@ -29503,9 +29707,9 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
29503
29707
  }
29504
29708
 
29505
29709
  // src/index.ts
29506
- import { readFile as readFile38, writeFile as writeFile33 } from "fs/promises";
29710
+ import { readFile as readFile38, writeFile as writeFile35 } from "fs/promises";
29507
29711
  import { readFileSync as readFileSync4 } from "fs";
29508
- import path55 from "path";
29712
+ import path57 from "path";
29509
29713
  import os6 from "os";
29510
29714
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
29511
29715
  var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
@@ -29513,7 +29717,7 @@ function loadPluginConfigFromFile() {
29513
29717
  try {
29514
29718
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
29515
29719
  const homeDir = process.env.HOME ?? os6.homedir();
29516
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path55.join(homeDir, ".openclaw", "openclaw.json");
29720
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path57.join(homeDir, ".openclaw", "openclaw.json");
29517
29721
  const content = readFileSync4(configPath, "utf-8");
29518
29722
  const config = JSON.parse(content);
29519
29723
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -29763,11 +29967,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29763
29967
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
29764
29968
  );
29765
29969
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
29766
- const signalPath = path55.join(
29970
+ const signalPath = path57.join(
29767
29971
  workspaceDir,
29768
29972
  `.compaction-reset-signal-${safeSessionKey}`
29769
29973
  );
29770
- await writeFile33(
29974
+ await writeFile35(
29771
29975
  signalPath,
29772
29976
  JSON.stringify({
29773
29977
  sessionKey,
@@ -29794,7 +29998,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29794
29998
  );
29795
29999
  async function ensureHourlySummaryCron(api2) {
29796
30000
  const jobId = "engram-hourly-summary";
29797
- const cronFilePath = path55.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
30001
+ const cronFilePath = path57.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
29798
30002
  try {
29799
30003
  let jobsData = { version: 1, jobs: [] };
29800
30004
  try {
@@ -29835,7 +30039,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29835
30039
  state: {}
29836
30040
  };
29837
30041
  jobsData.jobs.push(newJob);
29838
- await writeFile33(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
30042
+ await writeFile35(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
29839
30043
  log.info("auto-registered hourly summary cron job");
29840
30044
  } catch (err) {
29841
30045
  log.error("failed to auto-register hourly summary cron job:", err);