@joshuaswarren/openclaw-engram 8.3.6 → 8.3.8

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
@@ -312,6 +312,16 @@ function parseConfig(raw) {
312
312
  factArchivalMaxImportance: typeof cfg.factArchivalMaxImportance === "number" ? cfg.factArchivalMaxImportance : 0.3,
313
313
  factArchivalMaxAccessCount: typeof cfg.factArchivalMaxAccessCount === "number" ? cfg.factArchivalMaxAccessCount : 2,
314
314
  factArchivalProtectedCategories: Array.isArray(cfg.factArchivalProtectedCategories) ? cfg.factArchivalProtectedCategories.filter((c) => typeof c === "string") : ["commitment", "preference", "decision", "principle"],
315
+ // v8.3 lifecycle policy engine (default off)
316
+ lifecyclePolicyEnabled: cfg.lifecyclePolicyEnabled === true,
317
+ lifecycleFilterStaleEnabled: cfg.lifecycleFilterStaleEnabled === true,
318
+ lifecyclePromoteHeatThreshold: typeof cfg.lifecyclePromoteHeatThreshold === "number" ? Math.min(1, Math.max(0, cfg.lifecyclePromoteHeatThreshold)) : 0.55,
319
+ lifecycleStaleDecayThreshold: typeof cfg.lifecycleStaleDecayThreshold === "number" ? Math.min(1, Math.max(0, cfg.lifecycleStaleDecayThreshold)) : 0.65,
320
+ lifecycleArchiveDecayThreshold: typeof cfg.lifecycleArchiveDecayThreshold === "number" ? Math.min(1, Math.max(0, cfg.lifecycleArchiveDecayThreshold)) : 0.85,
321
+ lifecycleProtectedCategories: Array.isArray(cfg.lifecycleProtectedCategories) ? cfg.lifecycleProtectedCategories.filter(
322
+ (c) => typeof c === "string" && VALID_MEMORY_CATEGORIES.has(c)
323
+ ) : ["decision", "principle", "commitment", "preference"],
324
+ lifecycleMetricsEnabled: typeof cfg.lifecycleMetricsEnabled === "boolean" ? cfg.lifecycleMetricsEnabled : cfg.lifecyclePolicyEnabled === true,
315
325
  // v8.0 phase 1
316
326
  recallPlannerEnabled: cfg.recallPlannerEnabled !== false,
317
327
  recallPlannerMaxQmdResultsMinimal: typeof cfg.recallPlannerMaxQmdResultsMinimal === "number" ? cfg.recallPlannerMaxQmdResultsMinimal : 4,
@@ -4989,6 +4999,37 @@ ${sanitized.text}
4989
4999
  log.debug(`updated memory ${id}`);
4990
5000
  return true;
4991
5001
  }
5002
+ /**
5003
+ * Update frontmatter fields without changing memory content.
5004
+ * Returns false when the memory is not found.
5005
+ */
5006
+ async writeMemoryFrontmatter(memory, patch) {
5007
+ const beforeStatus = memory.frontmatter.status ?? "active";
5008
+ const updated = {
5009
+ ...memory.frontmatter,
5010
+ ...patch
5011
+ };
5012
+ const afterStatus = updated.status ?? "active";
5013
+ const fileContent = `${serializeFrontmatter(updated)}
5014
+
5015
+ ${memory.content}
5016
+ `;
5017
+ await writeFile2(memory.path, fileContent, "utf-8");
5018
+ if (beforeStatus !== afterStatus) {
5019
+ this.bumpMemoryStatusVersion();
5020
+ }
5021
+ return true;
5022
+ }
5023
+ /**
5024
+ * Update frontmatter by memory ID.
5025
+ * Prefer writeMemoryFrontmatter(memory, patch) in batch loops to avoid full-corpus rescans.
5026
+ */
5027
+ async updateMemoryFrontmatter(id, patch) {
5028
+ const memories = await this.readAllMemories();
5029
+ const memory = memories.find((m) => m.frontmatter.id === id);
5030
+ if (!memory) return false;
5031
+ return this.writeMemoryFrontmatter(memory, patch);
5032
+ }
4992
5033
  /** Remove memories past their TTL expiresAt date */
