@joshuaswarren/openclaw-engram 8.3.5 → 8.3.7

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";
@@ -11777,6 +11979,14 @@ _Context: ${topQuestion.context}_`);
11777
11979
  }
11778
11980
  }
11779
11981
  }
11982
+ if (this.config.lifecyclePolicyEnabled) {
11983
+ try {
11984
+ const lifecycleCorpus = await this.storage.readAllMemories();
11985
+ await this.runLifecyclePolicyPass(lifecycleCorpus);
11986
+ } catch (err) {
11987
+ log.warn(`lifecycle policy pass failed (ignored): ${err}`);
11988
+ }
11989
+ }
11780
11990
  if (this.config.factArchivalEnabled) {
11781
11991
  const archived = await this.runFactArchival(allMemories);
11782
11992
  if (archived > 0) {
@@ -11838,6 +12048,76 @@ ${texts.map((t, i) => `[${i + 1}] ${t}`).join("\n\n")}`;
11838
12048
  log.info("consolidation complete");
11839
12049
  return { memoriesProcessed: allMemories.length, merged, invalidated };
11840
12050
  }
12051
+ async runLifecyclePolicyPass(allMemories) {
12052
+ const now = /* @__PURE__ */ new Date();
12053
+ const nowIso = now.toISOString();
12054
+ const countsByState = {
12055
+ candidate: 0,
12056
+ validated: 0,
12057
+ active: 0,
12058
+ stale: 0,
12059
+ archived: 0
12060
+ };
12061
+ const transitionCounts = {};
12062
+ let updatedCount = 0;
12063
+ let disputedCount = 0;
12064
+ let evaluatedCount = 0;
12065
+ const policy = {
12066
+ promoteHeatThreshold: this.config.lifecyclePromoteHeatThreshold,
12067
+ staleDecayThreshold: this.config.lifecycleStaleDecayThreshold,
12068
+ archiveDecayThreshold: this.config.lifecycleArchiveDecayThreshold,
12069
+ protectedCategories: this.config.lifecycleProtectedCategories
12070
+ };
12071
+ for (const memory of allMemories) {
12072
+ if (memory.frontmatter.status === "superseded") {
12073
+ continue;
12074
+ }
12075
+ evaluatedCount += 1;
12076
+ const currentState = resolveLifecycleState(memory.frontmatter);
12077
+ const decision = decideLifecycleTransition(memory, policy, now);
12078
+ const nextState = memory.frontmatter.status === "archived" ? "archived" : decision.nextState;
12079
+ countsByState[nextState] += 1;
12080
+ if (memory.frontmatter.verificationState === "disputed") {
12081
+ disputedCount += 1;
12082
+ }
12083
+ if (nextState !== currentState) {
12084
+ const key = `${currentState}->${nextState}`;
12085
+ transitionCounts[key] = (transitionCounts[key] ?? 0) + 1;
12086
+ }
12087
+ const prevHeat = memory.frontmatter.heatScore;
12088
+ const prevDecay = memory.frontmatter.decayScore;
12089
+ const scoreDelta = Math.abs((prevHeat ?? -1) - decision.heatScore) + Math.abs((prevDecay ?? -1) - decision.decayScore);
12090
+ const shouldPersist = memory.frontmatter.lifecycleState !== nextState || memory.frontmatter.heatScore === void 0 || memory.frontmatter.decayScore === void 0 || memory.frontmatter.lastValidatedAt === void 0 || scoreDelta >= 0.01;
12091
+ if (!shouldPersist) continue;
12092
+ const wrote = await this.storage.writeMemoryFrontmatter(memory, {
12093
+ lifecycleState: nextState,
12094
+ heatScore: decision.heatScore,
12095
+ decayScore: decision.decayScore,
12096
+ lastValidatedAt: nowIso
12097
+ });
12098
+ if (wrote) updatedCount += 1;
12099
+ }
12100
+ if (!this.config.lifecycleMetricsEnabled) return;
12101
+ const total = evaluatedCount;
12102
+ const metrics = {
12103
+ generatedAt: nowIso,
12104
+ memoriesEvaluated: total,
12105
+ memoriesUpdated: updatedCount,
12106
+ countsByLifecycleState: countsByState,
12107
+ transitionCounts,
12108
+ staleRatio: total > 0 ? countsByState.stale / total : 0,
12109
+ disputedRatio: total > 0 ? disputedCount / total : 0,
12110
+ policy: {
12111
+ promoteHeatThreshold: this.config.lifecyclePromoteHeatThreshold,
12112
+ staleDecayThreshold: this.config.lifecycleStaleDecayThreshold,
12113
+ archiveDecayThreshold: this.config.lifecycleArchiveDecayThreshold,
12114
+ protectedCategories: this.config.lifecycleProtectedCategories
12115
+ }
12116
+ };
12117
+ const metricsPath = path22.join(this.storage.dir, "state", "lifecycle-metrics.json");
12118
+ await mkdir16(path22.dirname(metricsPath), { recursive: true });
12119
+ await writeFile15(metricsPath, JSON.stringify(metrics, null, 2), "utf-8");
12120
+ }
11841
12121
  /**
11842
12122
  * Archive old, low-importance, rarely-accessed facts (v6.0).
11843
12123
  * Moves eligible facts from facts/ to archive/YYYY-MM-DD/.