@joshuaswarren/openclaw-engram 9.0.19 → 9.0.20

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
@@ -31,6 +31,7 @@ AI agents forget everything between conversations. Engram fixes that.
31
31
  - **Pluggable search** — Choose from six search backends: QMD (hybrid BM25+vector+reranking), LanceDB, Meilisearch, Orama, remote HTTP, or bring your own.
32
32
  - **Memory OS features** — Graph recall, temporal memory tree, lifecycle policy, compounding, shared context, memory boxes, and identity continuity can be enabled progressively as your install grows.
33
33
  - **Benchmark-first roadmap** — Engram now has an evaluation harness with live shadow recall recording and a CI benchmark delta gate, so memory improvements can be measured and regression-checked instead of argued from anecdotes.
34
+ - **Objective-state foundation** — When `objectiveStateMemoryEnabled` and `objectiveStateSnapshotWritesEnabled` are enabled together, Engram records normalized file, process, and tool outcomes from `agent_end` tool activity into a dedicated objective-state store.
34
35
  - **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.
35
36
 
36
37
  ## Quick Start
package/dist/index.js CHANGED
@@ -26056,6 +26056,13 @@ function assertIsoRecordedAt(value) {
26056
26056
  }
26057
26057
  return value;
26058
26058
  }
26059
+ function objectiveStateDay(recordedAt) {
26060
+ const day = recordedAt.slice(0, 10);
26061
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) {
26062
+ throw new Error("recordedAt must start with a valid YYYY-MM-DD date");
26063
+ }
26064
+ return day;
26065
+ }
26059
26066
  function optionalStringArray2(value, field) {
26060
26067
  if (value === void 0) return void 0;
26061
26068
  if (!Array.isArray(value)) throw new Error(`${field} must be an array of strings`);
@@ -26128,6 +26135,16 @@ function validateObjectiveStateSnapshot(raw) {
26128
26135
  metadata: validateMetadata(raw.metadata)
26129
26136
  };
26130
26137
  }
