@joshuaswarren/openclaw-engram 9.0.41 → 9.0.42

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
@@ -43,6 +43,7 @@ AI agents forget everything between conversations. Engram fixes that.
43
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
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.
45
45
  - **Verified rule recall** — Engram can now, when `semanticRuleVerificationEnabled` is enabled, inject a dedicated `Verified Rules` recall section that re-checks promoted rule memories against their cited source episodes at recall time and downgrades stale provenance before the rule can surface.
46
+ - **Creation-memory ledger** — Engram can now, when `creationMemoryEnabled` is enabled, persist a typed work-product ledger for explicit outputs agents create or update, inspect it with `openclaw engram work-product-status`, and write deterministic entries through `openclaw engram work-product-record`.
46
47
  - **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.
47
48
 
48
49
  ## Quick Start
@@ -172,6 +173,8 @@ openclaw engram trust-zone-status # Trust-zone record counts and lates
172
173
  openclaw engram trust-zone-promote # Dry-run or apply a trust-zone promotion with provenance/corroboration enforcement
173
174
  openclaw engram harmonic-search <query> # Preview blended harmonic retrieval matches
174
175
  openclaw engram verified-recall-search <query> # Preview verified episodic recall matches
176
+ openclaw engram work-product-status # Work-product ledger counts and latest recorded output
177
+ openclaw engram work-product-record # Record a typed work-product ledger entry
175
178
  openclaw engram conversation-index-health # Conversation index status
176
179
  openclaw engram graph-health # Entity graph status
177
180
  openclaw engram tier-status # Hot/cold tier metrics
@@ -213,6 +216,8 @@ Key settings:
213
216
  | `verifiedRecallEnabled` | `false` | Inject prompt-relevant memory boxes only when their cited source memories verify as non-archived episodes |
214
217
  | `semanticRulePromotionEnabled` | `false` | Enable deterministic promotion of explicit `IF ... THEN ...` rules from verified episodic memories via `openclaw engram semantic-rule-promote` |
215
218
  | `semanticRuleVerificationEnabled` | `false` | Verify promoted semantic rules against their cited source episodes at recall time and inject a dedicated `Verified Rules` section via `openclaw engram semantic-rule-verify` |
219
+ | `creationMemoryEnabled` | `false` | Enable the creation-memory foundation, including the work-product ledger and operator-facing write/status commands |
220
+ | `workProductLedgerDir` | `{memoryDir}/state/work-product-ledger` | Root directory for typed work-product ledger entries |
216
221
 
217
222
  Full reference: [Config Reference](docs/config-reference.md)
218
223
 
