@joshuaswarren/openclaw-engram 9.0.36 → 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,7 +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
+ - **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
  - **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.
43
43
 
44
44
  ## Quick Start
package/dist/index.js CHANGED
@@ -24223,7 +24223,7 @@ promotionCandidates: ${res.promotionCandidateCount}`
24223
24223
  }
24224
24224
 
24225
24225
  // src/cli.ts
24226
- import path55 from "path";
24226
+ import path56 from "path";
24227
24227
  import { access as access3, readFile as readFile37, readdir as readdir24, unlink as unlink7 } from "fs/promises";
24228
24228
  import { createHash as createHash10 } from "crypto";
24229
24229
 
@@ -25104,8 +25104,8 @@ function gatherCandidates(input, warnings) {
25104
25104
  const record = rec;
25105
25105
  const content = typeof record.content === "string" ? record.content : null;
25106
25106
  if (!content) continue;
25107
- const path57 = typeof record.path === "string" ? record.path : "";
25108
- if (!path57.startsWith("transcripts/") && !path57.includes("/transcripts/")) continue;
25107
+ const path58 = typeof record.path === "string" ? record.path : "";
25108
+ if (!path58.startsWith("transcripts/") && !path58.includes("/transcripts/")) continue;
25109
25109
  rows.push(...parseJsonl(content, warnings));
25110
25110
  }
25111
25111
  return rows;
@@ -26823,6 +26823,87 @@ async function getAbstractionNodeStoreStatus(options) {
26823
26823
  };
26824
26824
  }
26825
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
+
26826
26907
  // src/cli.ts
26827
26908
  function rankCandidateForKeep(a, b) {
26828
26909
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -27040,6 +27121,14 @@ async function runAbstractionNodeStatusCliCommand(options) {
27040
27121
  anchorsEnabled: options.abstractionAnchorsEnabled
27041
27122
  });
27042
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
+ }
27043
27132
  async function runTrustZonePromoteCliCommand(options) {
27044
27133
  const result = await promoteTrustZoneRecord({
27045
27134
  memoryDir: options.memoryDir,
@@ -27286,7 +27375,7 @@ function policyVersionForValues(values, config) {
27286
27375
  return createHash10("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
27287
27376
  }
27288
27377
  async function readRuntimePolicySnapshot2(config, fileName) {
27289
- const filePath = path55.join(config.memoryDir, "state", fileName);
27378
+ const filePath = path56.join(config.memoryDir, "state", fileName);
27290
27379
  const snapshot = await readRuntimePolicySnapshot(filePath, {
27291
27380
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
27292
27381
  });
@@ -27870,14 +27959,14 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
27870
27959
  const ns = (namespace ?? "").trim();
27871
27960
  if (!ns) return orchestrator.config.memoryDir;
27872
27961
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
27873
- const candidate = path55.join(orchestrator.config.memoryDir, "namespaces", ns);
27962
+ const candidate = path56.join(orchestrator.config.memoryDir, "namespaces", ns);
27874
27963
  if (ns === orchestrator.config.defaultNamespace) {
27875
27964
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
27876
27965
  }
27877
27966
  return candidate;
27878
27967
  }
27879
27968
  async function readAllMemoryFiles(memoryDir) {
27880
- const roots = [path55.join(memoryDir, "facts"), path55.join(memoryDir, "corrections")];
27969
+ const roots = [path56.join(memoryDir, "facts"), path56.join(memoryDir, "corrections")];
27881
27970
  const out = [];
27882
27971
  const walk = async (dir) => {
27883
27972
  let entries;
@@ -27888,7 +27977,7 @@ async function readAllMemoryFiles(memoryDir) {
27888
27977
  }
27889
27978
  for (const entry of entries) {
27890
27979
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
27891
- const fullPath = path55.join(dir, entryName);
27980
+ const fullPath = path56.join(dir, entryName);
27892
27981
  if (entry.isDirectory()) {
27893
27982
  await walk(fullPath);
27894
27983
  continue;
@@ -28241,6 +28330,16 @@ function registerCli(api, orchestrator) {
28241
28330
  console.log(JSON.stringify(status, null, 2));
28242
28331
  console.log("OK");
28243
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
+ });
28244
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) => {
28245
28344
  const options = args[0] ?? {};
28246
28345
  const result = await runTrustZonePromoteCliCommand({
@@ -28908,7 +29007,7 @@ function registerCli(api, orchestrator) {
28908
29007
  }
28909
29008
  });
28910
29009
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
28911
- const workspaceDir = path55.join(process.env.HOME ?? "~", ".openclaw", "workspace");
29010
+ const workspaceDir = path56.join(process.env.HOME ?? "~", ".openclaw", "workspace");
28912
29011
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
28913
29012
  if (!identity) {
28914
29013
  console.log("No identity file found.");
@@ -29131,8 +29230,8 @@ function registerCli(api, orchestrator) {
29131
29230
  const options = args[0] ?? {};
29132
29231
  const threadId = options.thread;
29133
29232
  const top = parseInt(options.top ?? "10", 10);
29134
- const memoryDir = path55.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
29135
- const threading = new ThreadingManager(path55.join(memoryDir, "threads"));
29233
+ const memoryDir = path56.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
29234
+ const threading = new ThreadingManager(path56.join(memoryDir, "threads"));
29136
29235
  if (threadId) {
29137
29236
  const thread = await threading.loadThread(threadId);
29138
29237
  if (!thread) {
@@ -29608,9 +29707,9 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
29608
29707
  }
29609
29708
 
29610
29709
  // src/index.ts
29611
- import { readFile as readFile38, writeFile as writeFile34 } from "fs/promises";
29710
+ import { readFile as readFile38, writeFile as writeFile35 } from "fs/promises";
29612
29711
  import { readFileSync as readFileSync4 } from "fs";
29613
- import path56 from "path";
29712
+ import path57 from "path";
29614
29713
  import os6 from "os";
29615
29714
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
29616
29715
  var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
@@ -29618,7 +29717,7 @@ function loadPluginConfigFromFile() {
29618
29717
  try {
29619
29718
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
29620
29719
  const homeDir = process.env.HOME ?? os6.homedir();
29621
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path56.join(homeDir, ".openclaw", "openclaw.json");
29720
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path57.join(homeDir, ".openclaw", "openclaw.json");
29622
29721
  const content = readFileSync4(configPath, "utf-8");
29623
29722
  const config = JSON.parse(content);
29624
29723
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -29868,11 +29967,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29868
29967
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
29869
29968
  );
29870
29969
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
29871
- const signalPath = path56.join(
29970
+ const signalPath = path57.join(
29872
29971
  workspaceDir,
29873
29972
  `.compaction-reset-signal-${safeSessionKey}`
29874
29973
  );
29875
- await writeFile34(
29974
+ await writeFile35(
29876
29975
  signalPath,
29877
29976
  JSON.stringify({
29878
29977
  sessionKey,
@@ -29899,7 +29998,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29899
29998
  );
29900
29999
  async function ensureHourlySummaryCron(api2) {
29901
30000
  const jobId = "engram-hourly-summary";
29902
- const cronFilePath = path56.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
30001
+ const cronFilePath = path57.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
29903
30002
  try {
29904
30003
  let jobsData = { version: 1, jobs: [] };
29905
30004
  try {
@@ -29940,7 +30039,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
29940
30039
  state: {}
29941
30040
  };
29942
30041
  jobsData.jobs.push(newJob);
29943
- await writeFile34(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
30042
+ await writeFile35(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
29944
30043
  log.info("auto-registered hourly summary cron job");
29945
30044
  } catch (err) {
29946
30045
  log.error("failed to auto-register hourly summary cron job:", err);