26138
+ async function recordObjectiveStateSnapshot(options) {
26139
+ const rootDir = resolveObjectiveStateStoreDir(options.memoryDir, options.objectiveStateStoreDir);
26140
+ const validated = validateObjectiveStateSnapshot(options.snapshot);
26141
+ const day = objectiveStateDay(validated.recordedAt);
26142
+ const snapshotsDir = path52.join(rootDir, "snapshots", day);
26143
+ const filePath = path52.join(snapshotsDir, `${validated.snapshotId}.json`);
26144
+ await mkdir34(snapshotsDir, { recursive: true });
26145
+ await writeFile30(filePath, JSON.stringify(validated, null, 2), "utf8");
26146
+ return filePath;
26147
+ }
26131
26148
  async function getObjectiveStateStoreStatus(options) {
26132
26149
  const rootDir = resolveObjectiveStateStoreDir(options.memoryDir, options.objectiveStateStoreDir);
26133
26150
  const snapshotsDir = path52.join(rootDir, "snapshots");
@@ -28555,6 +28572,303 @@ function parseDuration(duration) {
28555
28572
  return total || 12;
28556
28573
  }
28557
28574
 
28575
+ // src/objective-state-writers.ts
28576
+ import crypto2 from "crypto";
28577
+ function hashSha256(value) {
28578
+ return crypto2.createHash("sha256").update(value).digest("hex");
28579
+ }
28580
+ function isRecord3(value) {
28581
+ return typeof value === "object" && value !== null && !Array.isArray(value);
28582
+ }
28583
+ function optionalString2(value) {
28584
+ if (typeof value !== "string") return void 0;
28585
+ const trimmed = value.trim();
28586
+ return trimmed.length > 0 ? trimmed : void 0;
28587
+ }
28588
+ function toolNameTokens(toolName) {
28589
+ if (!toolName) return [];
28590
+ return toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 0);
28591
+ }
28592
+ function normalizedToolName(toolName) {
28593
+ return toolNameTokens(toolName).join("_");
28594
+ }
28595
+ function parseToolArguments(value) {
28596
+ if (isRecord3(value)) return value;
28597
+ if (typeof value !== "string") return void 0;
28598
+ try {
28599
+ const parsed = JSON.parse(value);
28600
+ return isRecord3(parsed) ? parsed : void 0;
28601
+ } catch {
28602
+ return void 0;
28603
+ }
28604
+ }
28605
+ function extractTextContent(value) {
28606
+ if (typeof value === "string") return value.trim();
28607
+ if (Array.isArray(value)) {
28608
+ return value.map((block) => {
28609
+ if (typeof block === "string") return block.trim();
28610
+ if (isRecord3(block) && block.type === "text" && typeof block.text === "string") {
28611
+ return block.text.trim();
28612
+ }
28613
+ return "";
28614
+ }).filter((item) => item.length > 0).join("\n");
28615
+ }
28616
+ if (isRecord3(value)) {
28617
+ return JSON.stringify(value);
28618
+ }
28619
+ return "";
28620
+ }
28621
+ function parseToolResultPayload(content) {
28622
+ const text = extractTextContent(content);
28623
+ if (text.length === 0) return void 0;
28624
+ try {
28625
+ return JSON.parse(text);
28626
+ } catch {
28627
+ return text;
28628
+ }
28629
+ }
28630
+ function resultHash(value) {
28631
+ if (value === void 0) return void 0;
28632
+ const canonical = typeof value === "string" ? value : JSON.stringify(value);
28633
+ if (!canonical || canonical.length === 0) return void 0;
28634
+ return `sha256:${hashSha256(canonical)}`;
28635
+ }
28636
+ function getToolCallContexts(messages) {
28637
+ const contexts = /* @__PURE__ */ new Map();
28638
+ for (const message of messages) {
28639
+ if (message.role !== "assistant") continue;
28640
+ const toolCalls = message.tool_calls ?? message.toolCalls;
28641
+ if (!Array.isArray(toolCalls)) continue;
28642
+ for (const call of toolCalls) {
28643
+ if (!isRecord3(call)) continue;
28644
+ const toolCallId = optionalString2(call.id) ?? optionalString2(call.toolCallId);
28645
+ if (!toolCallId) continue;
28646
+ const fn = isRecord3(call.function) ? call.function : void 0;
28647
+ const toolName = optionalString2(fn?.name) ?? optionalString2(call.name);
28648
+ const args = parseToolArguments(fn?.arguments) ?? parseToolArguments(call.arguments) ?? parseToolArguments(call.args) ?? parseToolArguments(call.input);
28649
+ contexts.set(toolCallId, { toolCallId, toolName, args });
28650
+ }
28651
+ }
28652
+ return contexts;
28653
+ }
28654
+ function toolCallIdForMessage(message) {
28655
+ return optionalString2(message.tool_call_id) ?? optionalString2(message.toolCallId) ?? optionalString2(message.tool_use_id) ?? optionalString2(message.toolUseId);
28656
+ }
28657
+ function toolNameForMessage(message, context) {
28658
+ return optionalString2(message.name) ?? optionalString2(message.toolName) ?? optionalString2(message.tool) ?? context?.toolName;
28659
+ }
28660
+ function pickString(args, keys) {
28661
+ if (!args) return void 0;
28662
+ for (const key of keys) {
28663
+ const value = optionalString2(args[key]);
28664
+ if (value) return value;
28665
+ }
28666
+ return void 0;
28667
+ }
28668
+ function pickFirstStringArrayValue(args, key) {
28669
+ const value = args?.[key];
28670
+ if (!Array.isArray(value)) return void 0;
28671
+ for (const item of value) {
28672
+ const candidate = optionalString2(item);
28673
+ if (candidate) return candidate;
28674
+ }
28675
+ return void 0;
28676
+ }
28677
+ function fileScopeFromArgs(args) {
28678
+ const destinationPath = pickString(args, ["destination", "dest", "targetPath", "target", "to"]) ?? pickString(args, ["path", "filePath", "workspacePath", "projectPath"]) ?? pickFirstStringArrayValue(args, "paths");
28679
+ const sourcePath = pickString(args, ["source", "src", "from", "oldPath"]);
28680
+ const scope = destinationPath ?? sourcePath;
28681
+ return { scope, sourcePath, destinationPath };
28682
+ }
28683
+ function fileContentHash(args) {
28684
+ const content = pickString(args, ["content", "patch", "diff", "text", "value"]) ?? args?.updates;
28685
+ return resultHash(content);
28686
+ }
28687
+ function inferOutcome(message, parsedPayload) {
28688
+ if (message.isError === true) return "failure";
28689
+ if (isRecord3(parsedPayload)) {
28690
+ if (parsedPayload.partial === true || parsedPayload.status === "partial") return "partial";
28691
+ if (parsedPayload.success === false || parsedPayload.ok === false) return "failure";
28692
+ if (parsedPayload.success === true || parsedPayload.ok === true) return "success";
28693
+ if (typeof parsedPayload.exitCode === "number") {
28694
+ return parsedPayload.exitCode === 0 ? "success" : "failure";
28695
+ }
28696
+ if (optionalString2(parsedPayload.error)) return "failure";
28697
+ if (parsedPayload.status === "error" || parsedPayload.status === "failed") return "failure";
28698
+ if (parsedPayload.status === "ok" || parsedPayload.status === "success") return "success";
28699
+ }
28700
+ if (typeof parsedPayload === "string") {
28701
+ const lowered = parsedPayload.toLowerCase();
28702
+ if (lowered.includes("error") || lowered.includes("failed") || lowered.includes("exception")) {
28703
+ return "failure";
28704
+ }
28705
+ }
28706
+ return "unknown";
28707
+ }
28708
+ function isProcessTool(toolName, args) {
28709
+ const tokens = toolNameTokens(toolName);
28710
+ const normalizedName = normalizedToolName(toolName);
28711
+ if (pickString(args, ["cmd", "command", "script"])) return true;
28712
+ return ["exec", "shell", "bash", "terminal", "run_command", "exec_command"].some(
28713
+ (token) => token.includes("_") ? normalizedName === token : tokens.includes(token)
28714
+ );
28715
+ }
28716
+ function isFileTool(toolName, args) {
28717
+ const tokens = toolNameTokens(toolName);
28718
+ const fileScope = fileScopeFromArgs(args);
28719
+ if (fileScope.scope) return true;
28720
+ return ["file", "path", "patch", "directory", "mkdir", "rename", "move"].some(
28721
+ (token) => tokens.includes(token)
28722
+ );
28723
+ }
28724
+ function inferFileChangeKind(toolName, outcome) {
28725
+ if (outcome === "failure") return "failed";
28726
+ const tokens = toolNameTokens(toolName);
28727
+ if (["delete", "remove", "unlink"].some((token) => tokens.includes(token))) return "deleted";
28728
+ if (["create", "mkdir", "new"].some((token) => tokens.includes(token))) return "created";
28729
+ if (["write", "edit", "patch", "update", "append", "move", "rename"].some((token) => tokens.includes(token))) {
28730
+ return "updated";
28731
+ }
28732
+ return "observed";
28733
+ }
28734
+ function buildFileValueRefs(args, changeKind) {
28735
+ const { sourcePath, destinationPath, scope } = fileScopeFromArgs(args);
28736
+ const contentHash = fileContentHash(args);
28737
+ if (changeKind === "failed") {
28738
+ if (sourcePath && destinationPath && sourcePath !== destinationPath) {
28739
+ return {
28740
+ before: { ref: sourcePath },
28741
+ after: { ref: destinationPath }
28742
+ };
28743
+ }
28744
+ return {
28745
+ before: sourcePath ? { ref: sourcePath } : void 0,
28746
+ after: scope ? { ref: scope } : void 0
28747
+ };
28748
+ }
28749
+ if (changeKind === "deleted") {
28750
+ return {
28751
+ before: scope ? { exists: true, ref: scope } : void 0,
28752
+ after: { exists: false }
28753
+ };
28754
+ }
28755
+ if (changeKind === "created") {
28756
+ return {
28757
+ after: {
28758
+ exists: true,
28759
+ ref: scope,
28760
+ valueHash: contentHash
28761
+ }
28762
+ };
28763
+ }
28764
+ if (sourcePath && destinationPath && sourcePath !== destinationPath) {
28765
+ return {
28766
+ before: { exists: true, ref: sourcePath },
28767
+ after: {
28768
+ exists: true,
28769
+ ref: destinationPath
28770
+ }
28771
+ };
28772
+ }
28773
+ return {
28774
+ after: {
28775
+ exists: true,
28776
+ ref: scope,
28777
+ valueHash: contentHash
28778
+ }
28779
+ };
28780
+ }
28781
+ function summarizeSnapshot(kind, changeKind, toolName, scope) {
28782
+ const action = changeKind === "executed" ? "Executed" : changeKind === "failed" ? "Failed" : changeKind === "created" ? "Created" : changeKind === "deleted" ? "Deleted" : changeKind === "updated" ? "Updated" : "Observed";
28783
+ if (kind === "process") return `${action} process via ${toolName}: ${scope}`;
28784
+ if (kind === "file") return `${action} file via ${toolName}: ${scope}`;
28785
+ return `${action} tool result from ${toolName}: ${scope}`;
28786
+ }
28787
+ function buildGenericToolAfterRef(outcome, parsedPayload) {
28788
+ const valueHash = resultHash(parsedPayload);
28789
+ return valueHash ? { valueHash } : { exists: outcome !== "failure" };
28790
+ }
28791
+ function snapshotIdFor(sessionKey, recordedAt, index, toolName, scope) {
28792
+ const digest = crypto2.createHash("sha256").update(`${sessionKey}|${recordedAt}|${index}|${toolName}|${scope}`).digest("hex").slice(0, 12);
28793
+ return `obj-${digest}`;
28794
+ }
28795
+ function deriveObjectiveStateSnapshotsFromAgentMessages(options) {
28796
+ const toolCallsById = getToolCallContexts(options.messages);
28797
+ const snapshots = [];
28798
+ for (const message of options.messages) {
28799
+ if (message.role !== "tool") continue;
28800
+ const toolCallId = toolCallIdForMessage(message);
28801
+ const context = toolCallId ? toolCallsById.get(toolCallId) : void 0;
28802
+ const toolName = toolNameForMessage(message, context);
28803
+ if (!toolName) continue;
28804
+ const parsedPayload = parseToolResultPayload(message.content);
28805
+ const outcome = inferOutcome(message, parsedPayload);
28806
+ const args = context?.args;
28807
+ const command = pickString(args, ["cmd", "command", "script"]);
28808
+ let kind = "tool";
28809
+ let changeKind = outcome === "failure" ? "failed" : "observed";
28810
+ let scope = toolName;
28811
+ let before;
28812
+ let after;
28813
+ if (isProcessTool(toolName, args)) {
28814
+ kind = "process";
28815
+ changeKind = outcome === "failure" ? "failed" : "executed";
28816
+ scope = command ?? toolName;
28817
+ after = { exists: outcome !== "failure", valueHash: resultHash(parsedPayload) };
28818
+ } else if (isFileTool(toolName, args)) {
28819
+ kind = "file";
28820
+ changeKind = inferFileChangeKind(toolName, outcome);
28821
+ const fileScope = fileScopeFromArgs(args);
28822
+ scope = fileScope.scope ?? toolName;
28823
+ const refs = buildFileValueRefs(args, changeKind);
28824
+ before = refs.before;
28825
+ after = refs.after;
28826
+ } else {
28827
+ after = buildGenericToolAfterRef(outcome, parsedPayload);
28828
+ }
28829
+ snapshots.push({
28830
+ schemaVersion: 1,
28831
+ snapshotId: snapshotIdFor(options.sessionKey, options.recordedAt, snapshots.length, toolName, scope),
28832
+ recordedAt: options.recordedAt,
28833
+ sessionKey: options.sessionKey,
28834
+ source: "tool_result",
28835
+ kind,
28836
+ changeKind,
28837
+ scope,
28838
+ summary: summarizeSnapshot(kind, changeKind, toolName, scope),
28839
+ toolName,
28840
+ command,
28841
+ outcome,
28842
+ before,
28843
+ after,
28844
+ tags: ["agent-end", `tool:${toolName}`],
28845
+ metadata: toolCallId ? { toolCallId } : void 0
28846
+ });
28847
+ }
28848
+ return snapshots;
28849
+ }
28850
+ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
28851
+ if (!options.objectiveStateMemoryEnabled || !options.objectiveStateSnapshotWritesEnabled) {
28852
+ return { snapshots: [], filePaths: [] };
28853
+ }
28854
+ const snapshots = deriveObjectiveStateSnapshotsFromAgentMessages({
28855
+ sessionKey: options.sessionKey,
28856
+ recordedAt: options.recordedAt,
28857
+ messages: options.messages
28858
+ });
28859
+ const filePaths = [];
28860
+ for (const snapshot of snapshots) {
28861
+ filePaths.push(
28862
+ await recordObjectiveStateSnapshot({
28863
+ memoryDir: options.memoryDir,
28864
+ objectiveStateStoreDir: options.objectiveStateStoreDir,
28865
+ snapshot
28866
+ })
28867
+ );
28868
+ }
28869
+ return { snapshots, filePaths };
28870
+ }
28871
+
28558
28872
  // src/index.ts