package/dist/index.js CHANGED
@@ -307,6 +307,8 @@ function parseConfig(raw) {
307
307
  verifiedRecallEnabled: cfg.verifiedRecallEnabled === true,
308
308
  semanticRulePromotionEnabled: cfg.semanticRulePromotionEnabled === true,
309
309
  semanticRuleVerificationEnabled: cfg.semanticRuleVerificationEnabled === true,
310
+ creationMemoryEnabled: cfg.creationMemoryEnabled === true,
311
+ workProductLedgerDir: typeof cfg.workProductLedgerDir === "string" && cfg.workProductLedgerDir.trim().length > 0 ? cfg.workProductLedgerDir.trim() : path.join(memoryDir, "state", "work-product-ledger"),
310
312
  abstractionNodeStoreDir: typeof cfg.abstractionNodeStoreDir === "string" && cfg.abstractionNodeStoreDir.trim().length > 0 ? cfg.abstractionNodeStoreDir.trim() : path.join(memoryDir, "state", "abstraction-nodes"),
311
313
  // Local LLM Provider (v2.1)
312
314
  localLlmEnabled: cfg.localLlmEnabled === true || cfg.localLlmEnabled === "true",
@@ -24878,7 +24880,7 @@ promotionCandidates: ${res.promotionCandidateCount}`
24878
24880
  }
24879
24881
 
24880
24882
  // src/cli.ts
24881
- import path57 from "path";
24883
+ import path58 from "path";
24882
24884
  import { access as access3, readFile as readFile37, readdir as readdir24, unlink as unlink7 } from "fs/promises";
24883
24885
  import { createHash as createHash10 } from "crypto";
24884
24886
 
@@ -25759,8 +25761,8 @@ function gatherCandidates(input, warnings) {
25759
25761
  const record = rec;
25760
25762
  const content = typeof record.content === "string" ? record.content : null;
25761
25763
  if (!content) continue;
25762
- const path59 = typeof record.path === "string" ? record.path : "";
25763
- if (!path59.startsWith("transcripts/") && !path59.includes("/transcripts/")) continue;
25764
+ const path60 = typeof record.path === "string" ? record.path : "";
25765
+ if (!path60.startsWith("transcripts/") && !path60.includes("/transcripts/")) continue;
25764
25766
  rows.push(...parseJsonl(content, warnings));
25765
25767
  }
25766
25768
  return rows;
@@ -27394,6 +27396,104 @@ async function runCompatChecks(options) {
27394
27396
  };
27395
27397
  }
27396
27398
 
27399
+ // src/work-product-ledger.ts
27400
+ import path57 from "path";
27401
+ import { mkdir as mkdir38, writeFile as writeFile35 } from "fs/promises";
27402
+ function resolveWorkProductLedgerDir(memoryDir, overrideDir) {
27403
+ if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
27404
+ return overrideDir.trim();
27405
+ }
27406
+ return path57.join(memoryDir, "state", "work-product-ledger");
27407
+ }
27408
+ function validateWorkProductLedgerEntry(raw) {
27409
+ if (!isRecord2(raw)) throw new Error("work-product ledger entry must be an object");
27410
+ if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
27411
+ const source = assertString2(raw.source, "source");
27412
+ if (!["tool_result", "cli", "system", "manual"].includes(source)) {
27413
+ throw new Error("source must be one of tool_result|cli|system|manual");
27414
+ }
27415
+ const kind = assertString2(raw.kind, "kind");
27416
+ if (!["artifact", "file", "record", "report", "workspace"].includes(kind)) {
27417
+ throw new Error("kind must be one of artifact|file|record|report|workspace");
27418
+ }
27419
+ const action = assertString2(raw.action, "action");
27420
+ if (!["created", "updated", "deleted", "referenced", "published"].includes(action)) {
27421
+ throw new Error("action must be one of created|updated|deleted|referenced|published");
27422
+ }
27423
+ return {
27424
+ schemaVersion: 1,
27425
+ entryId: assertSafePathSegment(assertString2(raw.entryId, "entryId"), "entryId"),
27426
+ recordedAt: assertIsoRecordedAt(assertString2(raw.recordedAt, "recordedAt")),
27427
+ sessionKey: assertString2(raw.sessionKey, "sessionKey"),
27428
+ source,
27429
+ kind,
27430
+ action,
27431
+ scope: assertString2(raw.scope, "scope"),
27432
+ summary: assertString2(raw.summary, "summary"),
27433
+ artifactPath: optionalString(raw.artifactPath),
27434
+ objectiveStateSnapshotRefs: optionalStringArray2(raw.objectiveStateSnapshotRefs, "objectiveStateSnapshotRefs"),
27435
+ entityRefs: optionalStringArray2(raw.entityRefs, "entityRefs"),
27436
+ tags: optionalStringArray2(raw.tags, "tags"),
27437
+ metadata: validateStringRecord(raw.metadata, "metadata")
27438
+ };
27439
+ }
27440
+ async function recordWorkProductLedgerEntry(options) {
27441
+ const rootDir = resolveWorkProductLedgerDir(options.memoryDir, options.workProductLedgerDir);
27442
+ const validated = validateWorkProductLedgerEntry(options.entry);
27443
+ const day = recordStoreDay(validated.recordedAt);
27444
+ const entriesDir = path57.join(rootDir, "entries", day);
27445
+ const filePath = path57.join(entriesDir, `${validated.entryId}.json`);
27446
+ await mkdir38(entriesDir, { recursive: true });
27447
+ await writeFile35(filePath, JSON.stringify(validated, null, 2), "utf8");
27448
+ return filePath;
27449
+ }
27450
+ async function readWorkProductLedgerEntries(options) {
27451
+ const rootDir = resolveWorkProductLedgerDir(options.memoryDir, options.workProductLedgerDir);
27452
+ const files = await listJsonFiles(path57.join(rootDir, "entries"));
27453
+ const entries = [];
27454
+ const invalidEntries = [];
27455
+ for (const filePath of files) {
27456
+ try {
27457
+ entries.push(validateWorkProductLedgerEntry(await readJsonFile(filePath)));
27458
+ } catch (error) {
27459
+ invalidEntries.push({
27460
+ path: filePath,
27461
+ error: error instanceof Error ? error.message : String(error)
27462
+ });
27463
+ }
27464
+ }
27465
+ return { files, entries, invalidEntries };
27466
+ }
27467
+ async function getWorkProductLedgerStatus(options) {
27468
+ const rootDir = resolveWorkProductLedgerDir(options.memoryDir, options.workProductLedgerDir);
27469
+ const entriesDir = path57.join(rootDir, "entries");
27470
+ const { files, entries, invalidEntries } = await readWorkProductLedgerEntries(options);
27471
+ entries.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
27472
+ const byKind = {};
27473
+ const byAction = {};
27474
+ for (const entry of entries) {
27475
+ byKind[entry.kind] = (byKind[entry.kind] ?? 0) + 1;
27476
+ byAction[entry.action] = (byAction[entry.action] ?? 0) + 1;
27477
+ }
27478
+ return {
27479
+ enabled: options.enabled,
27480
+ rootDir,
27481
+ entriesDir,
27482
+ entries: {
27483
+ total: files.length,
27484
+ valid: entries.length,
27485
+ invalid: invalidEntries.length,
27486
+ byKind,
27487
+ byAction,
27488
+ latestEntryId: entries[0]?.entryId,
27489
+ latestRecordedAt: entries[0]?.recordedAt,
27490
+ latestSessionKey: entries[0]?.sessionKey
27491
+ },
27492
+ latestEntry: entries[0],
27493
+ invalidEntries
27494
+ };
27495
+ }
27496
+
27397
27497
  // src/semantic-rule-promotion.ts
27398
27498
  function normalizeRuleWhitespace(value) {
27399
27499
  return value.replace(/\s+/g, " ").trim();
@@ -27775,6 +27875,21 @@ async function runSemanticRuleVerifyCliCommand(options) {
27775
27875
  maxResults: Math.max(1, Math.floor(options.maxResults ?? 3))
27776
27876
  });
27777
27877
  }
27878
+ async function runWorkProductStatusCliCommand(options) {
27879
+ return getWorkProductLedgerStatus({
27880
+ memoryDir: options.memoryDir,
27881
+ workProductLedgerDir: options.workProductLedgerDir,
27882
+ enabled: options.creationMemoryEnabled
27883
+ });
27884
+ }
27885
+ async function runWorkProductRecordCliCommand(options) {
27886
+ if (!options.creationMemoryEnabled) return null;
27887
+ return recordWorkProductLedgerEntry({
27888
+ memoryDir: options.memoryDir,
27889
+ workProductLedgerDir: options.workProductLedgerDir,
27890
+ entry: options.entry
27891
+ });
27892
+ }
27778
27893
  async function runTrustZonePromoteCliCommand(options) {
27779
27894
  const result = await promoteTrustZoneRecord({
27780
27895
  memoryDir: options.memoryDir,
@@ -28021,7 +28136,7 @@ function policyVersionForValues(values, config) {
28021
28136
  return createHash10("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
28022
28137
  }
28023
28138
  async function readRuntimePolicySnapshot2(config, fileName) {
28024
- const filePath = path57.join(config.memoryDir, "state", fileName);
28139
+ const filePath = path58.join(config.memoryDir, "state", fileName);
28025
28140
  const snapshot = await readRuntimePolicySnapshot(filePath, {
28026
28141
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
28027
28142
  });
@@ -28605,14 +28720,14 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
28605
28720
  const ns = (namespace ?? "").trim();
28606
28721
  if (!ns) return orchestrator.config.memoryDir;
28607
28722
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
28608
- const candidate = path57.join(orchestrator.config.memoryDir, "namespaces", ns);
28723
+ const candidate = path58.join(orchestrator.config.memoryDir, "namespaces", ns);
28609
28724
  if (ns === orchestrator.config.defaultNamespace) {
28610
28725
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
28611
28726
  }
28612
28727
  return candidate;
28613
28728
  }
28614
28729
  async function readAllMemoryFiles(memoryDir) {
28615
- const roots = [path57.join(memoryDir, "facts"), path57.join(memoryDir, "corrections")];
28730
+ const roots = [path58.join(memoryDir, "facts"), path58.join(memoryDir, "corrections")];
28616
28731
  const out = [];
28617
28732
  const walk = async (dir) => {
28618
28733
  let entries;
@@ -28623,7 +28738,7 @@ async function readAllMemoryFiles(memoryDir) {
28623
28738
  }
28624
28739
  for (const entry of entries) {
28625
28740
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
28626
- const fullPath = path57.join(dir, entryName);
28741
+ const fullPath = path58.join(dir, entryName);
28627
28742
  if (entry.isDirectory()) {
28628
28743
  await walk(fullPath);
28629
28744
  continue;
@@ -29002,6 +29117,46 @@ function registerCli(api, orchestrator) {
29002
29117
  console.log(JSON.stringify(results, null, 2));
29003
29118
  console.log("OK");
29004
29119
  });
29120
+ cmd.command("work-product-status").description("Show work-product ledger status, entry counts, and the latest recorded work product").action(async () => {
29121
+ const status = await runWorkProductStatusCliCommand({
29122
+ memoryDir: orchestrator.config.memoryDir,
29123
+ workProductLedgerDir: orchestrator.config.workProductLedgerDir,
29124
+ creationMemoryEnabled: orchestrator.config.creationMemoryEnabled
29125
+ });
29126
+ console.log(JSON.stringify(status, null, 2));
29127
+ console.log("OK");
29128
+ });
29129
+ cmd.command("work-product-record").description("Record a work-product ledger entry when creation-memory is enabled").requiredOption("--entry-id <entryId>", "Ledger entry id").requiredOption("--recorded-at <recordedAt>", "ISO timestamp for the entry").requiredOption("--session-key <sessionKey>", "Session key that created the work product").requiredOption("--source <source>", "Entry source (tool_result|cli|system|manual)").requiredOption("--kind <kind>", "Entry kind (artifact|file|record|report|workspace)").requiredOption(
29130
+ "--entry-action <entryAction>",
29131
+ "Entry action (created|updated|deleted|referenced|published)"
29132
+ ).requiredOption("--scope <scope>", "Primary scope or identifier for the created work product").requiredOption("--summary <summary>", "Human-readable summary of the work product").option("--artifact-path <artifactPath>", "Optional path to the created artifact").option("--tag <tag...>", "Tags to attach to the work-product entry").option("--entity-ref <entityRef...>", "Entity refs to attach to the work-product entry").option(
29133
+ "--objective-state-snapshot-ref <objectiveStateSnapshotRef...>",
29134
+ "Objective-state snapshot refs to link to this work product"
29135
+ ).action(async (...args) => {
29136
+ const options = args[0] ?? {};
29137
+ const filePath = await runWorkProductRecordCliCommand({
29138
+ memoryDir: orchestrator.config.memoryDir,
29139
+ workProductLedgerDir: orchestrator.config.workProductLedgerDir,
29140
+ creationMemoryEnabled: orchestrator.config.creationMemoryEnabled,
29141
+ entry: {
29142
+ schemaVersion: 1,
29143
+ entryId: String(options.entryId ?? ""),
29144
+ recordedAt: String(options.recordedAt ?? ""),
29145
+ sessionKey: String(options.sessionKey ?? ""),
29146
+ source: String(options.source ?? ""),
29147
+ kind: String(options.kind ?? ""),
29148
+ action: String(options.entryAction ?? ""),
29149
+ scope: String(options.scope ?? ""),
29150
+ summary: String(options.summary ?? ""),
29151
+ artifactPath: typeof options.artifactPath === "string" ? options.artifactPath : void 0,
29152
+ tags: Array.isArray(options.tag) ? options.tag.map(String) : void 0,
29153
+ entityRefs: Array.isArray(options.entityRef) ? options.entityRef.map(String) : void 0,
29154
+ objectiveStateSnapshotRefs: Array.isArray(options.objectiveStateSnapshotRef) ? options.objectiveStateSnapshotRef.map(String) : void 0
29155
+ }
29156
+ });
29157
+ console.log(JSON.stringify({ wrote: filePath !== null, filePath }, null, 2));
29158
+ console.log("OK");
29159
+ });
29005
29160
  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) => {
29006
29161
  const options = args[0] ?? {};
29007
29162
  const result = await runTrustZonePromoteCliCommand({
@@ -29707,7 +29862,7 @@ function registerCli(api, orchestrator) {
29707
29862
  }
29708
29863
  });
29709
29864
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
29710
- const workspaceDir = path57.join(process.env.HOME ?? "~", ".openclaw", "workspace");
29865
+ const workspaceDir = path58.join(process.env.HOME ?? "~", ".openclaw", "workspace");
29711
29866
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
29712
29867
  if (!identity) {
29713
29868
  console.log("No identity file found.");
@@ -29930,8 +30085,8 @@ function registerCli(api, orchestrator) {
29930
30085
  const options = args[0] ?? {};
29931
30086
  const threadId = options.thread;
29932
30087
  const top = parseInt(options.top ?? "10", 10);
29933
- const memoryDir = path57.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
29934
- const threading = new ThreadingManager(path57.join(memoryDir, "threads"));
30088
+ const memoryDir = path58.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
30089
+ const threading = new ThreadingManager(path58.join(memoryDir, "threads"));
29935
30090
  if (threadId) {
29936
30091
  const thread = await threading.loadThread(threadId);
29937
30092
  if (!thread) {
@@ -30407,9 +30562,9 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
30407
30562
  }
30408
30563
 
30409
30564
  // src/index.ts
30410
- import { readFile as readFile38, writeFile as writeFile35 } from "fs/promises";
30565
+ import { readFile as readFile38, writeFile as writeFile36 } from "fs/promises";
30411
30566
  import { readFileSync as readFileSync4 } from "fs";
30412
- import path58 from "path";
30567
+ import path59 from "path";
30413
30568
  import os6 from "os";
30414
30569
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
30415
30570
  var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
@@ -30417,7 +30572,7 @@ function loadPluginConfigFromFile() {
30417
30572
  try {
30418
30573
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
30419
30574
  const homeDir = process.env.HOME ?? os6.homedir();
30420
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path58.join(homeDir, ".openclaw", "openclaw.json");
30575
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path59.join(homeDir, ".openclaw", "openclaw.json");
30421
30576
  const content = readFileSync4(configPath, "utf-8");
30422
30577
  const config = JSON.parse(content);
30423
30578
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -30667,11 +30822,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
30667
30822
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
30668
30823
  );
30669
30824
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
30670
- const signalPath = path58.join(
30825
+ const signalPath = path59.join(
30671
30826
  workspaceDir,
30672
30827
  `.compaction-reset-signal-${safeSessionKey}`
30673
30828
  );
30674
- await writeFile35(
30829
+ await writeFile36(
30675
30830
  signalPath,
30676
30831
  JSON.stringify({
30677
30832
  sessionKey,
@@ -30698,7 +30853,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
30698
30853
  );
30699
30854
  async function ensureHourlySummaryCron(api2) {
30700
30855
  const jobId = "engram-hourly-summary";
30701
- const cronFilePath = path58.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
30856
+ const cronFilePath = path59.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
30702
30857
  try {
30703
30858
  let jobsData = { version: 1, jobs: [] };
30704
30859
  try {
@@ -30739,7 +30894,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
30739
30894
  state: {}
30740
30895
  };
30741
30896
  jobsData.jobs.push(newJob);
30742
- await writeFile35(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
30897
+ await writeFile36(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
30743
30898
  log.info("auto-registered hourly summary cron job");
30744
30899
  } catch (err) {
30745
30900
  log.error("failed to auto-register hourly summary cron job:", err);