@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 +282 -2
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +70 -0
- package/package.json +1 -1
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
|
|
5329
|
-
const recency = 1 / (1 +
|
|
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/.
|