28559
28873
  import { readFile as readFile39, writeFile as writeFile31 } from "fs/promises";
28560
28874
  import { readFileSync as readFileSync4 } from "fs";
@@ -28698,6 +29012,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
28698
29012
  try {
28699
29013
  const messages = event.messages;
28700
29014
  const lastTurn = extractLastTurn(messages);
29015
+ const eventTimestamp = (/* @__PURE__ */ new Date()).toISOString();
28701
29016
  if (orchestrator.config.hourlySummariesIncludeToolStats) {
28702
29017
  const toolNames = [];
28703
29018
  for (const msg of messages) {
@@ -28716,23 +29031,35 @@ Use this context naturally when relevant. Never quote or expose this memory cont
28716
29031
  }
28717
29032
  }
28718
29033
  }
28719
- const ts = (/* @__PURE__ */ new Date()).toISOString();
28720
29034
  for (const tool of toolNames) {
28721
- await orchestrator.transcript.appendToolUse({ timestamp: ts, sessionKey, tool });
29035
+ await orchestrator.transcript.appendToolUse({ timestamp: eventTimestamp, sessionKey, tool });
28722
29036
  }
28723
29037
  }
29038
+ try {
29039
+ await recordObjectiveStateSnapshotsFromAgentMessages({
29040
+ memoryDir: orchestrator.config.memoryDir,
29041
+ objectiveStateStoreDir: orchestrator.config.objectiveStateStoreDir,
29042
+ objectiveStateMemoryEnabled: orchestrator.config.objectiveStateMemoryEnabled,
29043
+ objectiveStateSnapshotWritesEnabled: orchestrator.config.objectiveStateSnapshotWritesEnabled,
29044
+ sessionKey,
29045
+ recordedAt: eventTimestamp,
29046
+ messages
29047
+ });
29048
+ } catch (error) {
29049
+ log.debug(`agent_end objective-state writer skipped due to error: ${error}`);
29050
+ }
28724
29051
  for (const msg of lastTurn) {
28725
29052
  const rawRole = typeof msg.role === "string" ? msg.role : "";
28726
29053
  if (rawRole !== "user" && rawRole !== "assistant") {
28727
29054
  continue;
28728
29055
  }
28729
29056
  const role = rawRole;
28730
- const content = extractTextContent(msg);
29057
+ const content = extractTextContent2(msg);
28731
29058
  if (content.length < 10) continue;
28732
29059
  const cleaned = role === "user" ? cleanUserMessage(content) : content;
28733
29060
  if (orchestrator.config.transcriptEnabled) {
28734
29061
  await orchestrator.transcript.append({
28735
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29062
+ timestamp: eventTimestamp,
28736
29063
  role,
28737
29064
  content: cleaned,
28738
29065
  sessionKey,
@@ -28920,7 +29247,7 @@ function extractLastTurn(messages) {
28920
29247
  }
28921
29248
  return lastUserIdx >= 0 ? messages.slice(lastUserIdx) : messages.slice(-2);
28922
29249
  }
28923
- function extractTextContent(msg) {
29250
+ function extractTextContent2(msg) {
28924
29251
  if (typeof msg.content === "string") return msg.content;
28925
29252
  if (Array.isArray(msg.content)) {
28926
29253
  return msg.content.filter(