@joshuaswarren/openclaw-engram 9.0.35 → 9.0.36

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
+ - **Abstraction-node foundation** — Engram can now, when `harmonicRetrievalEnabled` is enabled, persist typed abstraction nodes for later harmonic retrieval slices, with `abstractionAnchorsEnabled` reserved for the future cue-anchor layer.
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 path55 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 path57 = typeof record.path === "string" ? record.path : "";
25108
+ if (!path57.startsWith("transcripts/") && !path57.includes("/transcripts/")) continue;
25106
25109
  rows.push(...parseJsonl(content, warnings));
25107
25110
  }
25108
25111
  return rows;
@@ -26736,6 +26739,90 @@ 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
+
26739
26826
  // src/cli.ts
26740
26827
  function rankCandidateForKeep(a, b) {
26741
26828
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -26945,6 +27032,14 @@ async function runTrustZoneStatusCliCommand(options) {
26945
27032
  poisoningDefenseEnabled: options.memoryPoisoningDefenseEnabled
26946
27033
  });
26947
27034
  }
27035
+ async function runAbstractionNodeStatusCliCommand(options) {
27036
+ return getAbstractionNodeStoreStatus({
27037
+ memoryDir: options.memoryDir,
27038
+ abstractionNodeStoreDir: options.abstractionNodeStoreDir,
27039
+ enabled: options.harmonicRetrievalEnabled,
27040
+ anchorsEnabled: options.abstractionAnchorsEnabled
27041
+ });
27042
+ }
26948
27043
  async function runTrustZonePromoteCliCommand(options) {
26949
27044
  const result = await promoteTrustZoneRecord({
26950
27045
  memoryDir: options.memoryDir,
@@ -27191,7 +27286,7 @@ function policyVersionForValues(values, config) {
27191
27286
  return createHash10("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
27192
27287
  }
27193
27288
  async function readRuntimePolicySnapshot2(config, fileName) {
27194
- const filePath = path54.join(config.memoryDir, "state", fileName);
27289
+ const filePath = path55.join(config.memoryDir, "state", fileName);
27195
27290
  const snapshot = await readRuntimePolicySnapshot(filePath, {
27196
27291
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
27197
27292
  });
@@ -27775,14 +27870,14 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
27775
27870
  const ns = (namespace ?? "").trim();
27776
27871
  if (!ns) return orchestrator.config.memoryDir;
27777
27872
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
27778
- const candidate = path54.join(orchestrator.config.memoryDir, "namespaces", ns);
27873
+ const candidate = path55.join(orchestrator.config.memoryDir, "namespaces", ns);
27779
27874
  if (ns === orchestrator.config.defaultNamespace) {
27780
27875
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
27781
27876
  }
27782
27877
  return candidate;
27783
27878
  }
27784
27879
  async function readAllMemoryFiles(memoryDir) {
27785
- const roots = [path54.join(memoryDir, "facts"), path54.join(memoryDir, "corrections")];
27880
+ const roots = [path55.join(memoryDir, "facts"), path55.join(memoryDir, "corrections")];
27786
27881
  const out = [];
27787
27882
  const walk = async (dir) => {
27788
27883
  let entries;
@@ -27793,7 +27888,7 @@ async function readAllMemoryFiles(memoryDir) {
27793
27888
  }
27794
27889
  for (const entry of entries) {
27795
27890
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
27796
- const fullPath = path54.join(dir, entryName);
27891
+ const fullPath = path55.join(dir, entryName);
27797
27892
  if (entry.isDirectory()) {
27798
27893
  await walk(fullPath);
27799
27894
  continue;
@@ -28136,6 +28231,16 @@ function registerCli(api, orchestrator) {
28136
28231
  console.log(JSON.stringify(status, null, 2));
28137
28232
  console.log("OK");
28138
28233
  });
28234
+ cmd.command("abstraction-node-status").description("Show abstraction-node store status, abstraction counts, and latest stored node").action(async () => {
28235
+ const status = await runAbstractionNodeStatusCliCommand({
28236
+ memoryDir: orchestrator.config.memoryDir,
28237
+ abstractionNodeStoreDir: orchestrator.config.abstractionNodeStoreDir,
28238
+ harmonicRetrievalEnabled: orchestrator.config.harmonicRetrievalEnabled,
28239
+ abstractionAnchorsEnabled: orchestrator.config.abstractionAnchorsEnabled
28240
+ });
28241
+ console.log(JSON.stringify(status, null, 2));
28242
+ console.log("OK");
28243
+ });
28139
28244
  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
28245
  const options = args[0] ?? {};
28141
28246
  const result = await runTrustZonePromoteCliCommand({
@@ -28803,7 +28908,7 @@ function registerCli(api, orchestrator) {
28803
28908
  }
28804
28909
  });
28805
28910
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
28806
- const workspaceDir = path54.join(process.env.HOME ?? "~", ".openclaw", "workspace");
28911
+ const workspaceDir = path55.join(process.env.HOME ?? "~", ".openclaw", "workspace");
28807
28912
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
28808
28913
  if (!identity) {
28809
28914
  console.log("No identity file found.");
@@ -29026,8 +29131,8 @@ function registerCli(api, orchestrator) {
29026
29131
  const options = args[0] ?? {};
29027
29132
  const threadId = options.thread;
29028
29133
  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"));
29134
+ const memoryDir = path55.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
29135
+ const threading = new ThreadingManager(path55.join(memoryDir, "threads"));
29031
29136
  if (threadId) {
29032
29137
  const thread = await threading.loadThread(threadId);
29033
29138
  if (!thread) {
@@ -29503,9 +29608,9 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
29503
29608
  }
29504
29609
 
29505
29610
  // src/index.ts
29506
- import { readFile as readFile38, writeFile as writeFile33 } from "fs/promises";
29611
+ import { readFile as readFile38, writeFile as writeFile34 } from "fs/promises";
29507
29612
  import { readFileSync as readFileSync4 } from "fs";
29508
- import path55 from "path";
29613
+ import path56 from "path";
29509
29614
  import os6 from "os";
29510
29615
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
29511
29616
  var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
@@ -29513,7 +29618,7 @@ function loadPluginConfigFromFile() {
29513
29618
  try {
29514
29619
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
29515
29620
  const homeDir = process.env.HOME ?? os6.homedir();
29516
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path55.join(homeDir, ".openclaw", "openclaw.json");
29621
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path56.join(homeDir, ".openclaw", "openclaw.json");
29517
29622
  const content = readFileSync4(configPath, "utf-8");
29518
29623
  const config = JSON.parse(content);
29519
29624
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -29763,11 +29868,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29763
29868
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
29764
29869
  );
29765
29870
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
29766
- const signalPath = path55.join(
29871
+ const signalPath = path56.join(
29767
29872
  workspaceDir,
29768
29873
  `.compaction-reset-signal-${safeSessionKey}`
29769
29874
  );
29770
- await writeFile33(
29875
+ await writeFile34(
29771
29876
  signalPath,
29772
29877
  JSON.stringify({
29773
29878
  sessionKey,
@@ -29794,7 +29899,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29794
29899
  );
29795
29900
  async function ensureHourlySummaryCron(api2) {
29796
29901
  const jobId = "engram-hourly-summary";
29797
- const cronFilePath = path55.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
29902
+ const cronFilePath = path56.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
29798
29903
  try {
29799
29904
  let jobsData = { version: 1, jobs: [] };
29800
29905
  try {
@@ -29835,7 +29940,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29835
29940
  state: {}
29836
29941
  };
29837
29942
  jobsData.jobs.push(newJob);
29838
- await writeFile33(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
29943
+ await writeFile34(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
29839
29944
  log.info("auto-registered hourly summary cron job");
29840
29945
  } catch (err) {
29841
29946
  log.error("failed to auto-register hourly summary cron job:", err);