4993
5034
  async cleanExpiredTTL() {
4994
5035
  const memories = await this.readAllMemories();
@@ -5325,8 +5366,8 @@ ${reflection}
5325
5366
  */
5326
5367
  static scoreEntity(entity, now) {
5327
5368
  const updated = entity.updated ? new Date(entity.updated).getTime() : 0;
5328
- const daysSince = Math.max(0, (now.getTime() - updated) / (1e3 * 60 * 60 * 24));
5329
- const recency = 1 / (1 + daysSince / 7);
5369
+ const daysSince2 = Math.max(0, (now.getTime() - updated) / (1e3 * 60 * 60 * 24));
5370
+ const recency = 1 / (1 + daysSince2 / 7);
5330
5371
  const frequency = Math.min(entity.facts.length / 20, 1);
5331
5372
  const activityScore = Math.min(entity.activity.length / 10, 1);
5332
5373
  const TYPE_PRIORITY = {
@@ -8752,6 +8793,167 @@ var TmtBuilder = class {
8752
8793
  }
8753
8794
  };
8754
8795
 
8796
+ // src/lifecycle.ts
8797
+ var DEFAULT_POLICY = {
8798
+ promoteHeatThreshold: 0.55,
8799
+ staleDecayThreshold: 0.65,
8800
+ archiveDecayThreshold: 0.85,
8801
+ protectedCategories: ["decision", "principle", "commitment", "preference"]
8802
+ };
8803
+ function clamp01(value) {
8804
+ if (!Number.isFinite(value)) return 0;
8805
+ if (value < 0) return 0;
8806
+ if (value > 1) return 1;
8807
+ return value;
8808
+ }
8809
+ function parseIsoMs(value) {
8810
+ if (!value) return null;
8811
+ const ms = Date.parse(value);
8812
+ return Number.isFinite(ms) ? ms : null;
8813
+ }
8814
+ function daysSince(value, nowMs) {
8815
+ const ts = parseIsoMs(value);
8816
+ if (ts === null) return 365;
8817
+ return Math.max(0, (nowMs - ts) / 864e5);
8818
+ }
8819
+ function confidenceTierWeight(frontmatter) {
8820
+ switch (frontmatter.confidenceTier) {
8821
+ case "explicit":
8822
+ return 1;
8823
+ case "implied":
8824
+ return 0.8;
8825
+ case "inferred":
8826
+ return 0.6;
8827
+ case "speculative":
8828
+ return 0.35;
8829
+ default:
8830
+ return clamp01(frontmatter.confidence ?? 0.5);
8831
+ }
8832
+ }
8833
+ function accessWeight(accessCount) {
8834
+ const raw = accessCount ?? 0;
8835
+ if (raw <= 0) return 0;
8836
+ return clamp01(Math.log1p(raw) / Math.log1p(20));
8837
+ }
8838
+ function recencyWeight(frontmatter, nowMs) {
8839
+ const lastTouch = frontmatter.lastAccessed ?? frontmatter.updated ?? frontmatter.created;
8840
+ const ageDays = daysSince(lastTouch, nowMs);
8841
+ return clamp01(1 - ageDays / 90);
8842
+ }
8843
+ function feedbackWeight(signals) {
8844
+ const raw = signals?.feedbackScore ?? 0;
8845
+ return clamp01((raw + 1) / 2);
8846
+ }
8847
+ function isProtectedMemory(frontmatter, policy) {
8848
+ return frontmatter.policyClass === "protected" || policy.protectedCategories.includes(frontmatter.category);
8849
+ }
8850
+ function resolveLifecycleState(frontmatter) {
8851
+ if (frontmatter.status === "archived") return "archived";
8852
+ return frontmatter.lifecycleState ?? "candidate";
8853
+ }
8854
+ function computeHeat(memory, now, signals) {
8855
+ const frontmatter = memory.frontmatter;
8856
+ if (frontmatter.status === "archived") return 0;
8857
+ const nowMs = now.getTime();
8858
+ const confidence = confidenceTierWeight(frontmatter);
8859
+ const access3 = accessWeight(frontmatter.accessCount);
8860
+ const recency = recencyWeight(frontmatter, nowMs);
8861
+ const importance = clamp01(frontmatter.importance?.score ?? 0.5);
8862
+ const feedback = feedbackWeight(signals);
8863
+ const disputedPenalty = frontmatter.verificationState === "disputed" ? 0.2 : 0;
8864
+ const score = confidence * 0.25 + access3 * 0.3 + recency * 0.2 + importance * 0.15 + feedback * 0.1 - disputedPenalty;
8865
+ return clamp01(score);
8866
+ }
8867
+ function computeDecay(memory, now, signals) {
8868
+ const frontmatter = memory.frontmatter;
8869
+ if (frontmatter.status === "archived") return 1;
8870
+ const nowMs = now.getTime();
8871
+ const ageDays = daysSince(frontmatter.updated ?? frontmatter.created, nowMs);
8872
+ const staleAccessDays = daysSince(frontmatter.lastAccessed, nowMs);
8873
+ const ageRisk = clamp01(ageDays / 180);
8874
+ const staleAccessRisk = clamp01(staleAccessDays / 120);
8875
+ const confidenceRisk = 1 - confidenceTierWeight(frontmatter);
8876
+ const feedbackRisk = clamp01(((signals?.feedbackScore ?? 0) * -1 + 1) / 2);
8877
+ const heat = computeHeat(memory, now, signals);
8878
+ const score = ageRisk * 0.3 + staleAccessRisk * 0.25 + confidenceRisk * 0.2 + feedbackRisk * 0.1 + (1 - heat) * 0.15;
8879
+ return clamp01(score);
8880
+ }
8881
+ function toTerminalDisputedState(currentState) {
8882
+ if (currentState === "archived") return "archived";
8883
+ return "stale";
8884
+ }
8885
+ function isActiveEligible(verificationState) {
8886
+ return verificationState === "user_confirmed" || verificationState === "system_inferred";
8887
+ }
8888
+ function decideLifecycleTransition(memory, policy, now, signals) {
8889
+ const mergedPolicy = { ...DEFAULT_POLICY, ...policy };
8890
+ const frontmatter = memory.frontmatter;
8891
+ const currentState = resolveLifecycleState(frontmatter);
8892
+ const heatScore = computeHeat(memory, now, signals);
8893
+ const decayScore = computeDecay(memory, now, signals);
8894
+ const protectedMemory = isProtectedMemory(frontmatter, mergedPolicy);
8895
+ if (currentState === "archived") {
8896
+ return {
8897
+ currentState,
8898
+ nextState: "archived",
8899
+ heatScore,
8900
+ decayScore,
8901
+ changed: false,
8902
+ reason: "archived_is_terminal"
8903
+ };
8904
+ }
8905
+ if (frontmatter.verificationState === "disputed") {
8906
+ const nextState = toTerminalDisputedState(currentState);
8907
+ return {
8908
+ currentState,
8909
+ nextState,
8910
+ heatScore,
8911
+ decayScore,
8912
+ changed: nextState !== currentState,
8913
+ reason: "disputed_memories_do_not_promote_to_active"
8914
+ };
8915
+ }
8916
+ if (decayScore >= mergedPolicy.archiveDecayThreshold && !protectedMemory) {
8917
+ return {
8918
+ currentState,
8919
+ nextState: "archived",
8920
+ heatScore,
8921
+ decayScore,
8922
+ changed: true,
8923
+ reason: "decay_exceeded_archive_threshold"
8924
+ };
8925
+ }
8926
+ if (decayScore >= mergedPolicy.staleDecayThreshold) {
8927
+ return {
8928
+ currentState,
8929
+ nextState: "stale",
8930
+ heatScore,
8931
+ decayScore,
8932
+ changed: currentState !== "stale",
8933
+ reason: "decay_exceeded_stale_threshold"
8934
+ };
8935
+ }
8936
+ if (heatScore >= mergedPolicy.promoteHeatThreshold) {
8937
+ const nextState = isActiveEligible(frontmatter.verificationState) ? "active" : "validated";
8938
+ return {
8939
+ currentState,
8940
+ nextState,
8941
+ heatScore,
8942
+ decayScore,
8943
+ changed: currentState !== nextState,
8944
+ reason: "heat_exceeded_promote_threshold"
8945
+ };
8946
+ }
8947
+ return {
8948
+ currentState,
8949
+ nextState: currentState,
8950
+ heatScore,
8951
+ decayScore,
8952
+ changed: false,
8953
+ reason: "no_transition"
8954
+ };
8955
+ }
8956
+
8755
8957
  // src/temporal-index.ts
8756
8958
  import * as fs2 from "fs";
8757
8959
  import * as path15 from "path";
@@ -9735,6 +9937,42 @@ function filterRecallCandidates(candidates, options) {
9735
9937
  const scopedByNamespace = options.namespacesEnabled ? candidates.filter((r) => options.recallNamespaces.includes(options.resolveNamespace(r.path))) : candidates;
9736
9938
  return scopedByNamespace.filter((r) => !isArtifactMemoryPath(r.path)).slice(0, Math.max(0, options.limit));
9737
9939
  }
9940
+ function hasLifecycleMetadata(frontmatter) {
9941
+ return frontmatter.lifecycleState !== void 0 || frontmatter.verificationState !== void 0 || frontmatter.policyClass !== void 0 || frontmatter.lastValidatedAt !== void 0 || frontmatter.decayScore !== void 0 || frontmatter.heatScore !== void 0;
9942
+ }
9943
+ function shouldFilterLifecycleRecallCandidate(frontmatter, options) {
9944
+ if (!options.lifecyclePolicyEnabled || !options.lifecycleFilterStaleEnabled) return false;
9945
+ if (!hasLifecycleMetadata(frontmatter)) return false;
9946
+ const lifecycleState = resolveLifecycleState(frontmatter);
9947
+ return lifecycleState === "stale" || lifecycleState === "archived";
9948
+ }
9949
+ function lifecycleRecallScoreAdjustment(frontmatter, options) {
9950
+ if (!options.lifecyclePolicyEnabled) return 0;
9951
+ if (!hasLifecycleMetadata(frontmatter)) return 0;
9952
+ let delta = 0;
9953
+ const lifecycleState = resolveLifecycleState(frontmatter);
9954
+ switch (lifecycleState) {
9955
+ case "active":
9956
+ delta += 0.05;
9957
+ break;
9958
+ case "validated":
9959
+ delta += 0.03;
9960
+ break;
9961
+ case "candidate":
9962
+ delta -= 0.01;
9963
+ break;
9964
+ case "stale":
9965
+ delta -= 0.06;
9966
+ break;
9967
+ case "archived":
9968
+ delta -= 0.08;
9969
+ break;
9970
+ }
9971
+ if (frontmatter.verificationState === "disputed") {
9972
+ delta -= 0.12;
9973
+ }
9974
+ return delta;
9975
+ }
9738
9976
  function computeArtifactRecallLimit(recallMode, recallResultLimit, verbatimArtifactsMaxRecall) {
9739
9977
  if (recallMode === "no_recall") return 0;
9740
9978
  if (Math.max(0, recallResultLimit) === 0) return 0;
@@ -11777,6 +12015,14 @@ _Context: ${topQuestion.context}_`);
11777
12015
  }
11778
12016
  }
11779
12017
  }
12018
+ if (this.config.lifecyclePolicyEnabled) {
12019
+ try {
12020
+ const lifecycleCorpus = await this.storage.readAllMemories();
12021
+ await this.runLifecyclePolicyPass(lifecycleCorpus);
12022
+ } catch (err) {
12023
+ log.warn(`lifecycle policy pass failed (ignored): ${err}`);
12024
+ }
12025
+ }
11780
12026
  if (this.config.factArchivalEnabled) {
11781
12027
  const archived = await this.runFactArchival(allMemories);
11782
12028
  if (archived > 0) {
@@ -11838,6 +12084,76 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
11838
12084
  log.info("consolidation complete");
11839
12085
  return { memoriesProcessed: allMemories.length, merged, invalidated };
11840
12086
  }
12087
+ async runLifecyclePolicyPass(allMemories) {
12088
+ const now = /* @__PURE__ */ new Date();
12089
+ const nowIso = now.toISOString();
12090
+ const countsByState = {
12091
+ candidate: 0,
12092
+ validated: 0,
12093
+ active: 0,
12094
+ stale: 0,
12095
+ archived: 0
12096
+ };
12097
+ const transitionCounts = {};
12098
+ let updatedCount = 0;
12099
+ let disputedCount = 0;
12100
+ let evaluatedCount = 0;
12101
+ const policy = {
12102
+ promoteHeatThreshold: this.config.lifecyclePromoteHeatThreshold,
12103
+ staleDecayThreshold: this.config.lifecycleStaleDecayThreshold,
12104
+ archiveDecayThreshold: this.config.lifecycleArchiveDecayThreshold,
12105
+ protectedCategories: this.config.lifecycleProtectedCategories
12106
+ };
12107
+ for (const memory of allMemories) {
12108
+ if (memory.frontmatter.status === "superseded") {
12109
+ continue;
12110
+ }
12111
+ evaluatedCount += 1;
12112
+ const currentState = resolveLifecycleState(memory.frontmatter);
12113
+ const decision = decideLifecycleTransition(memory, policy, now);
12114
+ const nextState = memory.frontmatter.status === "archived" ? "archived" : decision.nextState;
12115
+ countsByState[nextState] += 1;
12116
+ if (memory.frontmatter.verificationState === "disputed") {
12117
+ disputedCount += 1;
12118
+ }
12119
+ if (nextState !== currentState) {
12120
+ const key = `${currentState}->${nextState}`;
12121
+ transitionCounts[key] = (transitionCounts[key] ?? 0) + 1;
12122
+ }
12123
+ const prevHeat = memory.frontmatter.heatScore;
12124
+ const prevDecay = memory.frontmatter.decayScore;
12125
+ const scoreDelta = Math.abs((prevHeat ?? -1) - decision.heatScore) + Math.abs((prevDecay ?? -1) - decision.decayScore);
12126
+ const shouldPersist = memory.frontmatter.lifecycleState !== nextState || memory.frontmatter.heatScore === void 0 || memory.frontmatter.decayScore === void 0 || memory.frontmatter.lastValidatedAt === void 0 || scoreDelta >= 0.01;
12127
+ if (!shouldPersist) continue;
12128
+ const wrote = await this.storage.writeMemoryFrontmatter(memory, {
12129
+ lifecycleState: nextState,
12130
+ heatScore: decision.heatScore,
12131
+ decayScore: decision.decayScore,
12132
+ lastValidatedAt: nowIso
12133
+ });
12134
+ if (wrote) updatedCount += 1;
12135
+ }
12136
+ if (!this.config.lifecycleMetricsEnabled) return;
12137
+ const total = evaluatedCount;
12138
+ const metrics = {
12139
+ generatedAt: nowIso,
12140
+ memoriesEvaluated: total,
12141
+ memoriesUpdated: updatedCount,
12142
+ countsByLifecycleState: countsByState,
12143
+ transitionCounts,
12144
+ staleRatio: total > 0 ? countsByState.stale / total : 0,
12145
+ disputedRatio: total > 0 ? disputedCount / total : 0,
12146
+ policy: {
12147
+ promoteHeatThreshold: this.config.lifecyclePromoteHeatThreshold,
12148
+ staleDecayThreshold: this.config.lifecycleStaleDecayThreshold,
12149
+ archiveDecayThreshold: this.config.lifecycleArchiveDecayThreshold,
12150
+ protectedCategories: this.config.lifecycleProtectedCategories
12151
+ }
12152
+ };
12153
+ const metricsPath = path22.join(this.storage.dir, "state", "lifecycle-metrics.json");
12154
+ await mkdir16(path22.dirname(metricsPath), { recursive: true });
12155
+ await writeFile15(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
12156
+ }
11841
12157
  /**
11842
12158
  * Archive old, low-importance, rarely-accessed facts (v6.0).
11843
12159
  * Moves eligible facts from facts/ to archive/YYYY-MM-DD/.
@@ -12122,10 +12438,19 @@ ${lines.join("\n\n")}`;
12122
12438
  tagCandidates = capSet(rawTags);
12123
12439
  }
12124
12440
  }
12125
- const boosted = results.map((r) => {
12441
+ let lifecycleFilteredCount = 0;
12442
+ const boosted = [];
12443
+ for (const r of results) {
12126
12444
  const memory = memoryByPath.get(r.path);
12127
12445
  let score = r.score;
12128
12446
  if (memory) {
12447
+ if (shouldFilterLifecycleRecallCandidate(memory.frontmatter, {
12448
+ lifecyclePolicyEnabled: this.config.lifecyclePolicyEnabled,
12449
+ lifecycleFilterStaleEnabled: this.config.lifecycleFilterStaleEnabled
12450
+ })) {
12451
+ lifecycleFilteredCount += 1;
12452
+ continue;
12453
+ }
12129
12454
  if (this.config.recencyWeight > 0) {
12130
12455
  const createdAt = new Date(memory.frontmatter.created).getTime();
12131
12456
  const ageMs = now - createdAt;
@@ -12176,9 +12501,15 @@ ${lines.join("\n\n")}`;
12176
12501
  score += 0.06;
12177
12502
  }
12178
12503
  }
12504
+ score += lifecycleRecallScoreAdjustment(memory.frontmatter, {
12505
+ lifecyclePolicyEnabled: this.config.lifecyclePolicyEnabled
12506
+ });
12179
12507
  }
12180
- return { ...r, score };
12181
- });
12508
+ boosted.push({ ...r, score });
12509
+ }
12510
+ if (lifecycleFilteredCount > 0) {
12511
+ log.debug(`lifecycle retrieval filter removed ${lifecycleFilteredCount} stale/archived candidates`);
12512
+ }
12182
12513
  return boosted.sort((a, b) => b.score - a.score);
12183
12514
  }
12184
12515
  /**