@joshuaswarren/openclaw-engram 8.3.77 → 8.3.79

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/dist/index.js CHANGED
@@ -451,7 +451,7 @@ function parseConfig(raw) {
451
451
 
452
452
  // src/orchestrator.ts
453
453
  import path26 from "path";
454
- import { createHash as createHash5 } from "crypto";
454
+ import { createHash as createHash6 } from "crypto";
455
455
  import { mkdir as mkdir19, readdir as readdir11, readFile as readFile18, writeFile as writeFile17 } from "fs/promises";
456
456
 
457
457
  // src/signal.ts
@@ -5313,6 +5313,9 @@ var StorageManager = class _StorageManager {
5313
5313
  get compressionGuidelineStatePath() {
5314
5314
  return path4.join(this.stateDir, "compression-guideline-state.json");
5315
5315
  }
5316
+ get behaviorSignalsPath() {
5317
+ return path4.join(this.stateDir, "behavior-signals.jsonl");
5318
+ }
5316
5319
  /**
5317
5320
  * Load user-defined entity aliases from config/aliases.json in the memory store.
5318
5321
  * File format: { "variant": "canonical", "variant2": "canonical", ... }
@@ -5998,6 +6001,67 @@ ${memory.content}
5998
6001
  await appendFile(this.memoryActionsPath, payload, "utf-8");
5999
6002
  return events.length;
6000
6003
  }
6004
+ async appendBehaviorSignals(events) {
6005
+ if (events.length === 0) return 0;
6006
+ await this.ensureDirectories();
6007
+ let existingKeys = /* @__PURE__ */ new Set();
6008
+ try {
6009
+ const raw = await readFile2(this.behaviorSignalsPath, "utf-8");
6010
+ const lines = raw.split("\n");
6011
+ for (const line of lines) {
6012
+ const row = line.trim();
6013
+ if (!row) continue;
6014
+ try {
6015
+ const parsed = JSON.parse(row);
6016
+ if (typeof parsed.memoryId === "string" && typeof parsed.signalHash === "string") {
6017
+ existingKeys.add(`${parsed.memoryId}:${parsed.signalHash}`);
6018
+ }
6019
+ } catch {
6020
+ }
6021
+ }
6022
+ } catch {
6023
+ existingKeys = /* @__PURE__ */ new Set();
6024
+ }
6025
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
6026
+ const deduped = [];
6027
+ for (const event of events) {
6028
+ const key = `${event.memoryId}:${event.signalHash}`;
6029
+ if (existingKeys.has(key)) continue;
6030
+ existingKeys.add(key);
6031
+ deduped.push({
6032
+ ...event,
6033
+ timestamp: event.timestamp && event.timestamp.length > 0 ? event.timestamp : nowIso
6034
+ });
6035
+ }
6036
+ if (deduped.length === 0) return 0;
6037
+ const payload = deduped.map((event) => `${JSON.stringify(event)}
6038
+ `).join("");
6039
+ await appendFile(this.behaviorSignalsPath, payload, "utf-8");
6040
+ return deduped.length;
6041
+ }
6042
+ async readBehaviorSignals(limit = 200) {
6043
+ const cappedLimit = Math.max(0, Math.floor(limit));
6044
+ if (cappedLimit === 0) return [];
6045
+ try {
6046
+ const raw = await readFile2(this.behaviorSignalsPath, "utf-8");
6047
+ const out = [];
6048
+ const lines = raw.split("\n");
6049
+ for (let i = lines.length - 1; i >= 0 && out.length < cappedLimit; i -= 1) {
6050
+ const row = lines[i]?.trim();
6051
+ if (!row) continue;
6052
+ try {
6053
+ const parsed = JSON.parse(row);
6054
+ if (typeof parsed.timestamp === "string" && typeof parsed.namespace === "string" && typeof parsed.memoryId === "string" && typeof parsed.category === "string" && typeof parsed.signalType === "string" && typeof parsed.direction === "string" && typeof parsed.confidence === "number" && typeof parsed.signalHash === "string" && typeof parsed.source === "string") {
6055
+ out.push(parsed);
6056
+ }
6057
+ } catch {
6058
+ }
6059
+ }
6060
+ return out.reverse();
6061
+ } catch {
6062
+ return [];
6063
+ }
6064
+ }
6001
6065
  async readMemoryActionEvents(limit = 200) {
6002
6066
  const cappedLimit = Math.max(0, Math.floor(limit));
6003
6067
  if (cappedLimit === 0) return [];
@@ -9754,6 +9818,10 @@ function buildInstructionHeavyQuery(prompt, tokenCap, maxChars) {
9754
9818
  if (compact.length <= maxChars) return compact;
9755
9819
  return compact.slice(0, maxChars).trim();
9756
9820
  }
9821
+ function clampInstructionHeavyTokenCap(value) {
9822
+ if (!Number.isFinite(value)) return 8;
9823
+ return Math.max(8, Math.round(value));
9824
+ }
9757
9825
  function buildStandardQuery(prompt, maxChars) {
9758
9826
  const trimmed = collapseWhitespace(prompt);
9759
9827
  if (trimmed.length <= maxChars) return trimmed;
@@ -9772,7 +9840,7 @@ function buildRecallQueryPolicy(prompt, sessionKey, cfg) {
9772
9840
  }
9773
9841
  const promptShape = classifyRecallPromptShape(prompt);
9774
9842
  const maxChars = Math.max(120, cfg.cronRecallNormalizedQueryMaxChars);
9775
- const tokenCap = Math.max(8, cfg.cronRecallInstructionHeavyTokenCap);
9843
+ const tokenCap = clampInstructionHeavyTokenCap(cfg.cronRecallInstructionHeavyTokenCap);
9776
9844
  const retrievalQuery = promptShape === "instruction_heavy" ? buildInstructionHeavyQuery(prompt, tokenCap, maxChars) : buildStandardQuery(prompt, maxChars);
9777
9845
  const skipConversationRecall = cfg.cronConversationRecallMode === "never" ? true : cfg.cronConversationRecallMode === "always" ? false : promptShape === "instruction_heavy";
9778
9846
  const retrievalBudgetMode = promptShape === "instruction_heavy" ? "minimal" : "full";
@@ -13057,6 +13125,63 @@ var RoutingRulesStore = class {
13057
13125
  }
13058
13126
  };
13059
13127
 
13128
+ // src/behavior-signals.ts
13129
+ import { createHash as createHash5 } from "crypto";
13130
+ function normalizeSignalText(input) {
13131
+ return input.replace(/\s+/g, " ").trim().toLowerCase();
13132
+ }
13133
+ function buildBehaviorSignalHash(category, content) {
13134
+ const normalized = `${category}:${normalizeSignalText(content)}`;
13135
+ return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
13136
+ }
13137
+ function buildBehaviorSignalsForMemory(input) {
13138
+ const timestamp = input.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
13139
+ const signalHash = buildBehaviorSignalHash(input.category, input.content);
13140
+ const source = input.source ?? "extraction";
13141
+ if (input.category === "correction") {
13142
+ return [
13143
+ {
13144
+ timestamp,
13145
+ namespace: input.namespace,
13146
+ memoryId: input.memoryId,
13147
+ category: "correction",
13148
+ signalType: "correction_override",
13149
+ direction: "negative",
13150
+ confidence: input.confidence,
13151
+ signalHash,
13152
+ source
13153
+ }
13154
+ ];
13155
+ }
13156
+ if (input.category === "preference") {
13157
+ return [
13158
+ {
13159
+ timestamp,
13160
+ namespace: input.namespace,
13161
+ memoryId: input.memoryId,
13162
+ category: "preference",
13163
+ signalType: "preference_affinity",
13164
+ direction: "positive",
13165
+ confidence: input.confidence,
13166
+ signalHash,
13167
+ source
13168
+ }
13169
+ ];
13170
+ }
13171
+ return [];
13172
+ }
13173
+ function dedupeBehaviorSignalsByMemoryAndHash(signals) {
13174
+ const seen = /* @__PURE__ */ new Set();
13175
+ const out = [];
13176
+ for (const signal of signals) {
13177
+ const key = `${signal.memoryId}:${signal.signalHash}`;
13178
+ if (seen.has(key)) continue;
13179
+ seen.add(key);
13180
+ out.push(signal);
13181
+ }
13182
+ return out;
13183
+ }
13184
+
13060
13185
  // src/orchestrator.ts
13061
13186
  function isArtifactMemoryPath(filePath) {
13062
13187
  return /(?:^|[\\/])artifacts(?:[\\/]|$)/i.test(filePath);
@@ -14213,7 +14338,7 @@ ${r.snippet.trim()}
14213
14338
  const payload = {
14214
14339
  recordedAt: now,
14215
14340
  mode: options.recallMode,
14216
- queryHash: createHash5("sha256").update(options.prompt).digest("hex"),
14341
+ queryHash: createHash6("sha256").update(options.prompt).digest("hex"),
14217
14342
  queryLength: options.prompt.length,
14218
14343
  namespaces: options.recallNamespaces,
14219
14344
  seedCount: totalSeedCount,
@@ -14229,7 +14354,7 @@ ${r.snippet.trim()}
14229
14354
  async recallInternal(prompt, sessionKey) {
14230
14355
  const recallStart = Date.now();
14231
14356
  const timings = {};
14232
- const promptHash = createHash5("sha256").update(prompt).digest("hex");
14357
+ const promptHash = createHash6("sha256").update(prompt).digest("hex");
14233
14358
  const sections = [];
14234
14359
  const queryPolicy = buildRecallQueryPolicy(prompt, sessionKey, {
14235
14360
  cronRecallPolicyEnabled: this.config.cronRecallPolicyEnabled,
@@ -14238,7 +14363,7 @@ ${r.snippet.trim()}
14238
14363
  cronConversationRecallMode: this.config.cronConversationRecallMode
14239
14364
  });
14240
14365
  const retrievalQuery = queryPolicy.retrievalQuery || prompt;
14241
- const retrievalQueryHash = createHash5("sha256").update(retrievalQuery).digest("hex");
14366
+ const retrievalQueryHash = createHash6("sha256").update(retrievalQuery).digest("hex");
14242
14367
  let impressionRecorded = false;
14243
14368
  let recallSource = "none";
14244
14369
  let recalledMemoryCount = 0;
@@ -14273,7 +14398,7 @@ ${r.snippet.trim()}
14273
14398
  timings.total = `${Date.now() - recallStart}ms`;
14274
14399
  this.emitTrace({
14275
14400
  kind: "recall_summary",
14276
- traceId: createHash5("sha256").update(`${sessionKey ?? "default"}:${Date.now()}:${promptHash}`).digest("hex").slice(0, 16),
14401
+ traceId: createHash6("sha256").update(`${sessionKey ?? "default"}:${Date.now()}:${promptHash}`).digest("hex").slice(0, 16),
14277
14402
  operation: "recall",
14278
14403
  sessionKey,
14279
14404
  promptHash,
@@ -14870,7 +14995,7 @@ _Context: ${topQuestion.context}_`);
14870
14995
  const context = sections.length === 0 ? "" : sections.join("\n\n---\n\n");
14871
14996
  this.emitTrace({
14872
14997
  kind: "recall_summary",
14873
- traceId: createHash5("sha256").update(`${sessionKey ?? "default"}:${Date.now()}:${promptHash}`).digest("hex").slice(0, 16),
14998
+ traceId: createHash6("sha256").update(`${sessionKey ?? "default"}:${Date.now()}:${promptHash}`).digest("hex").slice(0, 16),
14874
14999
  operation: "recall",
14875
15000
  sessionKey,
14876
15001
  promptHash,
@@ -15019,7 +15144,7 @@ _Context: ${topQuestion.context}_`);
15019
15144
  if (!Array.isArray(turns) || turns.length === 0) return false;
15020
15145
  const normalized = turns.filter((t) => t.role === "user" || t.role === "assistant").map((t) => `${t.role}:${(t.content ?? "").trim().slice(0, this.config.extractionMaxTurnChars)}`).join("\n");
15021
15146
  if (!normalized) return false;
15022
- const fingerprint = createHash5("sha256").update(normalized).digest("hex");
15147
+ const fingerprint = createHash6("sha256").update(normalized).digest("hex");
15023
15148
  const now = Date.now();
15024
15149
  const seenAt = this.recentExtractionFingerprints.get(fingerprint);
15025
15150
  if (seenAt && now - seenAt < this.config.extractionDedupeWindowMs) {
@@ -15383,6 +15508,17 @@ _Context: ${topQuestion.context}_`);
15383
15508
  persistedIdsByStorage.set(key, { storage: targetStorage, ids: [id] });
15384
15509
  };
15385
15510
  let dedupedCount = 0;
15511
+ const behaviorSignalsByStorage = /* @__PURE__ */ new Map();
15512
+ const trackBehaviorSignals = (targetStorage, events) => {
15513
+ if (events.length === 0) return;
15514
+ const key = targetStorage.dir;
15515
+ const existing = behaviorSignalsByStorage.get(key);
15516
+ if (existing) {
15517
+ existing.events.push(...events);
15518
+ return;
15519
+ }
15520
+ behaviorSignalsByStorage.set(key, { storage: targetStorage, events: [...events] });
15521
+ };
15386
15522
  if (!result || !Array.isArray(result.facts)) {
15387
15523
  log.warn("persistExtraction: result or result.facts is invalid, skipping", { resultType: typeof result, factsType: typeof result?.facts });
15388
15524
  return persistedIds;
@@ -15573,6 +15709,17 @@ _Context: ${topQuestion.context}_`);
15573
15709
  } catch {
15574
15710
  }
15575
15711
  }
15712
+ trackBehaviorSignals(
15713
+ targetStorage,
15714
+ buildBehaviorSignalsForMemory({
15715
+ memoryId: parentId,
15716
+ category: writeCategory,
15717
+ content: fact.content,
15718
+ namespace: this.namespaceFromStorageDir(targetStorage.dir),
15719
+ confidence: fact.confidence,
15720
+ source: "extraction"
15721
+ })
15722
+ );
15576
15723
  continue;
15577
15724
  }
15578
15725
  }
@@ -15633,6 +15780,17 @@ _Context: ${topQuestion.context}_`);
15633
15780
  `routing applied for memory ${memoryId}: rule=${routedRuleId} category=${writeCategory} storage=${targetStorage.dir}`
15634
15781
  );
15635
15782
  }
15783
+ trackBehaviorSignals(
15784
+ targetStorage,
15785
+ buildBehaviorSignalsForMemory({
15786
+ memoryId,
15787
+ category: writeCategory,
15788
+ content: fact.content,
15789
+ namespace: this.namespaceFromStorageDir(targetStorage.dir),
15790
+ confidence: fact.confidence,
15791
+ source: "extraction"
15792
+ })
15793
+ );
15636
15794
  trackPersistedId(targetStorage, memoryId);
15637
15795
  if (threadEpisodeIdsForGraph && !threadEpisodeIdsForGraph.includes(memoryId)) {
15638
15796
  threadEpisodeIdsForGraph.push(memoryId);
@@ -15750,6 +15908,11 @@ _Context: ${topQuestion.context}_`);
15750
15908
  (err) => log.warn(`content-hash index save failed: ${err}`)
15751
15909
  );
15752
15910
  }
15911
+ for (const { storage: targetStorage, events } of behaviorSignalsByStorage.values()) {
15912
+ const dedupedSignals = dedupeBehaviorSignalsByMemoryAndHash(events);
15913
+ if (dedupedSignals.length === 0) continue;
15914
+ await targetStorage.appendBehaviorSignals(dedupedSignals).catch((err) => log.warn(`appendBehaviorSignals failed (non-fatal): ${err}`));
15915
+ }
15753
15916
  const dedupSuffix = dedupedCount > 0 ? ` (${dedupedCount} deduped)` : "";
15754
15917
  log.info(
15755
15918
  `persisted: ${facts.length - dedupedCount} facts${dedupSuffix}, ${entities.length} entities, ${questions.length} questions, ${profileUpdates.length} profile updates`
@@ -17096,7 +17259,7 @@ ${lines.join("\n\n")}`;
17096
17259
 
17097
17260
  // src/tools.ts
17098
17261
  import path28 from "path";
17099
- import { createHash as createHash6 } from "crypto";
17262
+ import { createHash as createHash7 } from "crypto";
17100
17263
  import { Type } from "@sinclair/typebox";
17101
17264
 
17102
17265
  // src/work/storage.ts
@@ -17867,7 +18030,7 @@ function registerTools(api, orchestrator) {
17867
18030
  ];
17868
18031
  function promptHashForTelemetry(input) {
17869
18032
  if (typeof input !== "string" || input.trim().length === 0) return void 0;
17870
- return createHash6("sha256").update(input).digest("hex").slice(0, 16);
18033
+ return createHash7("sha256").update(input).digest("hex").slice(0, 16);
17871
18034
  }
17872
18035
  function namespaceFromPath(p) {
17873
18036
  const m = p.match(/[\\/]+namespaces[\\/]+([^\\/]+)[\\/]+/);
@@ -19460,17 +19623,17 @@ var EXPORT_FORMAT = "openclaw-engram-export";
19460
19623
  var EXPORT_SCHEMA_VERSION = 1;
19461
19624
 
19462
19625
  // src/transfer/fs-utils.ts
19463
- import { createHash as createHash7 } from "crypto";
19626
+ import { createHash as createHash8 } from "crypto";
19464
19627
  import { mkdir as mkdir21, readdir as readdir13, readFile as readFile20, stat as stat6, writeFile as writeFile19 } from "fs/promises";
19465
19628
  import path29 from "path";
19466
19629
  async function sha256File(filePath) {
19467
19630
  const buf = await readFile20(filePath);
19468
- const sha256 = createHash7("sha256").update(buf).digest("hex");
19631
+ const sha256 = createHash8("sha256").update(buf).digest("hex");
19469
19632
  return { sha256, bytes: buf.byteLength };
19470
19633
  }
19471
19634
  function sha256String(content) {
19472
19635
  const buf = Buffer.from(content, "utf-8");
19473
- const sha256 = createHash7("sha256").update(buf).digest("hex");
19636
+ const sha256 = createHash8("sha256").update(buf).digest("hex");
19474
19637
  return { sha256, bytes: buf.byteLength };
19475
19638
  }
19476
19639
  async function writeJsonFile(filePath, value) {