anno-extension 0.1.0
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/LICENSE +7 -0
- package/README.md +30 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/dist/browser-fs.d.ts +14 -0
- package/dist/browser-fs.d.ts.map +1 -0
- package/dist/browser-fs.js +69 -0
- package/dist/browser-fs.js.map +1 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +82 -0
- package/dist/config.js.map +1 -0
- package/dist/consolidate-v2.d.ts +15 -0
- package/dist/consolidate-v2.d.ts.map +1 -0
- package/dist/consolidate-v2.js +125 -0
- package/dist/consolidate-v2.js.map +1 -0
- package/dist/consolidation.d.ts +12 -0
- package/dist/consolidation.d.ts.map +1 -0
- package/dist/consolidation.js +199 -0
- package/dist/consolidation.js.map +1 -0
- package/dist/dedup.d.ts +18 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/dedup.js +85 -0
- package/dist/dedup.js.map +1 -0
- package/dist/diagnostics.d.ts +24 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +53 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/embedding-index.d.ts +27 -0
- package/dist/embedding-index.d.ts.map +1 -0
- package/dist/embedding-index.js +111 -0
- package/dist/embedding-index.js.map +1 -0
- package/dist/engine-archive.d.ts +37 -0
- package/dist/engine-archive.d.ts.map +1 -0
- package/dist/engine-archive.js +155 -0
- package/dist/engine-archive.js.map +1 -0
- package/dist/engine-episode-sync.d.ts +19 -0
- package/dist/engine-episode-sync.d.ts.map +1 -0
- package/dist/engine-episode-sync.js +186 -0
- package/dist/engine-episode-sync.js.map +1 -0
- package/dist/engine-injection-text.d.ts +46 -0
- package/dist/engine-injection-text.d.ts.map +1 -0
- package/dist/engine-injection-text.js +271 -0
- package/dist/engine-injection-text.js.map +1 -0
- package/dist/engine-insights.d.ts +22 -0
- package/dist/engine-insights.d.ts.map +1 -0
- package/dist/engine-insights.js +127 -0
- package/dist/engine-insights.js.map +1 -0
- package/dist/engine-links.d.ts +43 -0
- package/dist/engine-links.d.ts.map +1 -0
- package/dist/engine-links.js +223 -0
- package/dist/engine-links.js.map +1 -0
- package/dist/engine-recall-select.d.ts +84 -0
- package/dist/engine-recall-select.d.ts.map +1 -0
- package/dist/engine-recall-select.js +127 -0
- package/dist/engine-recall-select.js.map +1 -0
- package/dist/engine-reinforce.d.ts +18 -0
- package/dist/engine-reinforce.d.ts.map +1 -0
- package/dist/engine-reinforce.js +116 -0
- package/dist/engine-reinforce.js.map +1 -0
- package/dist/engine-scoring-v2.d.ts +35 -0
- package/dist/engine-scoring-v2.d.ts.map +1 -0
- package/dist/engine-scoring-v2.js +178 -0
- package/dist/engine-scoring-v2.js.map +1 -0
- package/dist/engine-v2-mapping.d.ts +17 -0
- package/dist/engine-v2-mapping.d.ts.map +1 -0
- package/dist/engine-v2-mapping.js +190 -0
- package/dist/engine-v2-mapping.js.map +1 -0
- package/dist/engine.d.ts +339 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1964 -0
- package/dist/engine.js.map +1 -0
- package/dist/eviction.d.ts +14 -0
- package/dist/eviction.d.ts.map +1 -0
- package/dist/eviction.js +26 -0
- package/dist/eviction.js.map +1 -0
- package/dist/extraction.d.ts +11 -0
- package/dist/extraction.d.ts.map +1 -0
- package/dist/extraction.js +261 -0
- package/dist/extraction.js.map +1 -0
- package/dist/full-insights-html.d.ts +9 -0
- package/dist/full-insights-html.d.ts.map +1 -0
- package/dist/full-insights-html.js +422 -0
- package/dist/full-insights-html.js.map +1 -0
- package/dist/full-insights-sections.d.ts +48 -0
- package/dist/full-insights-sections.d.ts.map +1 -0
- package/dist/full-insights-sections.js +231 -0
- package/dist/full-insights-sections.js.map +1 -0
- package/dist/full-insights.d.ts +22 -0
- package/dist/full-insights.d.ts.map +1 -0
- package/dist/full-insights.js +370 -0
- package/dist/full-insights.js.map +1 -0
- package/dist/hash-embedding.d.ts +9 -0
- package/dist/hash-embedding.d.ts.map +1 -0
- package/dist/hash-embedding.js +44 -0
- package/dist/hash-embedding.js.map +1 -0
- package/dist/human-insights.d.ts +28 -0
- package/dist/human-insights.d.ts.map +1 -0
- package/dist/human-insights.js +260 -0
- package/dist/human-insights.js.map +1 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +367 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/insights-html.d.ts +9 -0
- package/dist/insights-html.d.ts.map +1 -0
- package/dist/insights-html.js +432 -0
- package/dist/insights-html.js.map +1 -0
- package/dist/linking.d.ts +22 -0
- package/dist/linking.d.ts.map +1 -0
- package/dist/linking.js +192 -0
- package/dist/linking.js.map +1 -0
- package/dist/llm-json.d.ts +10 -0
- package/dist/llm-json.d.ts.map +1 -0
- package/dist/llm-json.js +193 -0
- package/dist/llm-json.js.map +1 -0
- package/dist/privacy.d.ts +17 -0
- package/dist/privacy.d.ts.map +1 -0
- package/dist/privacy.js +53 -0
- package/dist/privacy.js.map +1 -0
- package/dist/procedural-v2.d.ts +9 -0
- package/dist/procedural-v2.d.ts.map +1 -0
- package/dist/procedural-v2.js +123 -0
- package/dist/procedural-v2.js.map +1 -0
- package/dist/reconsolidate-v2.d.ts +19 -0
- package/dist/reconsolidate-v2.d.ts.map +1 -0
- package/dist/reconsolidate-v2.js +115 -0
- package/dist/reconsolidate-v2.js.map +1 -0
- package/dist/scoring.d.ts +32 -0
- package/dist/scoring.d.ts.map +1 -0
- package/dist/scoring.js +108 -0
- package/dist/scoring.js.map +1 -0
- package/dist/store-v2.d.ts +35 -0
- package/dist/store-v2.d.ts.map +1 -0
- package/dist/store-v2.js +98 -0
- package/dist/store-v2.js.map +1 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +116 -0
- package/dist/store.js.map +1 -0
- package/dist/turn-context.d.ts +37 -0
- package/dist/turn-context.d.ts.map +1 -0
- package/dist/turn-context.js +31 -0
- package/dist/turn-context.js.map +1 -0
- package/dist/types-v2.d.ts +190 -0
- package/dist/types-v2.d.ts.map +1 -0
- package/dist/types-v2.js +8 -0
- package/dist/types-v2.js.map +1 -0
- package/dist/types.d.ts +330 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/update.d.ts +31 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +250 -0
- package/dist/update.js.map +1 -0
- package/package.json +34 -0
- package/src/browser-fs.ts +67 -0
- package/src/config.ts +138 -0
- package/src/consolidate-v2.ts +158 -0
- package/src/consolidation.ts +229 -0
- package/src/dedup.ts +91 -0
- package/src/diagnostics.ts +81 -0
- package/src/embedding-index.ts +138 -0
- package/src/engine-archive.ts +167 -0
- package/src/engine-episode-sync.ts +237 -0
- package/src/engine-injection-text.ts +334 -0
- package/src/engine-insights.ts +182 -0
- package/src/engine-links.ts +311 -0
- package/src/engine-recall-select.ts +298 -0
- package/src/engine-reinforce.ts +154 -0
- package/src/engine-scoring-v2.ts +188 -0
- package/src/engine-v2-mapping.ts +204 -0
- package/src/engine.ts +2499 -0
- package/src/eviction.ts +38 -0
- package/src/extraction.ts +300 -0
- package/src/full-insights-html.ts +474 -0
- package/src/full-insights-sections.ts +305 -0
- package/src/full-insights.ts +423 -0
- package/src/hash-embedding.ts +48 -0
- package/src/human-insights.ts +345 -0
- package/src/i18n.ts +455 -0
- package/src/index.ts +117 -0
- package/src/insights-html.ts +478 -0
- package/src/linking.ts +228 -0
- package/src/llm-json.ts +194 -0
- package/src/privacy.ts +54 -0
- package/src/procedural-v2.ts +142 -0
- package/src/reconsolidate-v2.ts +145 -0
- package/src/scoring.ts +156 -0
- package/src/store-v2.ts +138 -0
- package/src/store.ts +137 -0
- package/src/turn-context.ts +56 -0
- package/src/types-v2.ts +244 -0
- package/src/types.ts +372 -0
- package/src/update.ts +271 -0
package/src/engine.ts
ADDED
|
@@ -0,0 +1,2499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: NanoMemEngine class - unified facade for memory CRUD, injection, consolidation; delegates to engine-* modules
|
|
3
|
+
* [FROM]: Uses browser-fs.ts and the NanoMem core modules below
|
|
4
|
+
* [TO]: Consumed by packages/mem-core/src/index.ts, extension.ts, cli.ts, test/*
|
|
5
|
+
* [HERE]: packages/mem-core/src/engine.ts - facade layer composing all memory subsystems via thin delegation wrappers
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cp, mkdir, readdir, join } from "./browser-fs.js";
|
|
9
|
+
import { getConfig, type NanomemConfig } from "./config.js";
|
|
10
|
+
import { consolidateEpisodes } from "./consolidation.js";
|
|
11
|
+
import { consolidateV2Memories } from "./consolidate-v2.js";
|
|
12
|
+
import {
|
|
13
|
+
queryEmbeddingIndex,
|
|
14
|
+
syncEmbeddingIndex,
|
|
15
|
+
type EmbeddingSourceItem,
|
|
16
|
+
loadEmbeddingIndex,
|
|
17
|
+
saveEmbeddingIndex,
|
|
18
|
+
} from "./embedding-index.js";
|
|
19
|
+
import { utilityEntry, utilityWork } from "./eviction.js";
|
|
20
|
+
import { extractMemories, extractWork } from "./extraction.js";
|
|
21
|
+
import { createHashedEmbeddingFn } from "./hash-embedding.js";
|
|
22
|
+
import type { PromptSet } from "./i18n.js";
|
|
23
|
+
import { PROMPTS } from "./i18n.js";
|
|
24
|
+
import {
|
|
25
|
+
getGraphContextSummaries,
|
|
26
|
+
getGraphNeighborhoodBySeeds,
|
|
27
|
+
linkNewEntry,
|
|
28
|
+
reinforceRelations,
|
|
29
|
+
type GraphNeighbor,
|
|
30
|
+
} from "./linking.js";
|
|
31
|
+
import { parseLlmJson } from "./llm-json.js";
|
|
32
|
+
import { evictExpiredEntries, evictExpiredWork, filterByScope, filterPII } from "./privacy.js";
|
|
33
|
+
import { daysSince, extractTags, pickTop, scoreEntry, scoreEpisode, scoreWorkEntry, tagOverlap, tierEntries } from "./scoring.js";
|
|
34
|
+
import {
|
|
35
|
+
loadEntries,
|
|
36
|
+
loadEpisodes,
|
|
37
|
+
loadMeta,
|
|
38
|
+
loadWork,
|
|
39
|
+
saveEpisode as persistEpisode,
|
|
40
|
+
saveEntries,
|
|
41
|
+
saveWork,
|
|
42
|
+
writeJson,
|
|
43
|
+
} from "./store.js";
|
|
44
|
+
import { deduplicateMemoryEntries, deduplicateWorkEntries } from "./dedup.js";
|
|
45
|
+
import { buildFullInsightsReport } from "./full-insights.js";
|
|
46
|
+
import { generateHumanInsights } from "./human-insights.js";
|
|
47
|
+
import { reconsolidateV2Memories } from "./reconsolidate-v2.js";
|
|
48
|
+
import type {
|
|
49
|
+
DeveloperPersona,
|
|
50
|
+
Episode,
|
|
51
|
+
ExtractedItem,
|
|
52
|
+
FullInsightsReport,
|
|
53
|
+
HumanInsight,
|
|
54
|
+
InsightsReport,
|
|
55
|
+
AlignmentSnapshot,
|
|
56
|
+
LlmFn,
|
|
57
|
+
MemoryEntry,
|
|
58
|
+
MemoryScope,
|
|
59
|
+
Meta,
|
|
60
|
+
RootCauseInsight,
|
|
61
|
+
WorkEntry,
|
|
62
|
+
} from "./types.js";
|
|
63
|
+
import {
|
|
64
|
+
getV2Paths,
|
|
65
|
+
loadV2Episodes,
|
|
66
|
+
loadV2Facets,
|
|
67
|
+
loadV2Links,
|
|
68
|
+
loadV2Meta,
|
|
69
|
+
loadV2Procedural,
|
|
70
|
+
loadV2Semantic,
|
|
71
|
+
loadV2State,
|
|
72
|
+
saveV2Episodes,
|
|
73
|
+
saveV2Facets,
|
|
74
|
+
saveV2Links,
|
|
75
|
+
saveV2Meta,
|
|
76
|
+
saveV2Procedural,
|
|
77
|
+
saveV2Semantic,
|
|
78
|
+
type NanoMemV2Paths,
|
|
79
|
+
} from "./store-v2.js";
|
|
80
|
+
import { compileProcedureFromEpisode } from "./procedural-v2.js";
|
|
81
|
+
import type { EmbeddingFn, EpisodeFacet, EpisodeMemory, FacetKind, MemoryLink, ProceduralMemory, SemanticMemory } from "./types-v2.js";
|
|
82
|
+
|
|
83
|
+
type RememberableMemoryItem = Omit<ExtractedItem, "type"> & {
|
|
84
|
+
type: Exclude<ExtractedItem["type"], "retract">;
|
|
85
|
+
};
|
|
86
|
+
import { applyExtraction, checkConsolidationEntry, checkWorkDuplicate } from "./update.js";
|
|
87
|
+
import {
|
|
88
|
+
computeStructuralBoost,
|
|
89
|
+
currentStructuralAnchor,
|
|
90
|
+
scoreEpisodeFacet as scoreEpisodeFacetFn,
|
|
91
|
+
scoreEpisodeMemory as scoreEpisodeMemoryFn,
|
|
92
|
+
scoreProceduralMemory as scoreProceduralMemoryFn,
|
|
93
|
+
scoreV2SemanticMemory as scoreV2SemanticMemoryFn,
|
|
94
|
+
breakdownEpisodeMemory,
|
|
95
|
+
breakdownEpisodeFacet,
|
|
96
|
+
breakdownV2Semantic,
|
|
97
|
+
breakdownProcedural,
|
|
98
|
+
} from "./engine-scoring-v2.js";
|
|
99
|
+
import {
|
|
100
|
+
CONVERSATION_PREFERENCE_PATTERNS,
|
|
101
|
+
buildInjectedMemoryOrder,
|
|
102
|
+
buildProgressiveInjectionText,
|
|
103
|
+
isConversationPreference as isConversationPreferenceFn,
|
|
104
|
+
mergeUniqueEntries as mergeUniqueEntriesFn,
|
|
105
|
+
selectConversationPreferences as selectConversationPreferencesFn,
|
|
106
|
+
rankConversationPreference as rankConversationPreferenceFn,
|
|
107
|
+
} from "./engine-injection-text.js";
|
|
108
|
+
import {
|
|
109
|
+
inferSemanticRetention as inferSemanticRetentionFn,
|
|
110
|
+
inferSemanticStability as inferSemanticStabilityFn,
|
|
111
|
+
inferSemanticImportance as inferSemanticImportanceFn,
|
|
112
|
+
mapExtractedItemToSemanticType as mapExtractedItemToSemanticTypeFn,
|
|
113
|
+
upsertSemanticFromExtractedItem as upsertSemanticFromExtractedItemFn,
|
|
114
|
+
semanticKindToLegacyType as semanticKindToLegacyTypeFn,
|
|
115
|
+
semanticToRuntimeEntry as semanticToRuntimeEntryFn,
|
|
116
|
+
proceduralToRuntimeEntry as proceduralToRuntimeEntryFn,
|
|
117
|
+
} from "./engine-v2-mapping.js";
|
|
118
|
+
import {
|
|
119
|
+
mergeArchivedEntries as mergeArchivedEntriesFn,
|
|
120
|
+
mergeArchivedWork as mergeArchivedWorkFn,
|
|
121
|
+
mergeArchivedV2 as mergeArchivedV2Fn,
|
|
122
|
+
partitionArchivedEntries as partitionArchivedEntriesFn,
|
|
123
|
+
partitionArchivedWork as partitionArchivedWorkFn,
|
|
124
|
+
partitionArchivedSemantic as partitionArchivedSemanticFn,
|
|
125
|
+
partitionArchivedProcedural as partitionArchivedProceduralFn,
|
|
126
|
+
type ForgettingConfig,
|
|
127
|
+
} from "./engine-archive.js";
|
|
128
|
+
import {
|
|
129
|
+
AUTO_V2_LINK_PREFIX,
|
|
130
|
+
buildProceduralChains as buildProceduralChainsFn,
|
|
131
|
+
materializeV2Links as materializeV2LinksFn,
|
|
132
|
+
detectProceduralConflicts as detectProceduralConflictsFn,
|
|
133
|
+
detectSemanticConflicts as detectSemanticConflictsFn,
|
|
134
|
+
detectAlignmentConflicts as detectAlignmentConflictsFn,
|
|
135
|
+
suggestConflictAction as suggestConflictActionFn,
|
|
136
|
+
explainConflictAction as explainConflictActionFn,
|
|
137
|
+
} from "./engine-links.js";
|
|
138
|
+
import { generateInsightsReport } from "./engine-insights.js";
|
|
139
|
+
import { makeEpisodeMemoryId, mapEpisodeToV2, syncEpisodeToV2 } from "./engine-episode-sync.js";
|
|
140
|
+
import {
|
|
141
|
+
reinforceProcedural as reinforceProceduralFn,
|
|
142
|
+
reinforceEpisodeMemories as reinforceEpisodeMemoriesFn,
|
|
143
|
+
reinforceEpisodeFacets as reinforceEpisodeFacetsFn,
|
|
144
|
+
reinforceV2SemanticMemories as reinforceV2SemanticMemoriesFn,
|
|
145
|
+
reinforceWork as reinforceWorkFn,
|
|
146
|
+
reconsolidateV2AfterRecall as reconsolidateV2AfterRecallFn,
|
|
147
|
+
reconsolidateIfNeeded as reconsolidateIfNeededFn,
|
|
148
|
+
} from "./engine-reinforce.js";
|
|
149
|
+
import { selectRecallEntries } from "./engine-recall-select.js";
|
|
150
|
+
import { setTurnContext, type MemoryRecallRecord } from "./turn-context.js";
|
|
151
|
+
|
|
152
|
+
export class NanoMemEngine {
|
|
153
|
+
readonly cfg: NanomemConfig;
|
|
154
|
+
private llmFn?: LlmFn;
|
|
155
|
+
private embeddingFn?: EmbeddingFn;
|
|
156
|
+
private static readonly AUTO_V2_LINK_PREFIX = AUTO_V2_LINK_PREFIX;
|
|
157
|
+
private static readonly AUTO_REVIVE_MAX_ITEMS = 2;
|
|
158
|
+
private static readonly CONVERSATION_PREFERENCE_PATTERNS = CONVERSATION_PREFERENCE_PATTERNS;
|
|
159
|
+
|
|
160
|
+
private knowledgePath: string;
|
|
161
|
+
private lessonsPath: string;
|
|
162
|
+
private eventsPath: string;
|
|
163
|
+
private preferencesPath: string;
|
|
164
|
+
private facetsPath: string;
|
|
165
|
+
private workPath: string;
|
|
166
|
+
private metaPath: string;
|
|
167
|
+
private episodesDir: string;
|
|
168
|
+
private v2Paths: NanoMemV2Paths;
|
|
169
|
+
private archiveKnowledgePath: string;
|
|
170
|
+
private archiveLessonsPath: string;
|
|
171
|
+
private archiveEventsPath: string;
|
|
172
|
+
private archivePreferencesPath: string;
|
|
173
|
+
private archiveFacetsPath: string;
|
|
174
|
+
private archiveWorkPath: string;
|
|
175
|
+
private archiveV2Paths: NanoMemV2Paths;
|
|
176
|
+
|
|
177
|
+
constructor(overrides?: Partial<NanomemConfig>, llmFn?: LlmFn) {
|
|
178
|
+
this.cfg = getConfig(overrides);
|
|
179
|
+
this.llmFn = llmFn;
|
|
180
|
+
if (this.cfg.embeddings.enabled) {
|
|
181
|
+
this.embeddingFn = createHashedEmbeddingFn(this.cfg.embeddings.dim);
|
|
182
|
+
}
|
|
183
|
+
this.knowledgePath = join(this.cfg.memoryDir, "knowledge.json");
|
|
184
|
+
this.lessonsPath = join(this.cfg.memoryDir, "lessons.json");
|
|
185
|
+
this.eventsPath = join(this.cfg.memoryDir, "events.json");
|
|
186
|
+
this.preferencesPath = join(this.cfg.memoryDir, "preferences.json");
|
|
187
|
+
this.facetsPath = join(this.cfg.memoryDir, "facets.json");
|
|
188
|
+
this.workPath = join(this.cfg.memoryDir, "work.json");
|
|
189
|
+
this.metaPath = join(this.cfg.memoryDir, "meta.json");
|
|
190
|
+
this.episodesDir = join(this.cfg.memoryDir, "episodes");
|
|
191
|
+
this.v2Paths = getV2Paths(this.cfg.memoryDir);
|
|
192
|
+
const archiveDir = join(this.cfg.memoryDir, "_archive");
|
|
193
|
+
this.archiveKnowledgePath = join(archiveDir, "knowledge.json");
|
|
194
|
+
this.archiveLessonsPath = join(archiveDir, "lessons.json");
|
|
195
|
+
this.archiveEventsPath = join(archiveDir, "events.json");
|
|
196
|
+
this.archivePreferencesPath = join(archiveDir, "preferences.json");
|
|
197
|
+
this.archiveFacetsPath = join(archiveDir, "facets.json");
|
|
198
|
+
this.archiveWorkPath = join(archiveDir, "work.json");
|
|
199
|
+
this.archiveV2Paths = getV2Paths(archiveDir);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setLlmFn(fn: LlmFn): void {
|
|
203
|
+
this.llmFn = fn;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
setEmbeddingFn(fn: EmbeddingFn): void {
|
|
207
|
+
this.embeddingFn = fn;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── Extraction ──────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
async extractAndStore(conversation: string, project: string): Promise<ExtractedItem[]> {
|
|
213
|
+
const items = await extractMemories(conversation, this.cfg, this.llmFn);
|
|
214
|
+
if (!items.length) return [];
|
|
215
|
+
|
|
216
|
+
const knowledge = await loadEntries(this.knowledgePath);
|
|
217
|
+
const lessons = await loadEntries(this.lessonsPath);
|
|
218
|
+
const events = await loadEntries(this.eventsPath);
|
|
219
|
+
const prefs = await loadEntries(this.preferencesPath);
|
|
220
|
+
const facets = await loadEntries(this.facetsPath);
|
|
221
|
+
const v2Semantic = await loadV2Semantic(this.v2Paths);
|
|
222
|
+
|
|
223
|
+
for (const item of items) {
|
|
224
|
+
const target =
|
|
225
|
+
item.type === "lesson"
|
|
226
|
+
? lessons
|
|
227
|
+
: item.type === "event"
|
|
228
|
+
? events
|
|
229
|
+
: item.type === "preference"
|
|
230
|
+
? prefs
|
|
231
|
+
: item.type === "pattern" || item.type === "struggle"
|
|
232
|
+
? facets
|
|
233
|
+
: knowledge;
|
|
234
|
+
applyExtraction(target, item, project, this.cfg);
|
|
235
|
+
this.upsertSemanticFromExtractedItem(v2Semantic, item, project);
|
|
236
|
+
const stored = target.find(entry => entry.name === item.name && entry.summary === item.summary);
|
|
237
|
+
const mirror = v2Semantic.find(entry => entry.name === item.name && entry.summary === item.summary && entry.scope?.project === project);
|
|
238
|
+
if (stored && mirror) mirror.legacyIds = [...new Set([...(mirror.legacyIds ?? []), stored.id])];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const hl = this.cfg.halfLife;
|
|
242
|
+
const ew = this.cfg.evictionWeights;
|
|
243
|
+
await Promise.all([
|
|
244
|
+
saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
|
|
245
|
+
saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
|
|
246
|
+
saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
|
|
247
|
+
saveEntries(this.preferencesPath, prefs, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
|
|
248
|
+
saveEntries(this.facetsPath, facets, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
|
|
249
|
+
saveV2Semantic(this.v2Paths, v2Semantic),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
return items;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async remember(item: RememberableMemoryItem, project: string): Promise<MemoryEntry | null> {
|
|
256
|
+
const knowledge = await loadEntries(this.knowledgePath);
|
|
257
|
+
const lessons = await loadEntries(this.lessonsPath);
|
|
258
|
+
const events = await loadEntries(this.eventsPath);
|
|
259
|
+
const prefs = await loadEntries(this.preferencesPath);
|
|
260
|
+
const facets = await loadEntries(this.facetsPath);
|
|
261
|
+
const v2Semantic = await loadV2Semantic(this.v2Paths);
|
|
262
|
+
let target = knowledge;
|
|
263
|
+
if (item.type === "lesson") target = lessons;
|
|
264
|
+
if (item.type === "event") target = events;
|
|
265
|
+
if (item.type === "preference") target = prefs;
|
|
266
|
+
if (item.type === "pattern" || item.type === "struggle") target = facets;
|
|
267
|
+
applyExtraction(target, item, project, this.cfg);
|
|
268
|
+
this.upsertSemanticFromExtractedItem(v2Semantic, item, project);
|
|
269
|
+
const stored = target.find(entry => entry.name === item.name && entry.summary === item.summary);
|
|
270
|
+
const mirror = v2Semantic.find(entry => entry.name === item.name && entry.summary === item.summary && entry.scope?.project === project);
|
|
271
|
+
if (stored && mirror) mirror.legacyIds = [...new Set([...(mirror.legacyIds ?? []), stored.id])];
|
|
272
|
+
|
|
273
|
+
const hl = this.cfg.halfLife;
|
|
274
|
+
const ew = this.cfg.evictionWeights;
|
|
275
|
+
await Promise.all([
|
|
276
|
+
saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
|
|
277
|
+
saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
|
|
278
|
+
saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
|
|
279
|
+
saveEntries(this.preferencesPath, prefs, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
|
|
280
|
+
saveEntries(this.facetsPath, facets, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
|
|
281
|
+
saveV2Semantic(this.v2Paths, v2Semantic),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
return target.find((entry) => entry.name === item.name || entry.summary === item.summary) ?? target.at(-1) ?? null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async extractAndStoreWork(conversation: string, project: string, sessionGoal?: string): Promise<void> {
|
|
288
|
+
const extracted = await extractWork(conversation, this.cfg, this.llmFn);
|
|
289
|
+
if (!extracted || (!extracted.goal && !extracted.summary)) return;
|
|
290
|
+
|
|
291
|
+
const entries = await loadWork(this.workPath);
|
|
292
|
+
const now = new Date().toISOString();
|
|
293
|
+
const newWork: WorkEntry = {
|
|
294
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
295
|
+
goal: sessionGoal || extracted.goal,
|
|
296
|
+
summary: filterPII(extracted.summary),
|
|
297
|
+
detail: extracted.detail ? filterPII(extracted.detail) : undefined,
|
|
298
|
+
project,
|
|
299
|
+
tags: extractTags(`${extracted.goal} ${extracted.summary}`),
|
|
300
|
+
importance: 6,
|
|
301
|
+
strength: this.cfg.halfLife.work ?? 45,
|
|
302
|
+
created: now,
|
|
303
|
+
eventTime: now,
|
|
304
|
+
accessCount: 0,
|
|
305
|
+
relatedIds: [],
|
|
306
|
+
ttl: this.cfg.forgetting.workTtlDays,
|
|
307
|
+
scope: this.cfg.defaultScope,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const result = checkWorkDuplicate(entries, newWork);
|
|
311
|
+
if (result.action === "skip") return;
|
|
312
|
+
if (result.action === "update" && result.index !== undefined) {
|
|
313
|
+
const existing = entries[result.index]!;
|
|
314
|
+
entries[result.index] = {
|
|
315
|
+
...existing,
|
|
316
|
+
goal: newWork.goal,
|
|
317
|
+
summary: newWork.summary,
|
|
318
|
+
detail: newWork.detail ?? existing.detail,
|
|
319
|
+
tags: newWork.tags,
|
|
320
|
+
lastAccessed: now,
|
|
321
|
+
};
|
|
322
|
+
} else {
|
|
323
|
+
entries.push(newWork);
|
|
324
|
+
}
|
|
325
|
+
await saveWork(this.workPath, entries, this.cfg.maxEntries.work, (w) =>
|
|
326
|
+
utilityWork(w, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Episode Management ──────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
async saveEpisode(ep: Episode): Promise<void> {
|
|
333
|
+
const existing = (await loadEpisodes(this.episodesDir)).find(item => item.sessionId === ep.sessionId);
|
|
334
|
+
if (existing) ep = existing;
|
|
335
|
+
else await persistEpisode(this.episodesDir, ep);
|
|
336
|
+
const meta = await loadMeta(this.metaPath);
|
|
337
|
+
meta.totalSessions = (await loadEpisodes(this.episodesDir)).length;
|
|
338
|
+
await writeJson(this.metaPath, meta);
|
|
339
|
+
await this.syncEpisodeToV2(ep);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getV2EpisodeMemories(): Promise<EpisodeMemory[]> {
|
|
343
|
+
return loadV2Episodes(this.v2Paths);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async getV2Snapshot(): Promise<{
|
|
347
|
+
episodes: EpisodeMemory[];
|
|
348
|
+
facets: EpisodeFacet[];
|
|
349
|
+
semantic: import("./types-v2.js").SemanticMemory[];
|
|
350
|
+
procedural: ProceduralMemory[];
|
|
351
|
+
links: MemoryLink[];
|
|
352
|
+
}> {
|
|
353
|
+
const [episodes, facets, semantic, procedural, links] = await Promise.all([
|
|
354
|
+
loadV2Episodes(this.v2Paths),
|
|
355
|
+
loadV2Facets(this.v2Paths),
|
|
356
|
+
loadV2Semantic(this.v2Paths),
|
|
357
|
+
loadV2Procedural(this.v2Paths),
|
|
358
|
+
loadV2Links(this.v2Paths),
|
|
359
|
+
]);
|
|
360
|
+
return { episodes, facets, semantic, procedural, links };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async searchV2Memories(
|
|
364
|
+
query: string,
|
|
365
|
+
limit = 10,
|
|
366
|
+
scope?: MemoryScope,
|
|
367
|
+
): Promise<Array<{ kind: "episode" | "facet" | "semantic" | "procedural"; id: string; title: string; summary: string; score: number }>> {
|
|
368
|
+
await this.autoReviveRelevantArchive(query, scope);
|
|
369
|
+
const snapshot = await this.getV2Snapshot();
|
|
370
|
+
const project = (scope as { project?: string } | undefined)?.project ?? "";
|
|
371
|
+
const episodes = this.filterAndCleanV2Episodes(snapshot.episodes, project, scope);
|
|
372
|
+
const facets = this.filterAndCleanV2Facets(snapshot.facets, project, scope);
|
|
373
|
+
const semantic = this.filterAndCleanV2Semantic(snapshot.semantic, project, scope);
|
|
374
|
+
const procedural = this.filterAndCleanProcedural(snapshot.procedural, project, scope);
|
|
375
|
+
const matches = await this.querySemanticCandidates(query, project, scope, episodes, facets, semantic, procedural);
|
|
376
|
+
const episodeById = new Map(episodes.map((entry) => [entry.id, entry]));
|
|
377
|
+
const facetById = new Map(facets.map((entry) => [entry.id, entry]));
|
|
378
|
+
const semanticById = new Map(semantic.map((entry) => [entry.id, entry]));
|
|
379
|
+
const proceduralById = new Map(procedural.map((entry) => [entry.id, entry]));
|
|
380
|
+
const results: Array<{ kind: "episode" | "facet" | "semantic" | "procedural"; id: string; title: string; summary: string; score: number }> = [];
|
|
381
|
+
for (const match of matches.slice(0, limit)) {
|
|
382
|
+
if (match.memoryKind === "episode") {
|
|
383
|
+
const entry = episodeById.get(match.memoryId);
|
|
384
|
+
if (entry) {
|
|
385
|
+
results.push({ kind: "episode", id: entry.id, title: entry.title || "Episode", summary: entry.summary, score: match.score });
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (match.memoryKind === "facet") {
|
|
390
|
+
const entry = facetById.get(match.memoryId);
|
|
391
|
+
if (entry) {
|
|
392
|
+
results.push({ kind: "facet", id: entry.id, title: entry.searchText, summary: entry.summary || entry.anchorText || "", score: match.score });
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (match.memoryKind === "semantic") {
|
|
397
|
+
const entry = semanticById.get(match.memoryId);
|
|
398
|
+
if (entry) {
|
|
399
|
+
results.push({ kind: "semantic", id: entry.id, title: entry.name, summary: entry.summary, score: match.score });
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const entry = proceduralById.get(match.memoryId);
|
|
404
|
+
if (entry) {
|
|
405
|
+
results.push({ kind: "procedural", id: entry.id, title: entry.name, summary: entry.summary, score: match.score });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return results;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async syncV2Embeddings(model = this.cfg.embeddings.model): Promise<number> {
|
|
412
|
+
if (!this.embeddingFn) return 0;
|
|
413
|
+
const snapshot = await this.getV2Snapshot();
|
|
414
|
+
const items = this.buildEmbeddingSourceItems(snapshot.episodes, snapshot.facets, snapshot.semantic, snapshot.procedural);
|
|
415
|
+
await syncEmbeddingIndex(this.cfg.memoryDir, model, items, this.embeddingFn);
|
|
416
|
+
const meta = await loadV2Meta(this.v2Paths);
|
|
417
|
+
await saveV2Meta(this.v2Paths, {
|
|
418
|
+
...meta,
|
|
419
|
+
lastEmbeddingSyncAt: new Date().toISOString(),
|
|
420
|
+
});
|
|
421
|
+
return items.length;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async consolidate(): Promise<MemoryEntry[]> {
|
|
425
|
+
const result = await this.consolidateDetailed();
|
|
426
|
+
return result.entries;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async consolidateDetailed(options?: { signal?: AbortSignal }): Promise<{
|
|
430
|
+
entries: MemoryEntry[];
|
|
431
|
+
stats: { episodesConsidered: number; added: number; updated: number; skipped: number };
|
|
432
|
+
}> {
|
|
433
|
+
const episodes = await loadEpisodes(this.episodesDir);
|
|
434
|
+
const unconsolidatedCount = episodes.filter((ep) => !ep.consolidated).length;
|
|
435
|
+
|
|
436
|
+
if (options?.signal?.aborted) {
|
|
437
|
+
throw new Error("AbortError");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let newEntries: MemoryEntry[] = [];
|
|
441
|
+
try {
|
|
442
|
+
newEntries = await consolidateEpisodes(episodes, this.cfg, this.llmFn, options);
|
|
443
|
+
} catch (e) {
|
|
444
|
+
if (e instanceof Error && e.message === "AbortError") throw e;
|
|
445
|
+
throw e;
|
|
446
|
+
}
|
|
447
|
+
if (!newEntries.length) {
|
|
448
|
+
return { entries: [], stats: { episodesConsidered: unconsolidatedCount, added: 0, updated: 0, skipped: 0 } };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const knowledge = await loadEntries(this.knowledgePath);
|
|
452
|
+
const lessons = await loadEntries(this.lessonsPath);
|
|
453
|
+
const events = await loadEntries(this.eventsPath);
|
|
454
|
+
const [v2Episodes, v2Facets, v2Procedural, v2Semantic] = await Promise.all([
|
|
455
|
+
loadV2Episodes(this.v2Paths),
|
|
456
|
+
loadV2Facets(this.v2Paths),
|
|
457
|
+
loadV2Procedural(this.v2Paths),
|
|
458
|
+
loadV2Semantic(this.v2Paths),
|
|
459
|
+
]);
|
|
460
|
+
const v2Consolidated = consolidateV2Memories(v2Episodes, v2Facets, v2Procedural, this.cfg);
|
|
461
|
+
newEntries = [...newEntries, ...v2Consolidated.entries];
|
|
462
|
+
const allExisting = [...knowledge, ...lessons, ...events];
|
|
463
|
+
|
|
464
|
+
let added = 0;
|
|
465
|
+
let updated = 0;
|
|
466
|
+
let skipped = 0;
|
|
467
|
+
|
|
468
|
+
for (const entry of newEntries) {
|
|
469
|
+
if (options?.signal?.aborted) {
|
|
470
|
+
throw new Error("AbortError");
|
|
471
|
+
}
|
|
472
|
+
const target = entry.type === "lesson" ? lessons : entry.type === "event" ? events : knowledge;
|
|
473
|
+
const result = checkConsolidationEntry(target, entry, allExisting);
|
|
474
|
+
if (result.action === "skip") {
|
|
475
|
+
skipped++;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (result.action === "update" && result.index !== undefined) {
|
|
479
|
+
updated++;
|
|
480
|
+
const existing = target[result.index]!;
|
|
481
|
+
target[result.index] = {
|
|
482
|
+
...existing,
|
|
483
|
+
name: entry.name,
|
|
484
|
+
summary: entry.summary,
|
|
485
|
+
detail: entry.detail,
|
|
486
|
+
content: entry.detail,
|
|
487
|
+
tags: entry.tags,
|
|
488
|
+
lastAccessed: new Date().toISOString(),
|
|
489
|
+
};
|
|
490
|
+
} else {
|
|
491
|
+
added++;
|
|
492
|
+
linkNewEntry(entry, allExisting);
|
|
493
|
+
target.push(entry);
|
|
494
|
+
allExisting.push(entry);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (options?.signal?.aborted) {
|
|
499
|
+
throw new Error("AbortError");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const hl = this.cfg.halfLife;
|
|
503
|
+
const ew = this.cfg.evictionWeights;
|
|
504
|
+
await Promise.all([
|
|
505
|
+
saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
|
|
506
|
+
saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
|
|
507
|
+
saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
const nextSemantic = [...v2Semantic];
|
|
511
|
+
for (const item of v2Consolidated.semantic) {
|
|
512
|
+
if (!nextSemantic.some((existing) => tagOverlap(existing.tags, item.tags) >= 0.8 && existing.semanticType === item.semanticType)) {
|
|
513
|
+
nextSemantic.push(item);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const episode of v2Episodes) {
|
|
517
|
+
const derived = v2Consolidated.episodeSemanticMap.get(episode.id);
|
|
518
|
+
if (!derived?.length) continue;
|
|
519
|
+
episode.derivedSemanticIds = [...new Set([...(episode.derivedSemanticIds ?? []), ...derived])];
|
|
520
|
+
episode.consolidatedAt = episode.consolidatedAt ?? new Date().toISOString();
|
|
521
|
+
}
|
|
522
|
+
await Promise.all([
|
|
523
|
+
saveV2Episodes(this.v2Paths, v2Episodes),
|
|
524
|
+
saveV2Semantic(this.v2Paths, nextSemantic),
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
for (const ep of episodes) {
|
|
528
|
+
if (ep.consolidated) await persistEpisode(this.episodesDir, ep);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const meta = await loadMeta(this.metaPath);
|
|
532
|
+
meta.lastConsolidation = new Date().toISOString();
|
|
533
|
+
await writeJson(this.metaPath, meta);
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
entries: newEntries,
|
|
537
|
+
stats: { episodesConsidered: unconsolidatedCount, added, updated, skipped },
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ─── Retrieval & Injection (Progressive Recall) ────────────
|
|
542
|
+
|
|
543
|
+
async getMemoryInjection(project: string, contextTags: string[], scope?: MemoryScope): Promise<string> {
|
|
544
|
+
await this.autoReviveRelevantArchive([project, ...contextTags].join(" ").trim(), scope);
|
|
545
|
+
const [allKnowledge, allLessons, allEvents, allPrefs, allFacets, allEpisodes, allWork, allV2Episodes, allV2EpisodeFacets, allV2Semantic, allProcedural] = await Promise.all([
|
|
546
|
+
loadEntries(this.knowledgePath),
|
|
547
|
+
loadEntries(this.lessonsPath),
|
|
548
|
+
loadEntries(this.eventsPath),
|
|
549
|
+
loadEntries(this.preferencesPath),
|
|
550
|
+
loadEntries(this.facetsPath),
|
|
551
|
+
loadEpisodes(this.episodesDir),
|
|
552
|
+
loadWork(this.workPath),
|
|
553
|
+
loadV2Episodes(this.v2Paths),
|
|
554
|
+
loadV2Facets(this.v2Paths),
|
|
555
|
+
loadV2Semantic(this.v2Paths),
|
|
556
|
+
loadV2Procedural(this.v2Paths),
|
|
557
|
+
]);
|
|
558
|
+
|
|
559
|
+
const semanticCandidates = await this.querySemanticCandidates(
|
|
560
|
+
[project, ...contextTags].join(" ").trim(),
|
|
561
|
+
project,
|
|
562
|
+
scope,
|
|
563
|
+
this.filterAndCleanV2Episodes(allV2Episodes, project, scope),
|
|
564
|
+
this.filterAndCleanV2Facets(allV2EpisodeFacets, project, scope),
|
|
565
|
+
this.filterAndCleanV2Semantic(allV2Semantic, project, scope),
|
|
566
|
+
this.filterAndCleanProcedural(allProcedural, project, scope),
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
const selection = selectRecallEntries({
|
|
570
|
+
knowledge: this.filterAndCleanEntries(allKnowledge, scope),
|
|
571
|
+
lessons: this.filterAndCleanEntries(allLessons, scope),
|
|
572
|
+
events: this.filterAndCleanEntries(allEvents, scope),
|
|
573
|
+
prefs: this.filterAndCleanEntries(allPrefs, scope),
|
|
574
|
+
facets: this.filterAndCleanEntries(allFacets, scope),
|
|
575
|
+
work: this.filterAndCleanWork(allWork, scope),
|
|
576
|
+
episodes: filterByScope(allEpisodes, scope),
|
|
577
|
+
v2Episodes: this.filterAndCleanV2Episodes(allV2Episodes, project, scope),
|
|
578
|
+
v2EpisodeFacets: this.filterAndCleanV2Facets(allV2EpisodeFacets, project, scope),
|
|
579
|
+
v2Semantic: this.filterAndCleanV2Semantic(allV2Semantic, project, scope),
|
|
580
|
+
procedural: this.filterAndCleanProcedural(allProcedural, project, scope),
|
|
581
|
+
allKnowledge, allLessons, allEvents, allPrefs, allFacets,
|
|
582
|
+
semanticCandidates,
|
|
583
|
+
halfLife: this.cfg.halfLife,
|
|
584
|
+
scoreWeights: this.cfg.scoreWeights,
|
|
585
|
+
progressiveRecall: this.cfg.progressiveRecall,
|
|
586
|
+
tokenBudget: this.cfg.tokenBudget,
|
|
587
|
+
structuralWeight: this.cfg.structuralWeight,
|
|
588
|
+
project, contextTags,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Publish recall snapshot to turn-context bus for eval collection
|
|
592
|
+
this.publishRecallSnapshot(selection, project, contextTags);
|
|
593
|
+
|
|
594
|
+
// Reinforce all recalled entries via spaced repetition
|
|
595
|
+
await this.reinforceEntries(selection.injectedActiveKnowledge, allKnowledge, this.knowledgePath);
|
|
596
|
+
await this.reinforceEntries(selection.injectedActiveLessons, allLessons, this.lessonsPath);
|
|
597
|
+
await this.reinforceEntries(selection.injectedActiveEvents, allEvents, this.eventsPath);
|
|
598
|
+
await this.reinforceEntries(selection.injectedActivePrefs, allPrefs, this.preferencesPath);
|
|
599
|
+
await this.reinforceEntries(selection.legacyFacetBridgeActive, allFacets, this.facetsPath);
|
|
600
|
+
await this.reinforceWork(selection.cue.work, allWork);
|
|
601
|
+
await this.reinforceEpisodeMemories([...selection.active.episodeMemories, ...selection.cue.episodeMemories], allV2Episodes);
|
|
602
|
+
await this.reinforceEpisodeFacets([...selection.active.episodeFacets, ...selection.cue.episodeFacets], allV2EpisodeFacets);
|
|
603
|
+
await this.reinforceV2SemanticMemories([...selection.active.semanticMemories, ...selection.cue.semanticMemories], allV2Semantic);
|
|
604
|
+
await this.reinforceProcedural([...selection.active.procedural, ...selection.cue.procedural], allProcedural);
|
|
605
|
+
await this.reconsolidateV2AfterRecall(
|
|
606
|
+
allV2Episodes, allV2EpisodeFacets, allProcedural,
|
|
607
|
+
[...selection.active.episodeMemories, ...selection.cue.episodeMemories],
|
|
608
|
+
[...selection.active.episodeFacets, ...selection.cue.episodeFacets],
|
|
609
|
+
[...selection.active.procedural, ...selection.cue.procedural],
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
if (this.llmFn) {
|
|
613
|
+
await this.reconsolidateIfNeeded(selection.injectedActiveKnowledge, contextTags, allKnowledge);
|
|
614
|
+
await this.reconsolidateIfNeeded(selection.injectedActiveLessons, contextTags, allLessons);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const p = PROMPTS[this.cfg.locale] ?? PROMPTS.en;
|
|
618
|
+
return this.buildProgressiveInjectionText(selection.active, selection.cue, selection.allEntries, selection.graphContext, p);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ─── Recall snapshot for eval collection ──────────────────
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Build per-memory score breakdowns from the selection result and publish
|
|
625
|
+
* to the turn-context bus. The SAL extension (or any eval consumer) can
|
|
626
|
+
* read this snapshot from getTurnContext("memoryRecallSnapshot") in its
|
|
627
|
+
* agent_end hook to emit eval events. mem-core itself never imports from
|
|
628
|
+
* SAL or the eval system.
|
|
629
|
+
*/
|
|
630
|
+
private publishRecallSnapshot(
|
|
631
|
+
selection: import("./engine-recall-select.js").RecallSelectionResult,
|
|
632
|
+
project: string,
|
|
633
|
+
contextTags: string[],
|
|
634
|
+
): void {
|
|
635
|
+
const sw = this.cfg.structuralWeight;
|
|
636
|
+
const records: MemoryRecallRecord[] = [];
|
|
637
|
+
const injectionOrder = buildInjectedMemoryOrder(selection.active, selection.cue);
|
|
638
|
+
const injectOrderByMemory = new Map(
|
|
639
|
+
injectionOrder.map((record, index) => [`${record.memoryKind}:${record.memoryId}`, index + 1]),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// V2 episode memories
|
|
643
|
+
const activeEpIds = new Set(selection.active.episodeMemories.map((e) => e.id));
|
|
644
|
+
const allEpisodes = [...selection.active.episodeMemories, ...selection.cue.episodeMemories];
|
|
645
|
+
for (const entry of allEpisodes) {
|
|
646
|
+
const bd = breakdownEpisodeMemory(entry, project, contextTags, sw);
|
|
647
|
+
const injected = activeEpIds.has(entry.id);
|
|
648
|
+
records.push({
|
|
649
|
+
memoryId: entry.id, memoryKind: "episode",
|
|
650
|
+
anchorModule: entry.structuralAnchor?.modulePath,
|
|
651
|
+
anchorFile: entry.structuralAnchor?.filePath,
|
|
652
|
+
scoreBreakdownStatus: "available",
|
|
653
|
+
scoreRecency: bd.recency, scoreImportance: bd.importance,
|
|
654
|
+
scoreRelevance: bd.relevance, scoreStructural: bd.structural,
|
|
655
|
+
scoreFinal: bd.final, wasInjected: injected,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// V2 episode facets
|
|
660
|
+
const activeEfIds = new Set(selection.active.episodeFacets.map((e) => e.id));
|
|
661
|
+
const allFacets = [...selection.active.episodeFacets, ...selection.cue.episodeFacets];
|
|
662
|
+
for (const entry of allFacets) {
|
|
663
|
+
const bd = breakdownEpisodeFacet(entry, project, contextTags, sw);
|
|
664
|
+
const injected = activeEfIds.has(entry.id);
|
|
665
|
+
records.push({
|
|
666
|
+
memoryId: entry.id, memoryKind: "episodeFacet",
|
|
667
|
+
anchorModule: entry.structuralAnchor?.modulePath,
|
|
668
|
+
anchorFile: entry.structuralAnchor?.filePath,
|
|
669
|
+
scoreBreakdownStatus: "available",
|
|
670
|
+
scoreRecency: bd.recency, scoreImportance: bd.importance,
|
|
671
|
+
scoreRelevance: bd.relevance, scoreStructural: bd.structural,
|
|
672
|
+
scoreFinal: bd.final, wasInjected: injected,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// V2 semantic memories
|
|
677
|
+
const activeSmIds = new Set(selection.active.semanticMemories.map((e) => e.id));
|
|
678
|
+
const allSemantic = [...selection.active.semanticMemories, ...selection.cue.semanticMemories];
|
|
679
|
+
for (const entry of allSemantic) {
|
|
680
|
+
const bd = breakdownV2Semantic(entry, project, contextTags, sw);
|
|
681
|
+
const injected = activeSmIds.has(entry.id);
|
|
682
|
+
records.push({
|
|
683
|
+
memoryId: entry.id, memoryKind: "semantic",
|
|
684
|
+
anchorModule: entry.structuralAnchor?.modulePath,
|
|
685
|
+
anchorFile: entry.structuralAnchor?.filePath,
|
|
686
|
+
scoreBreakdownStatus: "available",
|
|
687
|
+
scoreRecency: bd.recency, scoreImportance: bd.importance,
|
|
688
|
+
scoreRelevance: bd.relevance, scoreStructural: bd.structural,
|
|
689
|
+
scoreFinal: bd.final, wasInjected: injected,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// V2 procedural memories
|
|
694
|
+
const activePrIds = new Set(selection.active.procedural.map((e) => e.id));
|
|
695
|
+
const allProcedural = [...selection.active.procedural, ...selection.cue.procedural];
|
|
696
|
+
for (const entry of allProcedural) {
|
|
697
|
+
const bd = breakdownProcedural(entry, project, contextTags, sw);
|
|
698
|
+
const injected = activePrIds.has(entry.id);
|
|
699
|
+
records.push({
|
|
700
|
+
memoryId: entry.id, memoryKind: "procedural",
|
|
701
|
+
anchorModule: entry.structuralAnchor?.modulePath,
|
|
702
|
+
anchorFile: entry.structuralAnchor?.filePath,
|
|
703
|
+
scoreBreakdownStatus: "available",
|
|
704
|
+
scoreRecency: bd.recency, scoreImportance: bd.importance,
|
|
705
|
+
scoreRelevance: bd.relevance, scoreStructural: bd.structural,
|
|
706
|
+
scoreFinal: bd.final, wasInjected: injected,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Legacy entries (knowledge/lessons/events/prefs/facets) — no structural anchor
|
|
711
|
+
const legacyKinds = [
|
|
712
|
+
{ active: selection.injectedActiveKnowledge, cue: selection.injectedCueKnowledge, kind: "knowledge" },
|
|
713
|
+
{ active: selection.injectedActiveLessons, cue: selection.injectedCueLessons, kind: "lesson" },
|
|
714
|
+
{ active: selection.injectedActiveEvents, cue: selection.injectedCueEvents, kind: "event" },
|
|
715
|
+
{ active: selection.injectedActivePrefs, cue: selection.injectedCuePrefs, kind: "preference" },
|
|
716
|
+
{ active: selection.legacyFacetBridgeActive, cue: selection.legacyFacetBridgeCue, kind: "facet" },
|
|
717
|
+
];
|
|
718
|
+
const hl = this.cfg.halfLife;
|
|
719
|
+
const scoreWeights = this.cfg.scoreWeights;
|
|
720
|
+
for (const { active, cue, kind } of legacyKinds) {
|
|
721
|
+
const activeIds = new Set(active.map((e) => e.id));
|
|
722
|
+
for (const entry of [...active, ...cue]) {
|
|
723
|
+
const score = scoreEntry(entry, project, contextTags, hl, scoreWeights);
|
|
724
|
+
records.push({
|
|
725
|
+
memoryId: entry.id, memoryKind: kind,
|
|
726
|
+
scoreBreakdownStatus: "unavailable",
|
|
727
|
+
scoreFinal: score, wasInjected: activeIds.has(entry.id),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Assign inject_rank by actual injection order, matching prompt assembly order.
|
|
733
|
+
for (const record of records) {
|
|
734
|
+
if (!record.wasInjected) continue;
|
|
735
|
+
record.injectRank = injectOrderByMemory.get(`${record.memoryKind}:${record.memoryId}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
setTurnContext("memoryRecallSnapshot", records);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ─── Progressive Recall Tools ───────────────────────────────
|
|
742
|
+
|
|
743
|
+
/** Retrieve a single entry by ID (for recall_memory tool) */
|
|
744
|
+
async recallV2ById(id: string) {
|
|
745
|
+
const snapshot = await this.getV2Snapshot();
|
|
746
|
+
const semantic = snapshot.semantic.find(entry => entry.id === id);
|
|
747
|
+
if (semantic) { await this.reinforceV2SemanticMemories([semantic], snapshot.semantic); return semantic; }
|
|
748
|
+
const episode = snapshot.episodes.find(entry => entry.id === id);
|
|
749
|
+
if (episode) { await this.reinforceEpisodeMemories([episode], snapshot.episodes); return episode; }
|
|
750
|
+
const facet = snapshot.facets.find(entry => entry.id === id);
|
|
751
|
+
if (facet) { await this.reinforceEpisodeFacets([facet], snapshot.facets); return facet; }
|
|
752
|
+
const procedural = snapshot.procedural.find(entry => entry.id === id);
|
|
753
|
+
if (procedural) { await this.reinforceProcedural([procedural], snapshot.procedural); return procedural; }
|
|
754
|
+
const state = await loadV2State(this.v2Paths);
|
|
755
|
+
return state.find(entry => entry.id === id) ?? null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async getEntryById(id: string): Promise<MemoryEntry | null> {
|
|
759
|
+
for (const { path } of this.getMemoryPathConfigs()) {
|
|
760
|
+
const entries = await loadEntries(path);
|
|
761
|
+
const entry = entries.find((e) => e.id === id);
|
|
762
|
+
if (entry) return entry;
|
|
763
|
+
}
|
|
764
|
+
const semantic = (await loadV2Semantic(this.v2Paths)).find(entry => entry.id === id);
|
|
765
|
+
if (semantic) return this.semanticToRuntimeEntry(semantic);
|
|
766
|
+
const procedural = (await loadV2Procedural(this.v2Paths)).find(entry => entry.id === id);
|
|
767
|
+
return procedural ? this.proceduralToRuntimeEntry(procedural) : null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** Reinforce a single entry by ID (bump accessCount, lastAccessed, strength) */
|
|
771
|
+
async reinforceEntryById(id: string): Promise<boolean> {
|
|
772
|
+
const pathConfigs = this.getMemoryPathConfigs();
|
|
773
|
+
const hl = this.cfg.halfLife;
|
|
774
|
+
const ew = this.cfg.evictionWeights;
|
|
775
|
+
|
|
776
|
+
for (const { path, max } of pathConfigs) {
|
|
777
|
+
const entries = await loadEntries(path);
|
|
778
|
+
const entry = entries.find((e) => e.id === id);
|
|
779
|
+
if (entry) {
|
|
780
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
781
|
+
entry.lastAccessed = new Date().toISOString();
|
|
782
|
+
entry.strength = (entry.strength || 30) * this.cfg.strengthGrowthFactor;
|
|
783
|
+
await saveEntries(path, entries, max, (e) => utilityEntry(e, hl, ew));
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return Boolean(await this.recallV2ById(id));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async editEntryById(
|
|
791
|
+
id: string,
|
|
792
|
+
patch: Partial<
|
|
793
|
+
Pick<MemoryEntry, "name" | "summary" | "detail" | "retention" | "salience" | "stability" | "ttl">
|
|
794
|
+
> & { tags?: string[] },
|
|
795
|
+
): Promise<MemoryEntry | null> {
|
|
796
|
+
const located = await this.findEntryLocation(id);
|
|
797
|
+
if (!located) {
|
|
798
|
+
const semantic = await loadV2Semantic(this.v2Paths);
|
|
799
|
+
const item = semantic.find(entry => entry.id === id);
|
|
800
|
+
if (item) {
|
|
801
|
+
const legacy = Object.values(await this.getAllEntries()).flat().find(entry => (item.legacyIds?.includes(entry.id) || (entry.name === item.name && entry.summary === item.summary && entry.project === item.scope?.project)));
|
|
802
|
+
if (legacy) { await this.editEntryById(legacy.id, patch); return this.getEntryById(id); }
|
|
803
|
+
Object.assign(item, patch, { ttlDays: patch.ttl ?? item.ttlDays, updatedAt: new Date().toISOString() });
|
|
804
|
+
item.tags = patch.tags ?? extractTags(`${item.name} ${item.summary} ${item.detail || ''}`);
|
|
805
|
+
await saveV2Semantic(this.v2Paths, semantic);
|
|
806
|
+
return this.semanticToRuntimeEntry(item);
|
|
807
|
+
}
|
|
808
|
+
const procedural = await loadV2Procedural(this.v2Paths);
|
|
809
|
+
const procedure = procedural.find(entry => entry.id === id);
|
|
810
|
+
if (!procedure) return null;
|
|
811
|
+
Object.assign(procedure, patch, { ttlDays: patch.ttl ?? procedure.ttlDays, updatedAt: new Date().toISOString() });
|
|
812
|
+
if (patch.detail !== undefined) procedure.contextText = patch.detail;
|
|
813
|
+
procedure.searchText = `${procedure.name} ${procedure.summary} ${procedure.contextText || ''}`;
|
|
814
|
+
procedure.tags = patch.tags ?? extractTags(procedure.searchText);
|
|
815
|
+
await saveV2Procedural(this.v2Paths, procedural);
|
|
816
|
+
return this.proceduralToRuntimeEntry(procedure);
|
|
817
|
+
}
|
|
818
|
+
const { entry, entries, path, max } = located;
|
|
819
|
+
const previousName = entry.name;
|
|
820
|
+
const previousSummary = entry.summary;
|
|
821
|
+
const detail = patch.detail ?? entry.detail ?? entry.content ?? "";
|
|
822
|
+
entry.name = patch.name ?? entry.name;
|
|
823
|
+
entry.summary = patch.summary ?? entry.summary;
|
|
824
|
+
entry.detail = detail;
|
|
825
|
+
entry.content = detail;
|
|
826
|
+
entry.retention = patch.retention ?? entry.retention;
|
|
827
|
+
entry.salience = patch.salience ?? entry.salience;
|
|
828
|
+
entry.stability = patch.stability ?? entry.stability;
|
|
829
|
+
entry.ttl = patch.ttl ?? entry.ttl;
|
|
830
|
+
entry.tags = patch.tags?.length ? patch.tags : extractTags(`${entry.name || ""} ${entry.summary || ""} ${detail}`);
|
|
831
|
+
entry.lastAccessed = new Date().toISOString();
|
|
832
|
+
await this.persistEntries(path, entries, max);
|
|
833
|
+
for (const v2Paths of [this.v2Paths, this.archiveV2Paths]) {
|
|
834
|
+
const semantic = await loadV2Semantic(v2Paths);
|
|
835
|
+
const mirror = semantic.find((item) => (item.legacyIds?.includes(id) || (item.name === previousName && item.summary === previousSummary && item.scope?.project === entry.project)));
|
|
836
|
+
if (!mirror) continue;
|
|
837
|
+
mirror.legacyIds = [...new Set([...(mirror.legacyIds ?? []), id])];
|
|
838
|
+
mirror.name = entry.name || mirror.name;
|
|
839
|
+
mirror.summary = entry.summary || mirror.summary;
|
|
840
|
+
mirror.detail = entry.detail;
|
|
841
|
+
mirror.tags = entry.tags;
|
|
842
|
+
mirror.retention = entry.retention || mirror.retention;
|
|
843
|
+
mirror.salience = entry.salience ?? mirror.salience;
|
|
844
|
+
mirror.stability = entry.stability || mirror.stability;
|
|
845
|
+
mirror.ttlDays = entry.ttl;
|
|
846
|
+
mirror.updatedAt = new Date().toISOString();
|
|
847
|
+
await saveV2Semantic(v2Paths, semantic);
|
|
848
|
+
}
|
|
849
|
+
return entry;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async resolveConflictByIds(
|
|
853
|
+
aId: string,
|
|
854
|
+
bId: string,
|
|
855
|
+
action: "merge" | "demote" | "forget" | "mark-situational",
|
|
856
|
+
): Promise<{ action: string; updatedIds: string[] } | null> {
|
|
857
|
+
const a = await this.getEntryById(aId);
|
|
858
|
+
const b = await this.getEntryById(bId);
|
|
859
|
+
if (!a || !b || aId === bId) return null;
|
|
860
|
+
const score = (entry: MemoryEntry) => (entry.salience ?? entry.importance) * 2 + (entry.accessCount ?? 0) + (entry.retention === "core" ? 4 : entry.retention === "key-event" ? 2 : 0);
|
|
861
|
+
const [primary, secondary] = score(a) >= score(b) ? [a, b] : [b, a];
|
|
862
|
+
if (action === "forget") {
|
|
863
|
+
await this.forgetEntry(secondary.id);
|
|
864
|
+
return { action, updatedIds: [primary.id] };
|
|
865
|
+
}
|
|
866
|
+
if (action === "demote" || action === "mark-situational") {
|
|
867
|
+
await this.editEntryById(secondary.id, {
|
|
868
|
+
retention: "ambient",
|
|
869
|
+
salience: action === "demote" ? Math.max(1, Math.min(secondary.salience ?? secondary.importance, 4)) : secondary.salience,
|
|
870
|
+
stability: action === "mark-situational" ? "situational" : secondary.stability,
|
|
871
|
+
ttl: action === "mark-situational" ? Math.min(14, this.cfg.forgetting.ambientTtlDays) : secondary.ttl ?? this.cfg.forgetting.ambientTtlDays,
|
|
872
|
+
});
|
|
873
|
+
return { action, updatedIds: [secondary.id] };
|
|
874
|
+
}
|
|
875
|
+
await this.editEntryById(primary.id, {
|
|
876
|
+
summary: [primary.summary, secondary.summary].filter(Boolean).join(" | ").slice(0, 300),
|
|
877
|
+
detail: [primary.detail || primary.content, secondary.detail || secondary.content].filter(Boolean).join("\n\n").slice(0, 4000),
|
|
878
|
+
tags: [...new Set([...(primary.tags ?? []), ...(secondary.tags ?? [])])].slice(0, 30),
|
|
879
|
+
salience: Math.max(primary.salience ?? primary.importance, secondary.salience ?? secondary.importance),
|
|
880
|
+
});
|
|
881
|
+
await this.forgetEntry(secondary.id);
|
|
882
|
+
return { action, updatedIds: [primary.id] };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Full-text search across ALL entries including dormant (for search_all_memories tool) */
|
|
886
|
+
async searchAllEntries(query: string, limit = 10): Promise<MemoryEntry[]> {
|
|
887
|
+
const tags = extractTags(query);
|
|
888
|
+
const queryLower = query.toLowerCase();
|
|
889
|
+
|
|
890
|
+
const [knowledge, lessons, events, prefs, facets] = await Promise.all([
|
|
891
|
+
loadEntries(this.knowledgePath),
|
|
892
|
+
loadEntries(this.lessonsPath),
|
|
893
|
+
loadEntries(this.eventsPath),
|
|
894
|
+
loadEntries(this.preferencesPath),
|
|
895
|
+
loadEntries(this.facetsPath),
|
|
896
|
+
]);
|
|
897
|
+
|
|
898
|
+
const all = [...knowledge, ...lessons, ...events, ...prefs, ...facets];
|
|
899
|
+
|
|
900
|
+
return all
|
|
901
|
+
.map((e) => {
|
|
902
|
+
const nameMatch = (e.name || "").toLowerCase().includes(queryLower) ? 2 : 0;
|
|
903
|
+
const summaryMatch = (e.summary || "").toLowerCase().includes(queryLower) ? 1 : 0;
|
|
904
|
+
const tagMatch = tagOverlap(e.tags, tags);
|
|
905
|
+
const relationBoost = (e.relations ?? [])
|
|
906
|
+
.filter((relation) => tags.some((tag) => relation.kind.includes(tag) || relation.id.includes(tag)))
|
|
907
|
+
.reduce((sum, relation) => sum + relation.weight * 0.15, 0);
|
|
908
|
+
return { entry: e, score: nameMatch + summaryMatch + tagMatch + relationBoost };
|
|
909
|
+
})
|
|
910
|
+
.filter((x) => x.score > 0)
|
|
911
|
+
.sort((a, b) => b.score - a.score)
|
|
912
|
+
.slice(0, limit)
|
|
913
|
+
.map((x) => x.entry);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ─── Stats ───────────────────────────────────────────────────
|
|
917
|
+
|
|
918
|
+
async getStats(): Promise<{
|
|
919
|
+
knowledge: number;
|
|
920
|
+
lessons: number;
|
|
921
|
+
events: number;
|
|
922
|
+
preferences: number;
|
|
923
|
+
facets: number;
|
|
924
|
+
episodes: number;
|
|
925
|
+
work: number;
|
|
926
|
+
archivedKnowledge: number;
|
|
927
|
+
archivedLessons: number;
|
|
928
|
+
archivedEvents: number;
|
|
929
|
+
archivedPreferences: number;
|
|
930
|
+
archivedFacets: number;
|
|
931
|
+
archivedWork: number;
|
|
932
|
+
totalSessions: number;
|
|
933
|
+
}> {
|
|
934
|
+
const [knowledge, lessons, events, prefs, facets, episodes, work, meta, archived] = await Promise.all([
|
|
935
|
+
loadEntries(this.knowledgePath),
|
|
936
|
+
loadEntries(this.lessonsPath),
|
|
937
|
+
loadEntries(this.eventsPath),
|
|
938
|
+
loadEntries(this.preferencesPath),
|
|
939
|
+
loadEntries(this.facetsPath),
|
|
940
|
+
loadEpisodes(this.episodesDir),
|
|
941
|
+
loadWork(this.workPath),
|
|
942
|
+
loadMeta(this.metaPath),
|
|
943
|
+
this.exportArchive(),
|
|
944
|
+
]);
|
|
945
|
+
return {
|
|
946
|
+
knowledge: knowledge.length,
|
|
947
|
+
lessons: lessons.length,
|
|
948
|
+
events: events.length,
|
|
949
|
+
preferences: prefs.length,
|
|
950
|
+
facets: facets.length,
|
|
951
|
+
episodes: episodes.length,
|
|
952
|
+
work: work.length,
|
|
953
|
+
archivedKnowledge: archived.knowledge.length,
|
|
954
|
+
archivedLessons: archived.lessons.length,
|
|
955
|
+
archivedEvents: archived.events.length,
|
|
956
|
+
archivedPreferences: archived.preferences.length,
|
|
957
|
+
archivedFacets: archived.facets.length,
|
|
958
|
+
archivedWork: archived.work.length,
|
|
959
|
+
totalSessions: meta.totalSessions,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
async getV2Stats(): Promise<{
|
|
964
|
+
episodes: number;
|
|
965
|
+
facets: number;
|
|
966
|
+
semantic: number;
|
|
967
|
+
procedural: number;
|
|
968
|
+
archivedSemantic: number;
|
|
969
|
+
archivedProcedural: number;
|
|
970
|
+
links: number;
|
|
971
|
+
embeddings: number;
|
|
972
|
+
lastEmbeddingSyncAt?: string;
|
|
973
|
+
lastReconsolidationAt?: string;
|
|
974
|
+
}> {
|
|
975
|
+
const [episodes, facets, semantic, procedural, links, embeddingIndex, meta, archived] = await Promise.all([
|
|
976
|
+
loadV2Episodes(this.v2Paths),
|
|
977
|
+
loadV2Facets(this.v2Paths),
|
|
978
|
+
loadV2Semantic(this.v2Paths),
|
|
979
|
+
loadV2Procedural(this.v2Paths),
|
|
980
|
+
loadV2Links(this.v2Paths),
|
|
981
|
+
loadEmbeddingIndex(this.cfg.memoryDir),
|
|
982
|
+
loadV2Meta(this.v2Paths),
|
|
983
|
+
this.exportArchive(),
|
|
984
|
+
]);
|
|
985
|
+
return {
|
|
986
|
+
episodes: episodes.length,
|
|
987
|
+
facets: facets.length,
|
|
988
|
+
semantic: semantic.length,
|
|
989
|
+
procedural: procedural.length,
|
|
990
|
+
archivedSemantic: archived.semantic.length,
|
|
991
|
+
archivedProcedural: archived.procedural.length,
|
|
992
|
+
links: links.length,
|
|
993
|
+
embeddings: embeddingIndex.records.length,
|
|
994
|
+
lastEmbeddingSyncAt: meta.lastEmbeddingSyncAt,
|
|
995
|
+
lastReconsolidationAt: meta.lastReconsolidationAt,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// ─── Direct Access (for CLI, testing) ────────────────────────
|
|
1000
|
+
|
|
1001
|
+
async getAllEntries(): Promise<{
|
|
1002
|
+
knowledge: MemoryEntry[];
|
|
1003
|
+
lessons: MemoryEntry[];
|
|
1004
|
+
events: MemoryEntry[];
|
|
1005
|
+
preferences: MemoryEntry[];
|
|
1006
|
+
facets: MemoryEntry[];
|
|
1007
|
+
}> {
|
|
1008
|
+
return {
|
|
1009
|
+
knowledge: await loadEntries(this.knowledgePath),
|
|
1010
|
+
lessons: await loadEntries(this.lessonsPath),
|
|
1011
|
+
events: await loadEntries(this.eventsPath),
|
|
1012
|
+
preferences: await loadEntries(this.preferencesPath),
|
|
1013
|
+
facets: await loadEntries(this.facetsPath),
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async getRuntimeIdentityEntries(): Promise<{
|
|
1018
|
+
knowledge: MemoryEntry[];
|
|
1019
|
+
lessons: MemoryEntry[];
|
|
1020
|
+
preferences: MemoryEntry[];
|
|
1021
|
+
facets: MemoryEntry[];
|
|
1022
|
+
}> {
|
|
1023
|
+
const runtime = await this.buildRuntimeMemoryView();
|
|
1024
|
+
return {
|
|
1025
|
+
knowledge: runtime.knowledge,
|
|
1026
|
+
lessons: runtime.lessons,
|
|
1027
|
+
preferences: runtime.preferences,
|
|
1028
|
+
facets: runtime.facets,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async getAllWork(): Promise<WorkEntry[]> {
|
|
1033
|
+
return loadWork(this.workPath);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async getAllEpisodes(): Promise<Episode[]> {
|
|
1037
|
+
return loadEpisodes(this.episodesDir);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async runStartupMaintenance(maintenanceVersion = 3): Promise<{
|
|
1041
|
+
ran: boolean;
|
|
1042
|
+
backupPath?: string;
|
|
1043
|
+
deduplicated: {
|
|
1044
|
+
knowledge: number;
|
|
1045
|
+
lessons: number;
|
|
1046
|
+
events: number;
|
|
1047
|
+
preferences: number;
|
|
1048
|
+
facets: number;
|
|
1049
|
+
work: number;
|
|
1050
|
+
total: number;
|
|
1051
|
+
};
|
|
1052
|
+
migratedEpisodesToV2: number;
|
|
1053
|
+
archived: {
|
|
1054
|
+
knowledge: number;
|
|
1055
|
+
lessons: number;
|
|
1056
|
+
events: number;
|
|
1057
|
+
preferences: number;
|
|
1058
|
+
facets: number;
|
|
1059
|
+
work: number;
|
|
1060
|
+
semantic: number;
|
|
1061
|
+
procedural: number;
|
|
1062
|
+
total: number;
|
|
1063
|
+
};
|
|
1064
|
+
}> {
|
|
1065
|
+
const [meta, v2Meta] = await Promise.all([loadMeta(this.metaPath), loadV2Meta(this.v2Paths)]);
|
|
1066
|
+
const alreadyMaintained =
|
|
1067
|
+
(meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion &&
|
|
1068
|
+
(v2Meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion;
|
|
1069
|
+
if (alreadyMaintained) {
|
|
1070
|
+
return {
|
|
1071
|
+
ran: false,
|
|
1072
|
+
backupPath: undefined,
|
|
1073
|
+
deduplicated: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, total: 0 },
|
|
1074
|
+
migratedEpisodesToV2: 0,
|
|
1075
|
+
archived: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, semantic: 0, procedural: 0, total: 0 },
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const now = new Date().toISOString();
|
|
1080
|
+
const backupPath = await this.createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now);
|
|
1081
|
+
const deduplicated = await this.deduplicateAll();
|
|
1082
|
+
const episodes = await this.getAllEpisodes();
|
|
1083
|
+
for (const episode of episodes) {
|
|
1084
|
+
await this.syncEpisodeToV2(episode);
|
|
1085
|
+
}
|
|
1086
|
+
await this.rebuildV2Links();
|
|
1087
|
+
const archived = await this.archiveStaleMemories();
|
|
1088
|
+
await this.rebuildV2Links();
|
|
1089
|
+
|
|
1090
|
+
await Promise.all([
|
|
1091
|
+
writeJson(this.metaPath, {
|
|
1092
|
+
...(await loadMeta(this.metaPath)),
|
|
1093
|
+
lastMaintenanceAt: now,
|
|
1094
|
+
lastMaintenanceVersion: maintenanceVersion,
|
|
1095
|
+
lastBackupAt: meta.lastBackupAt ?? now,
|
|
1096
|
+
lastBackupVersion: Math.max(meta.lastBackupVersion ?? 0, maintenanceVersion),
|
|
1097
|
+
}),
|
|
1098
|
+
saveV2Meta(this.v2Paths, {
|
|
1099
|
+
...(await loadV2Meta(this.v2Paths)),
|
|
1100
|
+
version: 2,
|
|
1101
|
+
lastMaintenanceAt: now,
|
|
1102
|
+
lastMaintenanceVersion: maintenanceVersion,
|
|
1103
|
+
lastMigrationAt: (await loadV2Meta(this.v2Paths)).lastMigrationAt ?? now,
|
|
1104
|
+
lastBackupAt: v2Meta.lastBackupAt ?? now,
|
|
1105
|
+
lastBackupVersion: Math.max(v2Meta.lastBackupVersion ?? 0, maintenanceVersion),
|
|
1106
|
+
}),
|
|
1107
|
+
]);
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
ran: true,
|
|
1111
|
+
backupPath,
|
|
1112
|
+
deduplicated,
|
|
1113
|
+
migratedEpisodesToV2: episodes.length,
|
|
1114
|
+
archived,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private async createMaintenanceBackup(
|
|
1119
|
+
meta: Meta,
|
|
1120
|
+
v2Meta: { lastBackupVersion?: number },
|
|
1121
|
+
maintenanceVersion: number,
|
|
1122
|
+
now: string,
|
|
1123
|
+
): Promise<string | undefined> {
|
|
1124
|
+
const alreadyBackedUp =
|
|
1125
|
+
(meta.lastBackupVersion ?? 0) >= maintenanceVersion &&
|
|
1126
|
+
(v2Meta.lastBackupVersion ?? 0) >= maintenanceVersion;
|
|
1127
|
+
if (alreadyBackedUp) return undefined;
|
|
1128
|
+
|
|
1129
|
+
const safeTimestamp = now.replace(/[:.]/g, "-");
|
|
1130
|
+
const backupRoot = join(this.cfg.memoryDir, "_backups");
|
|
1131
|
+
const backupDir = join(backupRoot, `maintenance-v${maintenanceVersion}-${safeTimestamp}`);
|
|
1132
|
+
await mkdir(backupDir, { recursive: true });
|
|
1133
|
+
|
|
1134
|
+
const filesToCopy = [
|
|
1135
|
+
this.knowledgePath,
|
|
1136
|
+
this.lessonsPath,
|
|
1137
|
+
this.eventsPath,
|
|
1138
|
+
this.preferencesPath,
|
|
1139
|
+
this.facetsPath,
|
|
1140
|
+
this.workPath,
|
|
1141
|
+
this.metaPath,
|
|
1142
|
+
];
|
|
1143
|
+
for (const filePath of filesToCopy) {
|
|
1144
|
+
try {
|
|
1145
|
+
await cp(filePath, join(backupDir, filePath.split("/").pop() ?? "unknown.json"));
|
|
1146
|
+
} catch {
|
|
1147
|
+
// Missing files are fine for first-run users.
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
for (const dirName of ["episodes", "v2", "_archive"]) {
|
|
1152
|
+
const sourceDir = join(this.cfg.memoryDir, dirName);
|
|
1153
|
+
try {
|
|
1154
|
+
await readdir(sourceDir);
|
|
1155
|
+
await cp(sourceDir, join(backupDir, dirName), { recursive: true });
|
|
1156
|
+
} catch {
|
|
1157
|
+
// Skip directories that do not exist yet.
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
await writeJson(join(backupDir, "manifest.json"), {
|
|
1162
|
+
createdAt: now,
|
|
1163
|
+
maintenanceVersion,
|
|
1164
|
+
memoryDir: this.cfg.memoryDir,
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
return backupDir;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private async syncEpisodeToV2(ep: Episode): Promise<void> {
|
|
1171
|
+
const [episodes, facets, links, procedural, meta] = await Promise.all([
|
|
1172
|
+
loadV2Episodes(this.v2Paths),
|
|
1173
|
+
loadV2Facets(this.v2Paths),
|
|
1174
|
+
loadV2Links(this.v2Paths),
|
|
1175
|
+
loadV2Procedural(this.v2Paths),
|
|
1176
|
+
loadV2Meta(this.v2Paths),
|
|
1177
|
+
]);
|
|
1178
|
+
|
|
1179
|
+
const now = new Date().toISOString();
|
|
1180
|
+
const episodeId = this.makeEpisodeMemoryId(ep);
|
|
1181
|
+
const mapped = this.mapEpisodeToV2(ep, now);
|
|
1182
|
+
const nextEpisodes = episodes.filter((existing) => existing.id !== episodeId);
|
|
1183
|
+
nextEpisodes.push(mapped.episode);
|
|
1184
|
+
|
|
1185
|
+
const nextFacets = facets.filter((facet) => facet.episodeId !== episodeId);
|
|
1186
|
+
nextFacets.push(...mapped.facets);
|
|
1187
|
+
|
|
1188
|
+
const facetIds = new Set(mapped.facets.map((facet) => facet.id));
|
|
1189
|
+
const nextLinks = links.filter(
|
|
1190
|
+
(link) =>
|
|
1191
|
+
link.fromId !== episodeId &&
|
|
1192
|
+
link.toId !== episodeId &&
|
|
1193
|
+
!facetIds.has(link.fromId) &&
|
|
1194
|
+
!facetIds.has(link.toId),
|
|
1195
|
+
);
|
|
1196
|
+
nextLinks.push(...mapped.links);
|
|
1197
|
+
|
|
1198
|
+
const nextProcedural = procedural.filter((item) => !item.sourceEpisodeIds?.includes(episodeId));
|
|
1199
|
+
const compiledProcedure = compileProcedureFromEpisode(mapped.episode, mapped.facets, now);
|
|
1200
|
+
if (compiledProcedure) {
|
|
1201
|
+
nextProcedural.push(compiledProcedure);
|
|
1202
|
+
mapped.episode.derivedProcedureIds = [compiledProcedure.id];
|
|
1203
|
+
nextEpisodes[nextEpisodes.length - 1] = mapped.episode;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
await Promise.all([
|
|
1207
|
+
saveV2Episodes(this.v2Paths, nextEpisodes),
|
|
1208
|
+
saveV2Facets(this.v2Paths, nextFacets),
|
|
1209
|
+
saveV2Links(this.v2Paths, nextLinks),
|
|
1210
|
+
saveV2Procedural(this.v2Paths, nextProcedural),
|
|
1211
|
+
saveV2Meta(this.v2Paths, {
|
|
1212
|
+
...meta,
|
|
1213
|
+
version: 2,
|
|
1214
|
+
lastMigrationAt: meta.lastMigrationAt ?? now,
|
|
1215
|
+
}),
|
|
1216
|
+
]);
|
|
1217
|
+
if (this.cfg.embeddings.enabled && this.cfg.embeddings.autoSync && this.embeddingFn) {
|
|
1218
|
+
await this.syncV2Embeddings();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
private mapEpisodeToV2(
|
|
1223
|
+
ep: Episode,
|
|
1224
|
+
now: string,
|
|
1225
|
+
): {
|
|
1226
|
+
episode: EpisodeMemory;
|
|
1227
|
+
facets: EpisodeFacet[];
|
|
1228
|
+
links: MemoryLink[];
|
|
1229
|
+
} {
|
|
1230
|
+
const episodeId = this.makeEpisodeMemoryId(ep);
|
|
1231
|
+
const baseTags = [...new Set([...(ep.tags ?? []), ...extractTags(`${ep.project} ${ep.summary} ${ep.userGoal ?? ""}`)])];
|
|
1232
|
+
const facetDefs: Array<{
|
|
1233
|
+
facetType: FacetKind;
|
|
1234
|
+
searchText: string;
|
|
1235
|
+
anchorText?: string;
|
|
1236
|
+
summary?: string;
|
|
1237
|
+
detail?: string;
|
|
1238
|
+
outcomeScore?: number;
|
|
1239
|
+
causalRole?: EpisodeFacet["causalRole"];
|
|
1240
|
+
}> = [];
|
|
1241
|
+
|
|
1242
|
+
if (ep.userGoal?.trim()) {
|
|
1243
|
+
facetDefs.push({
|
|
1244
|
+
facetType: "goal",
|
|
1245
|
+
searchText: ep.userGoal.trim(),
|
|
1246
|
+
anchorText: ep.summary,
|
|
1247
|
+
summary: `Goal: ${ep.userGoal.trim()}`,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
for (const error of ep.errors.slice(0, 8)) {
|
|
1252
|
+
if (!error.trim()) continue;
|
|
1253
|
+
facetDefs.push({
|
|
1254
|
+
facetType: "error",
|
|
1255
|
+
searchText: error.trim(),
|
|
1256
|
+
anchorText: ep.summary,
|
|
1257
|
+
summary: `Error: ${error.trim()}`,
|
|
1258
|
+
causalRole: "signal",
|
|
1259
|
+
outcomeScore: Math.max(1, 6 - ep.errors.length),
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
for (const observation of ep.keyObservations.slice(0, 8)) {
|
|
1264
|
+
if (!observation.trim()) continue;
|
|
1265
|
+
facetDefs.push({
|
|
1266
|
+
facetType: "insight",
|
|
1267
|
+
searchText: observation.trim(),
|
|
1268
|
+
anchorText: ep.summary,
|
|
1269
|
+
summary: `Insight: ${observation.trim()}`,
|
|
1270
|
+
causalRole: "resolution",
|
|
1271
|
+
outcomeScore: Math.min(10, ep.importance),
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (ep.summary.trim()) {
|
|
1276
|
+
facetDefs.push({
|
|
1277
|
+
facetType: "outcome",
|
|
1278
|
+
searchText: ep.summary.trim().slice(0, 200),
|
|
1279
|
+
anchorText: ep.summary.trim(),
|
|
1280
|
+
summary: ep.summary.trim(),
|
|
1281
|
+
detail: [ep.userGoal ? `Goal: ${ep.userGoal}` : "", ep.errors.length ? `Errors: ${ep.errors.join("; ")}` : ""]
|
|
1282
|
+
.filter(Boolean)
|
|
1283
|
+
.join("\n"),
|
|
1284
|
+
causalRole: ep.errors.length > 0 ? "effect" : "signal",
|
|
1285
|
+
outcomeScore: ep.errors.length > 0 ? Math.max(3, ep.importance - 1) : ep.importance,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const facets: EpisodeFacet[] = facetDefs.map((facet, index) => {
|
|
1290
|
+
const facetId = `${episodeId}:facet:${index + 1}`;
|
|
1291
|
+
const facetTags = [
|
|
1292
|
+
...new Set([
|
|
1293
|
+
...baseTags,
|
|
1294
|
+
facet.facetType,
|
|
1295
|
+
...extractTags(`${facet.searchText} ${facet.summary ?? ""} ${facet.anchorText ?? ""}`),
|
|
1296
|
+
]),
|
|
1297
|
+
].slice(0, 40);
|
|
1298
|
+
return {
|
|
1299
|
+
id: facetId,
|
|
1300
|
+
kind: "facet" as const,
|
|
1301
|
+
episodeId,
|
|
1302
|
+
facetType: facet.facetType,
|
|
1303
|
+
searchText: facet.searchText,
|
|
1304
|
+
anchorText: facet.anchorText,
|
|
1305
|
+
summary: facet.summary,
|
|
1306
|
+
detail: facet.detail,
|
|
1307
|
+
accessCount: 0,
|
|
1308
|
+
importance: Math.max(1, Math.min(10, ep.importance)),
|
|
1309
|
+
salience: Math.max(1, Math.min(10, facet.outcomeScore ?? ep.importance)),
|
|
1310
|
+
confidence: 0.7,
|
|
1311
|
+
retention: ep.importance >= 8 ? ("key-event" as const) : ("ambient" as const),
|
|
1312
|
+
stability: ep.errors.length > 0 ? ("situational" as const) : ("stable" as const),
|
|
1313
|
+
tags: facetTags,
|
|
1314
|
+
sourceEpisodeIds: [episodeId],
|
|
1315
|
+
evidence: [],
|
|
1316
|
+
scope: { ...ep.scope, project: ep.project },
|
|
1317
|
+
createdAt: ep.startedAt ?? `${ep.date}T00:00:00.000Z`,
|
|
1318
|
+
updatedAt: now,
|
|
1319
|
+
entityRefs: [],
|
|
1320
|
+
aliases: [],
|
|
1321
|
+
causalRole: facet.causalRole,
|
|
1322
|
+
outcomeScore: facet.outcomeScore,
|
|
1323
|
+
structuralAnchor: this.currentStructuralAnchor(),
|
|
1324
|
+
};
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const links = facets.map((facet) => ({
|
|
1328
|
+
id: `${episodeId}->${facet.id}`,
|
|
1329
|
+
fromId: episodeId,
|
|
1330
|
+
toId: facet.id,
|
|
1331
|
+
type: "has-facet" as const,
|
|
1332
|
+
weight: 1,
|
|
1333
|
+
explicit: true,
|
|
1334
|
+
createdAt: now,
|
|
1335
|
+
updatedAt: now,
|
|
1336
|
+
evidence: [],
|
|
1337
|
+
}));
|
|
1338
|
+
|
|
1339
|
+
const episode: EpisodeMemory = {
|
|
1340
|
+
id: episodeId,
|
|
1341
|
+
kind: "episode",
|
|
1342
|
+
sessionId: ep.sessionId,
|
|
1343
|
+
title: ep.userGoal || ep.summary.slice(0, 80),
|
|
1344
|
+
summary: ep.summary,
|
|
1345
|
+
startedAt: ep.startedAt,
|
|
1346
|
+
endedAt: ep.endedAt,
|
|
1347
|
+
timeZone: ep.timeZone,
|
|
1348
|
+
userGoal: ep.userGoal,
|
|
1349
|
+
outcome: ep.errors.length ? "high-friction" : "completed",
|
|
1350
|
+
accessCount: 0,
|
|
1351
|
+
importance: Math.max(1, Math.min(10, ep.importance)),
|
|
1352
|
+
salience: Math.max(1, Math.min(10, ep.importance + Math.min(ep.errors.length, 2))),
|
|
1353
|
+
confidence: 0.85,
|
|
1354
|
+
retention: ep.importance >= 8 ? ("key-event" as const) : ("ambient" as const),
|
|
1355
|
+
stability: ep.errors.length > 0 ? ("situational" as const) : ("stable" as const),
|
|
1356
|
+
tags: baseTags.slice(0, 40),
|
|
1357
|
+
sourceEpisodeIds: [episodeId],
|
|
1358
|
+
evidence: [],
|
|
1359
|
+
scope: { ...ep.scope, project: ep.project },
|
|
1360
|
+
createdAt: ep.startedAt ?? `${ep.date}T00:00:00.000Z`,
|
|
1361
|
+
updatedAt: now,
|
|
1362
|
+
filesModified: ep.filesModified,
|
|
1363
|
+
toolsUsed: ep.toolsUsed,
|
|
1364
|
+
entities: [],
|
|
1365
|
+
facetIds: facets.map((facet) => facet.id),
|
|
1366
|
+
derivedSemanticIds: [],
|
|
1367
|
+
derivedProcedureIds: [],
|
|
1368
|
+
consolidatedAt: ep.consolidated ? now : undefined,
|
|
1369
|
+
structuralAnchor: this.currentStructuralAnchor(),
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
return { episode, facets, links };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private makeEpisodeMemoryId(ep: Episode): string {
|
|
1376
|
+
return `episode:${ep.sessionId}`;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** Deduplicate all memory and work entries, merge relatedIds, save. Returns counts of removed duplicates. */
|
|
1380
|
+
async deduplicateAll(): Promise<{
|
|
1381
|
+
knowledge: number;
|
|
1382
|
+
lessons: number;
|
|
1383
|
+
events: number;
|
|
1384
|
+
preferences: number;
|
|
1385
|
+
facets: number;
|
|
1386
|
+
work: number;
|
|
1387
|
+
total: number;
|
|
1388
|
+
}> {
|
|
1389
|
+
const [knowledge, lessons, events, prefs, facets, work] = await Promise.all([
|
|
1390
|
+
loadEntries(this.knowledgePath),
|
|
1391
|
+
loadEntries(this.lessonsPath),
|
|
1392
|
+
loadEntries(this.eventsPath),
|
|
1393
|
+
loadEntries(this.preferencesPath),
|
|
1394
|
+
loadEntries(this.facetsPath),
|
|
1395
|
+
loadWork(this.workPath),
|
|
1396
|
+
]);
|
|
1397
|
+
|
|
1398
|
+
const k = deduplicateMemoryEntries(knowledge);
|
|
1399
|
+
const l = deduplicateMemoryEntries(lessons);
|
|
1400
|
+
const ev = deduplicateMemoryEntries(events);
|
|
1401
|
+
const p = deduplicateMemoryEntries(prefs);
|
|
1402
|
+
const f = deduplicateMemoryEntries(facets);
|
|
1403
|
+
const w = deduplicateWorkEntries(work);
|
|
1404
|
+
|
|
1405
|
+
const hl = this.cfg.halfLife;
|
|
1406
|
+
const ew = this.cfg.evictionWeights;
|
|
1407
|
+
await Promise.all([
|
|
1408
|
+
saveEntries(this.knowledgePath, k.deduped, this.cfg.maxEntries.knowledge, (e) =>
|
|
1409
|
+
utilityEntry(e, hl, ew),
|
|
1410
|
+
),
|
|
1411
|
+
saveEntries(this.lessonsPath, l.deduped, this.cfg.maxEntries.lessons, (e) =>
|
|
1412
|
+
utilityEntry(e, hl, ew),
|
|
1413
|
+
),
|
|
1414
|
+
saveEntries(this.eventsPath, ev.deduped, this.cfg.maxEntries.events, (e) =>
|
|
1415
|
+
utilityEntry(e, hl, ew),
|
|
1416
|
+
),
|
|
1417
|
+
saveEntries(this.preferencesPath, p.deduped, this.cfg.maxEntries.preferences, (e) =>
|
|
1418
|
+
utilityEntry(e, hl, ew),
|
|
1419
|
+
),
|
|
1420
|
+
saveEntries(this.facetsPath, f.deduped, this.cfg.maxEntries.facets, (e) =>
|
|
1421
|
+
utilityEntry(e, hl, ew),
|
|
1422
|
+
),
|
|
1423
|
+
saveWork(this.workPath, w.deduped, this.cfg.maxEntries.work, (wr) =>
|
|
1424
|
+
utilityWork(wr, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
1425
|
+
),
|
|
1426
|
+
]);
|
|
1427
|
+
|
|
1428
|
+
const total =
|
|
1429
|
+
k.removedCount + l.removedCount + ev.removedCount + p.removedCount + f.removedCount + w.removedCount;
|
|
1430
|
+
return {
|
|
1431
|
+
knowledge: k.removedCount,
|
|
1432
|
+
lessons: l.removedCount,
|
|
1433
|
+
events: ev.removedCount,
|
|
1434
|
+
preferences: p.removedCount,
|
|
1435
|
+
facets: f.removedCount,
|
|
1436
|
+
work: w.removedCount,
|
|
1437
|
+
total,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
async searchEntries(query: string, scope?: MemoryScope): Promise<MemoryEntry[]> {
|
|
1442
|
+
await this.autoReviveRelevantArchive(query, scope);
|
|
1443
|
+
const runtime = await this.buildRuntimeMemoryView();
|
|
1444
|
+
const tags = extractTags(query);
|
|
1445
|
+
const v2Primary = runtime.v2SearchEntries
|
|
1446
|
+
.map((entry) => ({
|
|
1447
|
+
entry,
|
|
1448
|
+
score: scoreEntry(entry, entry.project, tags, this.cfg.halfLife, this.cfg.scoreWeights),
|
|
1449
|
+
}))
|
|
1450
|
+
.filter((item) => item.score >= 0.42)
|
|
1451
|
+
.sort((a, b) => b.score - a.score)
|
|
1452
|
+
.map((item) => item.entry);
|
|
1453
|
+
if (v2Primary.length > 0) {
|
|
1454
|
+
const bridgeFacets = runtime.facets
|
|
1455
|
+
.filter((entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle")
|
|
1456
|
+
.map((entry) => ({
|
|
1457
|
+
entry,
|
|
1458
|
+
score: scoreEntry(entry, entry.project, tags, this.cfg.halfLife, this.cfg.scoreWeights),
|
|
1459
|
+
}))
|
|
1460
|
+
.filter((item) => item.score >= 0.38)
|
|
1461
|
+
.sort((a, b) => b.score - a.score)
|
|
1462
|
+
.map((item) => item.entry);
|
|
1463
|
+
return [...v2Primary, ...bridgeFacets].slice(0, 12);
|
|
1464
|
+
}
|
|
1465
|
+
const all = [...runtime.knowledge, ...runtime.lessons, ...runtime.events, ...runtime.preferences, ...runtime.facets];
|
|
1466
|
+
return all
|
|
1467
|
+
.map((entry) => ({
|
|
1468
|
+
entry,
|
|
1469
|
+
score:
|
|
1470
|
+
tagOverlap(entry.tags, tags) +
|
|
1471
|
+
(entry.relations ?? [])
|
|
1472
|
+
.filter((relation) => tags.some((tag) => relation.kind.includes(tag) || relation.id.includes(tag)))
|
|
1473
|
+
.reduce((sum, relation) => sum + relation.weight * 0.12, 0),
|
|
1474
|
+
}))
|
|
1475
|
+
.filter((item) => item.score > 0)
|
|
1476
|
+
.sort((a, b) => b.score - a.score)
|
|
1477
|
+
.map((item) => item.entry);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
async autoReviveRelevantArchive(
|
|
1481
|
+
query: string,
|
|
1482
|
+
scope?: MemoryScope,
|
|
1483
|
+
options?: { maxItems?: number; legacyThreshold?: number; v2Threshold?: number },
|
|
1484
|
+
): Promise<Array<{ id: string; location: "knowledge" | "lessons" | "events" | "preferences" | "facets" | "semantic" | "procedural" | "work" }>> {
|
|
1485
|
+
const trimmed = query.trim();
|
|
1486
|
+
const contextTags = extractTags(trimmed);
|
|
1487
|
+
if (!trimmed || contextTags.length === 0) return [];
|
|
1488
|
+
|
|
1489
|
+
const project = (scope as { project?: string } | undefined)?.project ?? "";
|
|
1490
|
+
const hl = this.cfg.halfLife;
|
|
1491
|
+
const sw = this.cfg.scoreWeights;
|
|
1492
|
+
const maxItems = options?.maxItems ?? NanoMemEngine.AUTO_REVIVE_MAX_ITEMS;
|
|
1493
|
+
const legacyThreshold = options?.legacyThreshold ?? 0.82;
|
|
1494
|
+
const v2Threshold = options?.v2Threshold ?? 0.8;
|
|
1495
|
+
const revived: Array<{ id: string; location: "knowledge" | "lessons" | "events" | "preferences" | "facets" | "semantic" | "procedural" | "work" }> = [];
|
|
1496
|
+
|
|
1497
|
+
const archive = await this.exportArchive();
|
|
1498
|
+
const legacyCandidates = [
|
|
1499
|
+
...archive.knowledge.map((entry) => ({ entry, location: "knowledge" as const })),
|
|
1500
|
+
...archive.lessons.map((entry) => ({ entry, location: "lessons" as const })),
|
|
1501
|
+
...archive.events.map((entry) => ({ entry, location: "events" as const })),
|
|
1502
|
+
...archive.preferences.map((entry) => ({ entry, location: "preferences" as const })),
|
|
1503
|
+
...archive.facets.map((entry) => ({ entry, location: "facets" as const })),
|
|
1504
|
+
]
|
|
1505
|
+
.filter(({ entry }) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
|
|
1506
|
+
.filter(({ entry }) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
|
|
1507
|
+
.map(({ entry, location }) => ({
|
|
1508
|
+
id: entry.id,
|
|
1509
|
+
location,
|
|
1510
|
+
score: scoreEntry(entry, project || entry.project, contextTags, hl, sw),
|
|
1511
|
+
}))
|
|
1512
|
+
.filter((candidate) => candidate.score >= legacyThreshold)
|
|
1513
|
+
.sort((a, b) => b.score - a.score)
|
|
1514
|
+
.slice(0, maxItems);
|
|
1515
|
+
|
|
1516
|
+
for (const candidate of legacyCandidates) {
|
|
1517
|
+
const result = await this.restoreArchivedEntry(candidate.id);
|
|
1518
|
+
if (result.ok && result.location) revived.push({ id: candidate.id, location: result.location });
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (revived.length >= maxItems) return revived;
|
|
1522
|
+
|
|
1523
|
+
const semanticCandidates = archive.semantic
|
|
1524
|
+
.filter((entry) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
|
|
1525
|
+
.filter((entry) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
|
|
1526
|
+
.filter((entry) => !project || !entry.scope?.project || entry.scope.project === project)
|
|
1527
|
+
.map((entry) => ({ id: entry.id, location: "semantic" as const, score: this.scoreV2SemanticMemory(entry, project, contextTags) }))
|
|
1528
|
+
.filter((candidate) => candidate.score >= v2Threshold);
|
|
1529
|
+
const proceduralCandidates = archive.procedural
|
|
1530
|
+
.filter((entry) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
|
|
1531
|
+
.filter((entry) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
|
|
1532
|
+
.filter((entry) => !project || !entry.scope?.project || entry.scope.project === project)
|
|
1533
|
+
.map((entry) => ({
|
|
1534
|
+
id: entry.id,
|
|
1535
|
+
location: "procedural" as const,
|
|
1536
|
+
// Archived procedures are often older superseded versions, so revive scoring should not punish status as heavily.
|
|
1537
|
+
score: this.scoreProceduralMemory(entry, project, contextTags) + (entry.status === "superseded" || entry.status === "deprecated" ? 0.24 : 0),
|
|
1538
|
+
}))
|
|
1539
|
+
.filter((candidate) => candidate.score >= v2Threshold);
|
|
1540
|
+
|
|
1541
|
+
const v2Candidates = [...semanticCandidates, ...proceduralCandidates]
|
|
1542
|
+
.sort((a, b) => b.score - a.score)
|
|
1543
|
+
.slice(0, Math.max(0, maxItems - revived.length));
|
|
1544
|
+
|
|
1545
|
+
for (const candidate of v2Candidates) {
|
|
1546
|
+
const result = await this.restoreArchivedEntry(candidate.id);
|
|
1547
|
+
if (result.ok && result.location) revived.push({ id: candidate.id, location: result.location });
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
return revived;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
async getAlignmentSnapshot(): Promise<AlignmentSnapshot> {
|
|
1554
|
+
const all = await this.buildRuntimeMemoryView();
|
|
1555
|
+
const memoryEntries = [...all.knowledge, ...all.lessons, ...all.events, ...all.preferences, ...all.facets];
|
|
1556
|
+
const sortByInfluence = (entries: MemoryEntry[]) =>
|
|
1557
|
+
[...entries].sort((a, b) => {
|
|
1558
|
+
const aScore =
|
|
1559
|
+
(a.salience ?? a.importance) * ((a.accessCount ?? 0) + 1) + (a.retention === "core" ? 2 : 0);
|
|
1560
|
+
const bScore =
|
|
1561
|
+
(b.salience ?? b.importance) * ((b.accessCount ?? 0) + 1) + (b.retention === "core" ? 2 : 0);
|
|
1562
|
+
return bScore - aScore;
|
|
1563
|
+
});
|
|
1564
|
+
const identityCore = sortByInfluence(
|
|
1565
|
+
memoryEntries.filter((entry) => entry.stability !== "situational" && entry.retention === "core"),
|
|
1566
|
+
).slice(0, 8);
|
|
1567
|
+
const keyEvents = sortByInfluence(
|
|
1568
|
+
memoryEntries.filter((entry) => entry.type === "event" || entry.retention === "key-event"),
|
|
1569
|
+
).slice(0, 8);
|
|
1570
|
+
const behaviorDrivers = sortByInfluence(
|
|
1571
|
+
memoryEntries.filter((entry) => entry.type === "pattern" || entry.type === "preference" || entry.type === "lesson"),
|
|
1572
|
+
).slice(0, 8);
|
|
1573
|
+
const currentState = sortByInfluence(
|
|
1574
|
+
memoryEntries.filter((entry) => entry.stability === "situational" || entry.stateData),
|
|
1575
|
+
).slice(0, 6);
|
|
1576
|
+
const relationshipEdges = sortByInfluence(memoryEntries)
|
|
1577
|
+
.flatMap((entry) =>
|
|
1578
|
+
(entry.relations ?? []).map((relation) => ({
|
|
1579
|
+
fromId: entry.id,
|
|
1580
|
+
toId: relation.id,
|
|
1581
|
+
kind: relation.kind,
|
|
1582
|
+
weight: relation.weight,
|
|
1583
|
+
})),
|
|
1584
|
+
)
|
|
1585
|
+
.sort((a, b) => b.weight - a.weight)
|
|
1586
|
+
.slice(0, 12);
|
|
1587
|
+
const conflicts = this.detectAlignmentConflicts(memoryEntries).slice(0, 8);
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
identityCore,
|
|
1591
|
+
keyEvents,
|
|
1592
|
+
behaviorDrivers,
|
|
1593
|
+
currentState,
|
|
1594
|
+
relationshipEdges,
|
|
1595
|
+
conflicts,
|
|
1596
|
+
generatedAt: new Date().toISOString(),
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
async forgetEntry(id: string): Promise<boolean> {
|
|
1601
|
+
const paths = [this.knowledgePath, this.lessonsPath, this.eventsPath, this.preferencesPath, this.facetsPath];
|
|
1602
|
+
for (const path of paths) {
|
|
1603
|
+
const entries = await loadEntries(path);
|
|
1604
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
1605
|
+
if (idx >= 0) {
|
|
1606
|
+
const forgotten = entries[idx]!;
|
|
1607
|
+
entries.splice(idx, 1);
|
|
1608
|
+
const hl = this.cfg.halfLife;
|
|
1609
|
+
const ew = this.cfg.evictionWeights;
|
|
1610
|
+
await saveEntries(path, entries, Infinity, (e) => utilityEntry(e, hl, ew));
|
|
1611
|
+
const archivePath = path.replace(`${this.cfg.memoryDir}/`, `${this.cfg.memoryDir}/_archive/`);
|
|
1612
|
+
const archived = await loadEntries(archivePath);
|
|
1613
|
+
if (archived.some((entry) => entry.id === id)) await saveEntries(archivePath, archived.filter((entry) => entry.id !== id), Infinity, (e) => utilityEntry(e, hl, ew));
|
|
1614
|
+
const removedIds = new Set<string>([id]);
|
|
1615
|
+
for (const v2Paths of [this.v2Paths, this.archiveV2Paths]) {
|
|
1616
|
+
const semantic = await loadV2Semantic(v2Paths);
|
|
1617
|
+
const retained = semantic.filter((entry) => {
|
|
1618
|
+
const matches = (entry.legacyIds?.includes(id) || (entry.name === forgotten.name && entry.summary === forgotten.summary && entry.scope?.project === forgotten.project));
|
|
1619
|
+
if (matches) removedIds.add(entry.id);
|
|
1620
|
+
return !matches;
|
|
1621
|
+
});
|
|
1622
|
+
if (retained.length !== semantic.length) await saveV2Semantic(v2Paths, retained);
|
|
1623
|
+
}
|
|
1624
|
+
for (const v2Paths of [this.v2Paths, this.archiveV2Paths]) {
|
|
1625
|
+
const links = await loadV2Links(v2Paths);
|
|
1626
|
+
if (links.some((link) => removedIds.has(link.fromId) || removedIds.has(link.toId))) {
|
|
1627
|
+
await saveV2Links(v2Paths, links.filter((link) => !removedIds.has(link.fromId) && !removedIds.has(link.toId)));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const index = await loadEmbeddingIndex(this.cfg.memoryDir);
|
|
1631
|
+
if (index.records.some((record) => removedIds.has(record.memoryId))) {
|
|
1632
|
+
await saveEmbeddingIndex(this.cfg.memoryDir, { ...index, records: index.records.filter((record) => !removedIds.has(record.memoryId)) });
|
|
1633
|
+
}
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const semantic = (await loadV2Semantic(this.v2Paths)).find(entry => entry.id === id);
|
|
1638
|
+
if (semantic) {
|
|
1639
|
+
const legacy = Object.values(await this.getAllEntries()).flat().find(entry => (semantic.legacyIds?.includes(entry.id) || (entry.name === semantic.name && entry.summary === semantic.summary && entry.project === semantic.scope?.project)));
|
|
1640
|
+
if (legacy) return this.forgetEntry(legacy.id);
|
|
1641
|
+
}
|
|
1642
|
+
let removed = false;
|
|
1643
|
+
for (const paths of [this.v2Paths, this.archiveV2Paths]) {
|
|
1644
|
+
const semantic = await loadV2Semantic(paths);
|
|
1645
|
+
const procedural = await loadV2Procedural(paths);
|
|
1646
|
+
if (semantic.some(entry => entry.id === id)) { await saveV2Semantic(paths, semantic.filter(entry => entry.id !== id)); removed = true; }
|
|
1647
|
+
if (procedural.some(entry => entry.id === id)) { await saveV2Procedural(paths, procedural.filter(entry => entry.id !== id)); removed = true; }
|
|
1648
|
+
const links = await loadV2Links(paths);
|
|
1649
|
+
await saveV2Links(paths, links.filter(link => link.fromId !== id && link.toId !== id));
|
|
1650
|
+
}
|
|
1651
|
+
if (removed) {
|
|
1652
|
+
const index = await loadEmbeddingIndex(this.cfg.memoryDir);
|
|
1653
|
+
await saveEmbeddingIndex(this.cfg.memoryDir, { ...index, records: index.records.filter(record => record.memoryId !== id) });
|
|
1654
|
+
}
|
|
1655
|
+
return removed;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
async exportAll(): Promise<{
|
|
1659
|
+
knowledge: MemoryEntry[];
|
|
1660
|
+
lessons: MemoryEntry[];
|
|
1661
|
+
events: MemoryEntry[];
|
|
1662
|
+
preferences: MemoryEntry[];
|
|
1663
|
+
facets: MemoryEntry[];
|
|
1664
|
+
work: WorkEntry[];
|
|
1665
|
+
episodes: Episode[];
|
|
1666
|
+
meta: Meta;
|
|
1667
|
+
}> {
|
|
1668
|
+
const [knowledge, lessons, events, preferences, facets, work, episodes, meta] = await Promise.all([
|
|
1669
|
+
loadEntries(this.knowledgePath),
|
|
1670
|
+
loadEntries(this.lessonsPath),
|
|
1671
|
+
loadEntries(this.eventsPath),
|
|
1672
|
+
loadEntries(this.preferencesPath),
|
|
1673
|
+
loadEntries(this.facetsPath),
|
|
1674
|
+
loadWork(this.workPath),
|
|
1675
|
+
loadEpisodes(this.episodesDir),
|
|
1676
|
+
loadMeta(this.metaPath),
|
|
1677
|
+
]);
|
|
1678
|
+
return { knowledge, lessons, events, preferences, facets, work, episodes, meta };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async exportArchive(): Promise<{
|
|
1682
|
+
knowledge: MemoryEntry[];
|
|
1683
|
+
lessons: MemoryEntry[];
|
|
1684
|
+
events: MemoryEntry[];
|
|
1685
|
+
preferences: MemoryEntry[];
|
|
1686
|
+
facets: MemoryEntry[];
|
|
1687
|
+
work: WorkEntry[];
|
|
1688
|
+
semantic: SemanticMemory[];
|
|
1689
|
+
procedural: ProceduralMemory[];
|
|
1690
|
+
}> {
|
|
1691
|
+
const [knowledge, lessons, events, preferences, facets, work, semantic, procedural] = await Promise.all([
|
|
1692
|
+
loadEntries(this.archiveKnowledgePath),
|
|
1693
|
+
loadEntries(this.archiveLessonsPath),
|
|
1694
|
+
loadEntries(this.archiveEventsPath),
|
|
1695
|
+
loadEntries(this.archivePreferencesPath),
|
|
1696
|
+
loadEntries(this.archiveFacetsPath),
|
|
1697
|
+
loadWork(this.archiveWorkPath),
|
|
1698
|
+
loadV2Semantic(this.archiveV2Paths),
|
|
1699
|
+
loadV2Procedural(this.archiveV2Paths),
|
|
1700
|
+
]);
|
|
1701
|
+
return { knowledge, lessons, events, preferences, facets, work, semantic, procedural };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
async exportAllV2(): Promise<{
|
|
1705
|
+
episodes: EpisodeMemory[];
|
|
1706
|
+
facets: EpisodeFacet[];
|
|
1707
|
+
semantic: import("./types-v2.js").SemanticMemory[];
|
|
1708
|
+
procedural: ProceduralMemory[];
|
|
1709
|
+
links: MemoryLink[];
|
|
1710
|
+
state: import("./types-v2.js").StateMemory[];
|
|
1711
|
+
}> {
|
|
1712
|
+
const [episodes, facets, semantic, procedural, links, state] = await Promise.all([
|
|
1713
|
+
loadV2Episodes(this.v2Paths),
|
|
1714
|
+
loadV2Facets(this.v2Paths),
|
|
1715
|
+
loadV2Semantic(this.v2Paths),
|
|
1716
|
+
loadV2Procedural(this.v2Paths),
|
|
1717
|
+
loadV2Links(this.v2Paths),
|
|
1718
|
+
loadV2State(this.v2Paths),
|
|
1719
|
+
]);
|
|
1720
|
+
return { episodes, facets, semantic, procedural, links, state };
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
async rebuildV2Links(): Promise<{ total: number; auto: number }> {
|
|
1724
|
+
const snapshot = await this.exportAllV2();
|
|
1725
|
+
const nextLinks = this.materializeV2Links(snapshot.semantic, snapshot.procedural, snapshot.links);
|
|
1726
|
+
await saveV2Links(this.v2Paths, nextLinks);
|
|
1727
|
+
return {
|
|
1728
|
+
total: nextLinks.length,
|
|
1729
|
+
auto: nextLinks.filter((link) => link.id.startsWith(NanoMemEngine.AUTO_V2_LINK_PREFIX)).length,
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
async archiveStaleMemories(now = new Date().toISOString()): Promise<{
|
|
1734
|
+
knowledge: number;
|
|
1735
|
+
lessons: number;
|
|
1736
|
+
events: number;
|
|
1737
|
+
preferences: number;
|
|
1738
|
+
facets: number;
|
|
1739
|
+
work: number;
|
|
1740
|
+
semantic: number;
|
|
1741
|
+
procedural: number;
|
|
1742
|
+
total: number;
|
|
1743
|
+
}> {
|
|
1744
|
+
const [knowledge, lessons, events, preferences, facets, work, archived, semantic, procedural, archivedSemantic, archivedProcedural] =
|
|
1745
|
+
await Promise.all([
|
|
1746
|
+
loadEntries(this.knowledgePath),
|
|
1747
|
+
loadEntries(this.lessonsPath),
|
|
1748
|
+
loadEntries(this.eventsPath),
|
|
1749
|
+
loadEntries(this.preferencesPath),
|
|
1750
|
+
loadEntries(this.facetsPath),
|
|
1751
|
+
loadWork(this.workPath),
|
|
1752
|
+
this.exportArchive(),
|
|
1753
|
+
loadV2Semantic(this.v2Paths),
|
|
1754
|
+
loadV2Procedural(this.v2Paths),
|
|
1755
|
+
loadV2Semantic(this.archiveV2Paths),
|
|
1756
|
+
loadV2Procedural(this.archiveV2Paths),
|
|
1757
|
+
]);
|
|
1758
|
+
|
|
1759
|
+
const knowledgeResult = this.partitionArchivedEntries(knowledge, now);
|
|
1760
|
+
const lessonsResult = this.partitionArchivedEntries(lessons, now);
|
|
1761
|
+
const eventsResult = this.partitionArchivedEntries(events, now);
|
|
1762
|
+
const preferencesResult = this.partitionArchivedEntries(preferences, now);
|
|
1763
|
+
const facetsResult = this.partitionArchivedEntries(facets, now);
|
|
1764
|
+
const workResult = this.partitionArchivedWork(work, now);
|
|
1765
|
+
const semanticResult = this.partitionArchivedSemantic(semantic, now);
|
|
1766
|
+
const proceduralResult = this.partitionArchivedProcedural(procedural, now);
|
|
1767
|
+
|
|
1768
|
+
const hl = this.cfg.halfLife;
|
|
1769
|
+
const ew = this.cfg.evictionWeights;
|
|
1770
|
+
await Promise.all([
|
|
1771
|
+
saveEntries(this.knowledgePath, knowledgeResult.active, this.cfg.maxEntries.knowledge, (entry) => utilityEntry(entry, hl, ew)),
|
|
1772
|
+
saveEntries(this.lessonsPath, lessonsResult.active, this.cfg.maxEntries.lessons, (entry) => utilityEntry(entry, hl, ew)),
|
|
1773
|
+
saveEntries(this.eventsPath, eventsResult.active, this.cfg.maxEntries.events, (entry) => utilityEntry(entry, hl, ew)),
|
|
1774
|
+
saveEntries(this.preferencesPath, preferencesResult.active, this.cfg.maxEntries.preferences, (entry) => utilityEntry(entry, hl, ew)),
|
|
1775
|
+
saveEntries(this.facetsPath, facetsResult.active, this.cfg.maxEntries.facets, (entry) => utilityEntry(entry, hl, ew)),
|
|
1776
|
+
saveWork(this.workPath, workResult.active, this.cfg.maxEntries.work, (entry) => utilityWork(entry, this.cfg.halfLife, this.cfg.evictionWeights)),
|
|
1777
|
+
saveEntries(this.archiveKnowledgePath, this.mergeArchivedEntries(archived.knowledge, knowledgeResult.archived), Infinity, (entry) =>
|
|
1778
|
+
utilityEntry(entry, hl, ew),
|
|
1779
|
+
),
|
|
1780
|
+
saveEntries(this.archiveLessonsPath, this.mergeArchivedEntries(archived.lessons, lessonsResult.archived), Infinity, (entry) =>
|
|
1781
|
+
utilityEntry(entry, hl, ew),
|
|
1782
|
+
),
|
|
1783
|
+
saveEntries(this.archiveEventsPath, this.mergeArchivedEntries(archived.events, eventsResult.archived), Infinity, (entry) =>
|
|
1784
|
+
utilityEntry(entry, hl, ew),
|
|
1785
|
+
),
|
|
1786
|
+
saveEntries(
|
|
1787
|
+
this.archivePreferencesPath,
|
|
1788
|
+
this.mergeArchivedEntries(archived.preferences, preferencesResult.archived),
|
|
1789
|
+
Infinity,
|
|
1790
|
+
(entry) => utilityEntry(entry, hl, ew),
|
|
1791
|
+
),
|
|
1792
|
+
saveEntries(this.archiveFacetsPath, this.mergeArchivedEntries(archived.facets, facetsResult.archived), Infinity, (entry) =>
|
|
1793
|
+
utilityEntry(entry, hl, ew),
|
|
1794
|
+
),
|
|
1795
|
+
saveWork(
|
|
1796
|
+
this.archiveWorkPath,
|
|
1797
|
+
this.mergeArchivedWork(archived.work, workResult.archived),
|
|
1798
|
+
Infinity,
|
|
1799
|
+
(entry) => utilityWork(entry, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
1800
|
+
),
|
|
1801
|
+
saveV2Semantic(this.v2Paths, semanticResult.active),
|
|
1802
|
+
saveV2Procedural(this.v2Paths, proceduralResult.active),
|
|
1803
|
+
saveV2Semantic(this.archiveV2Paths, this.mergeArchivedV2(archivedSemantic, semanticResult.archived)),
|
|
1804
|
+
saveV2Procedural(this.archiveV2Paths, this.mergeArchivedV2(archivedProcedural, proceduralResult.archived)),
|
|
1805
|
+
]);
|
|
1806
|
+
|
|
1807
|
+
const total =
|
|
1808
|
+
knowledgeResult.archived.length +
|
|
1809
|
+
lessonsResult.archived.length +
|
|
1810
|
+
eventsResult.archived.length +
|
|
1811
|
+
preferencesResult.archived.length +
|
|
1812
|
+
facetsResult.archived.length +
|
|
1813
|
+
workResult.archived.length +
|
|
1814
|
+
semanticResult.archived.length +
|
|
1815
|
+
proceduralResult.archived.length;
|
|
1816
|
+
|
|
1817
|
+
return {
|
|
1818
|
+
knowledge: knowledgeResult.archived.length,
|
|
1819
|
+
lessons: lessonsResult.archived.length,
|
|
1820
|
+
events: eventsResult.archived.length,
|
|
1821
|
+
preferences: preferencesResult.archived.length,
|
|
1822
|
+
facets: facetsResult.archived.length,
|
|
1823
|
+
work: workResult.archived.length,
|
|
1824
|
+
semantic: semanticResult.archived.length,
|
|
1825
|
+
procedural: proceduralResult.archived.length,
|
|
1826
|
+
total,
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
async restoreArchivedEntry(id: string): Promise<{
|
|
1831
|
+
ok: boolean;
|
|
1832
|
+
location?: "knowledge" | "lessons" | "events" | "preferences" | "facets" | "work" | "semantic" | "procedural";
|
|
1833
|
+
}> {
|
|
1834
|
+
const hl = this.cfg.halfLife;
|
|
1835
|
+
const ew = this.cfg.evictionWeights;
|
|
1836
|
+
const restoreLegacy = async (
|
|
1837
|
+
activePath: string,
|
|
1838
|
+
archivePath: string,
|
|
1839
|
+
location: "knowledge" | "lessons" | "events" | "preferences" | "facets",
|
|
1840
|
+
): Promise<{ ok: boolean; location?: typeof location }> => {
|
|
1841
|
+
const [active, archived] = await Promise.all([loadEntries(activePath), loadEntries(archivePath)]);
|
|
1842
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1843
|
+
if (index < 0) return { ok: false };
|
|
1844
|
+
const [entry] = archived.splice(index, 1);
|
|
1845
|
+
active.push({
|
|
1846
|
+
...entry,
|
|
1847
|
+
archivedAt: undefined,
|
|
1848
|
+
archiveReason: undefined,
|
|
1849
|
+
revivedAt: new Date().toISOString(),
|
|
1850
|
+
});
|
|
1851
|
+
await Promise.all([
|
|
1852
|
+
saveEntries(activePath, active, Infinity, (candidate) => utilityEntry(candidate, hl, ew)),
|
|
1853
|
+
saveEntries(archivePath, archived, Infinity, (candidate) => utilityEntry(candidate, hl, ew)),
|
|
1854
|
+
]);
|
|
1855
|
+
return { ok: true, location };
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
for (const [activePath, archivePath, location] of [
|
|
1859
|
+
[this.knowledgePath, this.archiveKnowledgePath, "knowledge"],
|
|
1860
|
+
[this.lessonsPath, this.archiveLessonsPath, "lessons"],
|
|
1861
|
+
[this.eventsPath, this.archiveEventsPath, "events"],
|
|
1862
|
+
[this.preferencesPath, this.archivePreferencesPath, "preferences"],
|
|
1863
|
+
[this.facetsPath, this.archiveFacetsPath, "facets"],
|
|
1864
|
+
] as const) {
|
|
1865
|
+
const result = await restoreLegacy(activePath, archivePath, location);
|
|
1866
|
+
if (result.ok) return result;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
{
|
|
1870
|
+
const [active, archived] = await Promise.all([loadWork(this.workPath), loadWork(this.archiveWorkPath)]);
|
|
1871
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1872
|
+
if (index >= 0) {
|
|
1873
|
+
const [entry] = archived.splice(index, 1);
|
|
1874
|
+
active.push({
|
|
1875
|
+
...entry,
|
|
1876
|
+
archivedAt: undefined,
|
|
1877
|
+
archiveReason: undefined,
|
|
1878
|
+
revivedAt: new Date().toISOString(),
|
|
1879
|
+
});
|
|
1880
|
+
await Promise.all([
|
|
1881
|
+
saveWork(this.workPath, active, Infinity, (candidate) => utilityWork(candidate, this.cfg.halfLife, this.cfg.evictionWeights)),
|
|
1882
|
+
saveWork(
|
|
1883
|
+
this.archiveWorkPath,
|
|
1884
|
+
archived,
|
|
1885
|
+
Infinity,
|
|
1886
|
+
(candidate) => utilityWork(candidate, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
1887
|
+
),
|
|
1888
|
+
]);
|
|
1889
|
+
return { ok: true, location: "work" };
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
{
|
|
1894
|
+
const [active, archived] = await Promise.all([loadV2Semantic(this.v2Paths), loadV2Semantic(this.archiveV2Paths)]);
|
|
1895
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1896
|
+
if (index >= 0) {
|
|
1897
|
+
const [entry] = archived.splice(index, 1);
|
|
1898
|
+
active.push({
|
|
1899
|
+
...entry,
|
|
1900
|
+
archivedAt: undefined,
|
|
1901
|
+
archiveReason: undefined,
|
|
1902
|
+
revivedAt: new Date().toISOString(),
|
|
1903
|
+
});
|
|
1904
|
+
await Promise.all([saveV2Semantic(this.v2Paths, active), saveV2Semantic(this.archiveV2Paths, archived)]);
|
|
1905
|
+
await this.rebuildV2Links();
|
|
1906
|
+
return { ok: true, location: "semantic" };
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
{
|
|
1911
|
+
const [active, archived] = await Promise.all([loadV2Procedural(this.v2Paths), loadV2Procedural(this.archiveV2Paths)]);
|
|
1912
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1913
|
+
if (index >= 0) {
|
|
1914
|
+
const [entry] = archived.splice(index, 1);
|
|
1915
|
+
active.push({
|
|
1916
|
+
...entry,
|
|
1917
|
+
archivedAt: undefined,
|
|
1918
|
+
archiveReason: undefined,
|
|
1919
|
+
revivedAt: new Date().toISOString(),
|
|
1920
|
+
});
|
|
1921
|
+
await Promise.all([saveV2Procedural(this.v2Paths, active), saveV2Procedural(this.archiveV2Paths, archived)]);
|
|
1922
|
+
await this.rebuildV2Links();
|
|
1923
|
+
return { ok: true, location: "procedural" };
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return { ok: false };
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
async inspectV2Memory(project = "", scope?: MemoryScope): Promise<{
|
|
1931
|
+
counts: {
|
|
1932
|
+
episodes: number;
|
|
1933
|
+
facets: number;
|
|
1934
|
+
semantic: number;
|
|
1935
|
+
procedural: number;
|
|
1936
|
+
activeProcedural: number;
|
|
1937
|
+
supersededProcedural: number;
|
|
1938
|
+
semanticConflicts: number;
|
|
1939
|
+
proceduralConflicts: number;
|
|
1940
|
+
procedureChains: number;
|
|
1941
|
+
};
|
|
1942
|
+
procedureChains: Array<{
|
|
1943
|
+
rootId: string;
|
|
1944
|
+
name: string;
|
|
1945
|
+
status: ProceduralMemory["status"];
|
|
1946
|
+
versionDepth: number;
|
|
1947
|
+
ids: string[];
|
|
1948
|
+
}>;
|
|
1949
|
+
proceduralConflicts: Array<{
|
|
1950
|
+
aId: string;
|
|
1951
|
+
bId: string;
|
|
1952
|
+
aName: string;
|
|
1953
|
+
bName: string;
|
|
1954
|
+
score: number;
|
|
1955
|
+
reason: string;
|
|
1956
|
+
}>;
|
|
1957
|
+
semanticConflicts: Array<{
|
|
1958
|
+
aId: string;
|
|
1959
|
+
bId: string;
|
|
1960
|
+
aName: string;
|
|
1961
|
+
bName: string;
|
|
1962
|
+
reason: string;
|
|
1963
|
+
}>;
|
|
1964
|
+
}> {
|
|
1965
|
+
const snapshot = await this.exportAllV2();
|
|
1966
|
+
const episodes = this.filterAndCleanV2Episodes(snapshot.episodes, project, scope);
|
|
1967
|
+
const facets = this.filterAndCleanV2Facets(snapshot.facets, project, scope);
|
|
1968
|
+
const semantic = this.filterAndCleanV2Semantic(snapshot.semantic, project, scope);
|
|
1969
|
+
const allProcedural = snapshot.procedural.filter((entry) => {
|
|
1970
|
+
if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId) return false;
|
|
1971
|
+
if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId) return false;
|
|
1972
|
+
if (project && entry.scope?.project && entry.scope.project !== project) return false;
|
|
1973
|
+
return true;
|
|
1974
|
+
});
|
|
1975
|
+
const activeProcedural = allProcedural.filter((entry) => entry.status !== "deprecated" && entry.status !== "superseded");
|
|
1976
|
+
const procedureChains = this.buildProceduralChains(allProcedural);
|
|
1977
|
+
const proceduralConflicts = this.detectProceduralConflicts(activeProcedural);
|
|
1978
|
+
const semanticConflicts = this.detectSemanticConflicts(semantic);
|
|
1979
|
+
|
|
1980
|
+
return {
|
|
1981
|
+
counts: {
|
|
1982
|
+
episodes: episodes.length,
|
|
1983
|
+
facets: facets.length,
|
|
1984
|
+
semantic: semantic.length,
|
|
1985
|
+
procedural: allProcedural.length,
|
|
1986
|
+
activeProcedural: activeProcedural.length,
|
|
1987
|
+
supersededProcedural: allProcedural.filter((entry) => entry.status === "superseded").length,
|
|
1988
|
+
semanticConflicts: semanticConflicts.length,
|
|
1989
|
+
proceduralConflicts: proceduralConflicts.length,
|
|
1990
|
+
procedureChains: procedureChains.length,
|
|
1991
|
+
},
|
|
1992
|
+
procedureChains,
|
|
1993
|
+
proceduralConflicts,
|
|
1994
|
+
semanticConflicts,
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// ─── Full Insights Report ────────────────────────────────────────
|
|
1999
|
+
|
|
2000
|
+
async generateFullInsights(): Promise<FullInsightsReport> {
|
|
2001
|
+
const all = await this.buildRuntimeMemoryView();
|
|
2002
|
+
return buildFullInsightsReport(all, this.llmFn, this.cfg.locale);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/**
|
|
2006
|
+
* Generate enhanced insights report (includes human-readable insights + developer persona + root cause analysis)
|
|
2007
|
+
*/
|
|
2008
|
+
async generateEnhancedInsights(): Promise<{
|
|
2009
|
+
report: FullInsightsReport;
|
|
2010
|
+
persona?: DeveloperPersona;
|
|
2011
|
+
humanInsights: HumanInsight[];
|
|
2012
|
+
rootCauses: RootCauseInsight[];
|
|
2013
|
+
}> {
|
|
2014
|
+
const all = await this.buildRuntimeMemoryView();
|
|
2015
|
+
|
|
2016
|
+
// Generate base report and human-readable insights in parallel
|
|
2017
|
+
const [baseReport, humanData] = await Promise.all([
|
|
2018
|
+
buildFullInsightsReport(all, this.llmFn, this.cfg.locale),
|
|
2019
|
+
generateHumanInsights(all, this.llmFn, this.cfg.locale),
|
|
2020
|
+
]);
|
|
2021
|
+
|
|
2022
|
+
return {
|
|
2023
|
+
report: baseReport,
|
|
2024
|
+
persona: humanData.persona,
|
|
2025
|
+
humanInsights: humanData.humanInsights,
|
|
2026
|
+
rootCauses: humanData.rootCauses,
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// ─── Insights Generation ─────────────────────────────────────
|
|
2031
|
+
|
|
2032
|
+
async generateInsights(): Promise<InsightsReport> {
|
|
2033
|
+
const all = await this.buildRuntimeMemoryView();
|
|
2034
|
+
return generateInsightsReport(all, this.cfg.locale, this.llmFn);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// ─── Private Helpers ─────────────────────────────────────────
|
|
2038
|
+
|
|
2039
|
+
private getMemoryPathConfigs(): Array<{ path: string; max: number }> {
|
|
2040
|
+
return [
|
|
2041
|
+
{ path: this.knowledgePath, max: this.cfg.maxEntries.knowledge },
|
|
2042
|
+
{ path: this.lessonsPath, max: this.cfg.maxEntries.lessons },
|
|
2043
|
+
{ path: this.eventsPath, max: this.cfg.maxEntries.events },
|
|
2044
|
+
{ path: this.preferencesPath, max: this.cfg.maxEntries.preferences },
|
|
2045
|
+
{ path: this.facetsPath, max: this.cfg.maxEntries.facets },
|
|
2046
|
+
];
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
private async findEntryLocation(
|
|
2050
|
+
id: string,
|
|
2051
|
+
): Promise<{ entry: MemoryEntry; entries: MemoryEntry[]; path: string; max: number } | null> {
|
|
2052
|
+
for (const { path, max } of this.getMemoryPathConfigs()) {
|
|
2053
|
+
const entries = await loadEntries(path);
|
|
2054
|
+
const entry = entries.find((candidate) => candidate.id === id);
|
|
2055
|
+
if (entry) return { entry, entries, path, max };
|
|
2056
|
+
}
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
private async persistEntries(path: string, entries: MemoryEntry[], max: number): Promise<void> {
|
|
2061
|
+
const hl = this.cfg.halfLife;
|
|
2062
|
+
const ew = this.cfg.evictionWeights;
|
|
2063
|
+
await saveEntries(path, entries, max, (entry) => utilityEntry(entry, hl, ew));
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
private filterAndCleanEntries(entries: MemoryEntry[], scope?: MemoryScope): MemoryEntry[] {
|
|
2067
|
+
return filterByScope(evictExpiredEntries(entries).filter((entry) => !entry.archivedAt), scope);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
private filterAndCleanWork(entries: WorkEntry[], scope?: MemoryScope): WorkEntry[] {
|
|
2071
|
+
return filterByScope(evictExpiredWork(entries).filter((entry) => !entry.archivedAt), scope);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
private filterAndCleanProcedural(
|
|
2075
|
+
entries: ProceduralMemory[],
|
|
2076
|
+
project: string,
|
|
2077
|
+
scope?: MemoryScope,
|
|
2078
|
+
): ProceduralMemory[] {
|
|
2079
|
+
return entries.filter((entry) => {
|
|
2080
|
+
if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId) return false;
|
|
2081
|
+
if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId) return false;
|
|
2082
|
+
if (project && entry.scope?.project && entry.scope.project !== project) return false;
|
|
2083
|
+
return !entry.archivedAt && entry.status !== "deprecated" && entry.status !== "superseded";
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
private filterAndCleanV2Episodes(entries: EpisodeMemory[], project: string, scope?: MemoryScope): EpisodeMemory[] {
|
|
2088
|
+
return entries.filter((entry) => {
|
|
2089
|
+
if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId) return false;
|
|
2090
|
+
if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId) return false;
|
|
2091
|
+
if (project && entry.scope?.project && entry.scope.project !== project) return false;
|
|
2092
|
+
return !entry.archivedAt;
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
private filterAndCleanV2Facets(entries: EpisodeFacet[], project: string, scope?: MemoryScope): EpisodeFacet[] {
|
|
2097
|
+
return entries.filter((entry) => {
|
|
2098
|
+
if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId) return false;
|
|
2099
|
+
if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId) return false;
|
|
2100
|
+
if (project && entry.scope?.project && entry.scope.project !== project) return false;
|
|
2101
|
+
return !entry.archivedAt;
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
private filterAndCleanV2Semantic(entries: SemanticMemory[], project: string, scope?: MemoryScope): SemanticMemory[] {
|
|
2106
|
+
return entries.filter((entry) => {
|
|
2107
|
+
if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId) return false;
|
|
2108
|
+
if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId) return false;
|
|
2109
|
+
if (project && entry.scope?.project && entry.scope.project !== project) return false;
|
|
2110
|
+
return !entry.archivedAt && !entry.supersededById;
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
private mergeArchivedEntries = mergeArchivedEntriesFn;
|
|
2115
|
+
|
|
2116
|
+
private mergeArchivedWork = mergeArchivedWorkFn;
|
|
2117
|
+
|
|
2118
|
+
private mergeArchivedV2 = mergeArchivedV2Fn;
|
|
2119
|
+
|
|
2120
|
+
private partitionArchivedEntries(entries: MemoryEntry[], now: string) { return partitionArchivedEntriesFn(entries, now, this.cfg.forgetting); }
|
|
2121
|
+
|
|
2122
|
+
private partitionArchivedWork(entries: WorkEntry[], now: string) { return partitionArchivedWorkFn(entries, now, this.cfg.forgetting); }
|
|
2123
|
+
|
|
2124
|
+
private partitionArchivedSemantic(entries: SemanticMemory[], now: string) { return partitionArchivedSemanticFn(entries, now, this.cfg.forgetting); }
|
|
2125
|
+
|
|
2126
|
+
private partitionArchivedProcedural(entries: ProceduralMemory[], now: string) { return partitionArchivedProceduralFn(entries, now, this.cfg.forgetting); }
|
|
2127
|
+
|
|
2128
|
+
private getLegacyArchiveReason(entry: MemoryEntry): string | undefined {
|
|
2129
|
+
const f = this.cfg.forgetting;
|
|
2130
|
+
if (entry.archivedAt) return entry.archiveReason ?? "pre-archived";
|
|
2131
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays) return undefined;
|
|
2132
|
+
const anchor = entry.lastAccessed ?? entry.eventTime ?? entry.created;
|
|
2133
|
+
const ageDays = daysSince(anchor);
|
|
2134
|
+
const lowSignal = (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && (entry.salience ?? entry.importance) <= 6;
|
|
2135
|
+
if (entry.retention === "ambient" && lowSignal && ageDays > f.ambientTtlDays) return "stale-ambient-memory";
|
|
2136
|
+
return undefined;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
private inferSemanticRetention = inferSemanticRetentionFn;
|
|
2140
|
+
|
|
2141
|
+
private inferSemanticStability = inferSemanticStabilityFn;
|
|
2142
|
+
|
|
2143
|
+
private inferSemanticImportance = inferSemanticImportanceFn;
|
|
2144
|
+
|
|
2145
|
+
private mapExtractedItemToSemanticType = mapExtractedItemToSemanticTypeFn;
|
|
2146
|
+
|
|
2147
|
+
private upsertSemanticFromExtractedItem(semantic: SemanticMemory[], item: ExtractedItem, project: string): void {
|
|
2148
|
+
upsertSemanticFromExtractedItemFn(semantic, item, project, this.cfg.defaultScope);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
private semanticKindToLegacyType = semanticKindToLegacyTypeFn;
|
|
2152
|
+
|
|
2153
|
+
private semanticToRuntimeEntry = semanticToRuntimeEntryFn;
|
|
2154
|
+
|
|
2155
|
+
private proceduralToRuntimeEntry = proceduralToRuntimeEntryFn;
|
|
2156
|
+
|
|
2157
|
+
private async buildRuntimeMemoryView(): Promise<{
|
|
2158
|
+
knowledge: MemoryEntry[];
|
|
2159
|
+
lessons: MemoryEntry[];
|
|
2160
|
+
events: MemoryEntry[];
|
|
2161
|
+
preferences: MemoryEntry[];
|
|
2162
|
+
facets: MemoryEntry[];
|
|
2163
|
+
work: WorkEntry[];
|
|
2164
|
+
episodes: Episode[];
|
|
2165
|
+
meta: Meta;
|
|
2166
|
+
v2SearchEntries: MemoryEntry[];
|
|
2167
|
+
}> {
|
|
2168
|
+
const [legacy, work, episodes, meta, semantic, procedural] = await Promise.all([
|
|
2169
|
+
this.exportAll(),
|
|
2170
|
+
this.getAllWork(),
|
|
2171
|
+
this.getAllEpisodes(),
|
|
2172
|
+
loadMeta(this.metaPath),
|
|
2173
|
+
loadV2Semantic(this.v2Paths),
|
|
2174
|
+
loadV2Procedural(this.v2Paths),
|
|
2175
|
+
]);
|
|
2176
|
+
const cleanSemantic = semantic.filter((entry) => !entry.archivedAt && !entry.supersededById);
|
|
2177
|
+
const cleanProcedural = procedural.filter((entry) => !entry.archivedAt && entry.status !== "deprecated");
|
|
2178
|
+
const semanticEntries = cleanSemantic.map((entry) => this.semanticToRuntimeEntry(entry));
|
|
2179
|
+
const proceduralEntries = cleanProcedural.map((entry) => this.proceduralToRuntimeEntry(entry));
|
|
2180
|
+
const v2SignalCount = semanticEntries.length + proceduralEntries.length;
|
|
2181
|
+
const useLegacyFallback = v2SignalCount === 0;
|
|
2182
|
+
|
|
2183
|
+
const byType = (entries: MemoryEntry[], type: MemoryEntry["type"]) => entries.filter((entry) => entry.type === type);
|
|
2184
|
+
const runtimeEntries = [...semanticEntries, ...proceduralEntries];
|
|
2185
|
+
|
|
2186
|
+
return {
|
|
2187
|
+
knowledge: useLegacyFallback ? legacy.knowledge : byType(runtimeEntries, "fact").filter((entry) => entry.type === "fact"),
|
|
2188
|
+
lessons: useLegacyFallback ? legacy.lessons : byType(runtimeEntries, "lesson"),
|
|
2189
|
+
events: useLegacyFallback ? legacy.events : byType(runtimeEntries, "event"),
|
|
2190
|
+
preferences: useLegacyFallback ? legacy.preferences : byType(runtimeEntries, "preference"),
|
|
2191
|
+
facets: legacy.facets,
|
|
2192
|
+
work,
|
|
2193
|
+
episodes,
|
|
2194
|
+
meta,
|
|
2195
|
+
v2SearchEntries: runtimeEntries,
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
private getWorkArchiveReason(entry: WorkEntry): string | undefined {
|
|
2200
|
+
const f = this.cfg.forgetting;
|
|
2201
|
+
if (entry.archivedAt) return entry.archiveReason ?? "pre-archived";
|
|
2202
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays) return undefined;
|
|
2203
|
+
const anchor = entry.lastAccessed ?? entry.eventTime ?? entry.created;
|
|
2204
|
+
const ageDays = daysSince(anchor);
|
|
2205
|
+
if ((entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && ageDays > f.workTtlDays) return "stale-work-memory";
|
|
2206
|
+
return undefined;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
private getSemanticArchiveReason(entry: SemanticMemory): string | undefined {
|
|
2210
|
+
const f = this.cfg.forgetting;
|
|
2211
|
+
if (entry.archivedAt) return entry.archiveReason ?? "pre-archived";
|
|
2212
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays) return undefined;
|
|
2213
|
+
const anchor = entry.lastAccessedAt ?? entry.updatedAt ?? entry.createdAt;
|
|
2214
|
+
const ageDays = daysSince(anchor);
|
|
2215
|
+
if (entry.supersededById && ageDays > f.ambientTtlDays) return "superseded-semantic-memory";
|
|
2216
|
+
const lowSignal = (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && entry.confidence < 0.85;
|
|
2217
|
+
if (entry.retention === "ambient" && lowSignal && ageDays > f.ambientTtlDays * 2) return "stale-semantic-memory";
|
|
2218
|
+
return undefined;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
private getProceduralArchiveReason(entry: ProceduralMemory): string | undefined {
|
|
2222
|
+
const f = this.cfg.forgetting;
|
|
2223
|
+
if (entry.archivedAt) return entry.archiveReason ?? "pre-archived";
|
|
2224
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays) return undefined;
|
|
2225
|
+
const anchor = entry.lastAccessedAt ?? entry.updatedAt ?? entry.createdAt;
|
|
2226
|
+
const ageDays = daysSince(anchor);
|
|
2227
|
+
if ((entry.status === "superseded" || entry.status === "deprecated") && ageDays > f.ambientTtlDays) return "stale-procedure-version";
|
|
2228
|
+
if (entry.status === "draft" && (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && ageDays > f.ambientTtlDays * 2) return "abandoned-draft-procedure";
|
|
2229
|
+
return undefined;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
private buildProceduralChains = buildProceduralChainsFn;
|
|
2233
|
+
|
|
2234
|
+
private materializeV2Links(semantic: SemanticMemory[], procedural: ProceduralMemory[], existingLinks: MemoryLink[], now = new Date().toISOString()): MemoryLink[] {
|
|
2235
|
+
return materializeV2LinksFn(semantic, procedural, existingLinks, AUTO_V2_LINK_PREFIX, now);
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
private detectProceduralConflicts = detectProceduralConflictsFn;
|
|
2239
|
+
|
|
2240
|
+
private detectSemanticConflicts = detectSemanticConflictsFn;
|
|
2241
|
+
|
|
2242
|
+
private buildEmbeddingSourceItems(
|
|
2243
|
+
episodes: EpisodeMemory[],
|
|
2244
|
+
facets: EpisodeFacet[],
|
|
2245
|
+
semantic: SemanticMemory[],
|
|
2246
|
+
procedural: ProceduralMemory[],
|
|
2247
|
+
): EmbeddingSourceItem[] {
|
|
2248
|
+
const episodeItems: EmbeddingSourceItem[] = episodes.map((entry) => ({
|
|
2249
|
+
memoryId: entry.id,
|
|
2250
|
+
memoryKind: "episode",
|
|
2251
|
+
text: [entry.title ?? "", entry.summary, entry.userGoal ?? "", entry.outcome ?? ""].filter(Boolean).join("\n"),
|
|
2252
|
+
}));
|
|
2253
|
+
const facetItems: EmbeddingSourceItem[] = facets.map((entry) => ({
|
|
2254
|
+
memoryId: entry.id,
|
|
2255
|
+
memoryKind: "facet",
|
|
2256
|
+
text: [entry.searchText, entry.summary ?? "", entry.anchorText ?? "", entry.detail ?? ""].filter(Boolean).join("\n"),
|
|
2257
|
+
}));
|
|
2258
|
+
const semanticItems: EmbeddingSourceItem[] = semantic.map((entry) => ({
|
|
2259
|
+
memoryId: entry.id,
|
|
2260
|
+
memoryKind: "semantic",
|
|
2261
|
+
text: [entry.name, entry.summary, entry.detail ?? ""].filter(Boolean).join("\n"),
|
|
2262
|
+
}));
|
|
2263
|
+
const proceduralItems: EmbeddingSourceItem[] = procedural.map((entry) => ({
|
|
2264
|
+
memoryId: entry.id,
|
|
2265
|
+
memoryKind: "procedural",
|
|
2266
|
+
text: [
|
|
2267
|
+
entry.name,
|
|
2268
|
+
entry.searchText,
|
|
2269
|
+
entry.summary,
|
|
2270
|
+
entry.contextText ?? "",
|
|
2271
|
+
entry.boundaries ?? "",
|
|
2272
|
+
entry.steps.map((step) => step.text).join("\n"),
|
|
2273
|
+
]
|
|
2274
|
+
.filter(Boolean)
|
|
2275
|
+
.join("\n"),
|
|
2276
|
+
}));
|
|
2277
|
+
return [...episodeItems, ...facetItems, ...semanticItems, ...proceduralItems];
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
private async querySemanticCandidates(
|
|
2281
|
+
queryText: string,
|
|
2282
|
+
project: string,
|
|
2283
|
+
scope: MemoryScope | undefined,
|
|
2284
|
+
episodes: EpisodeMemory[],
|
|
2285
|
+
facets: EpisodeFacet[],
|
|
2286
|
+
semantic: SemanticMemory[],
|
|
2287
|
+
procedural: ProceduralMemory[],
|
|
2288
|
+
): Promise<Array<{ memoryId: string; memoryKind: "episode" | "facet" | "semantic" | "procedural"; score: number }>> {
|
|
2289
|
+
if (!this.embeddingFn || !queryText.trim()) return [];
|
|
2290
|
+
const items = this.buildEmbeddingSourceItems(episodes, facets, semantic, procedural);
|
|
2291
|
+
if (!items.length) return [];
|
|
2292
|
+
await syncEmbeddingIndex(this.cfg.memoryDir, this.cfg.embeddings.model, items, this.embeddingFn);
|
|
2293
|
+
const matches = await queryEmbeddingIndex(this.cfg.memoryDir, this.cfg.embeddings.model, queryText, this.embeddingFn, 12);
|
|
2294
|
+
const visibleIds = new Set([
|
|
2295
|
+
...episodes.map((entry) => entry.id),
|
|
2296
|
+
...facets.map((entry) => entry.id),
|
|
2297
|
+
...semantic.map((entry) => entry.id),
|
|
2298
|
+
...procedural.map((entry) => entry.id),
|
|
2299
|
+
]);
|
|
2300
|
+
return matches
|
|
2301
|
+
.filter((match) => visibleIds.has(match.memoryId))
|
|
2302
|
+
.map((match) => ({ memoryId: match.memoryId, memoryKind: match.memoryKind, score: match.score }))
|
|
2303
|
+
.filter((match) => match.score >= 0.18);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
private currentStructuralAnchor = currentStructuralAnchor;
|
|
2307
|
+
private computeStructuralBoost = computeStructuralBoost;
|
|
2308
|
+
private scoreEpisodeMemory(entry: EpisodeMemory, project: string, contextTags: string[]): number {
|
|
2309
|
+
return scoreEpisodeMemoryFn(entry, project, contextTags, this.cfg.structuralWeight);
|
|
2310
|
+
}
|
|
2311
|
+
private scoreEpisodeFacet(entry: EpisodeFacet, project: string, contextTags: string[]): number {
|
|
2312
|
+
return scoreEpisodeFacetFn(entry, project, contextTags, this.cfg.structuralWeight);
|
|
2313
|
+
}
|
|
2314
|
+
private scoreV2SemanticMemory(entry: SemanticMemory, project: string, contextTags: string[]): number {
|
|
2315
|
+
return scoreV2SemanticMemoryFn(entry, project, contextTags, this.cfg.structuralWeight);
|
|
2316
|
+
}
|
|
2317
|
+
private scoreProceduralMemory(entry: ProceduralMemory, project: string, contextTags: string[]): number {
|
|
2318
|
+
return scoreProceduralMemoryFn(entry, project, contextTags, this.cfg.structuralWeight);
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
private async reinforceEntries(recalled: MemoryEntry[], all: MemoryEntry[], savePath: string): Promise<void> {
|
|
2322
|
+
const ids = new Set(recalled.map((e) => e.id));
|
|
2323
|
+
const now = new Date().toISOString();
|
|
2324
|
+
for (const entry of all) {
|
|
2325
|
+
if (ids.has(entry.id)) {
|
|
2326
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
2327
|
+
entry.lastAccessed = now;
|
|
2328
|
+
entry.strength = (entry.strength || 30) * this.cfg.strengthGrowthFactor;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
reinforceRelations(recalled, all);
|
|
2332
|
+
const pathConfig = this.getMemoryPathConfigs().find((config) => config.path === savePath);
|
|
2333
|
+
await this.persistEntries(savePath, all, pathConfig?.max ?? this.cfg.maxEntries.knowledge);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
private async reinforceProcedural(recalled: ProceduralMemory[], all: ProceduralMemory[]): Promise<void> {
|
|
2337
|
+
if (!recalled.length) return;
|
|
2338
|
+
const ids = new Set(recalled.map((entry) => entry.id));
|
|
2339
|
+
const now = new Date().toISOString();
|
|
2340
|
+
for (const entry of all) {
|
|
2341
|
+
if (!ids.has(entry.id)) continue;
|
|
2342
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
2343
|
+
entry.lastAccessedAt = now;
|
|
2344
|
+
entry.updatedAt = entry.updatedAt || now;
|
|
2345
|
+
}
|
|
2346
|
+
await saveV2Procedural(this.v2Paths, all);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
private async reinforceEpisodeMemories(recalled: EpisodeMemory[], all: EpisodeMemory[]): Promise<void> {
|
|
2350
|
+
if (!recalled.length) return;
|
|
2351
|
+
const ids = new Set(recalled.map((entry) => entry.id));
|
|
2352
|
+
const now = new Date().toISOString();
|
|
2353
|
+
for (const entry of all) {
|
|
2354
|
+
if (!ids.has(entry.id)) continue;
|
|
2355
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
2356
|
+
entry.lastAccessedAt = now;
|
|
2357
|
+
entry.updatedAt = entry.updatedAt || now;
|
|
2358
|
+
}
|
|
2359
|
+
await saveV2Episodes(this.v2Paths, all);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
private async reinforceEpisodeFacets(recalled: EpisodeFacet[], all: EpisodeFacet[]): Promise<void> {
|
|
2363
|
+
if (!recalled.length) return;
|
|
2364
|
+
const ids = new Set(recalled.map((entry) => entry.id));
|
|
2365
|
+
const now = new Date().toISOString();
|
|
2366
|
+
for (const entry of all) {
|
|
2367
|
+
if (!ids.has(entry.id)) continue;
|
|
2368
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
2369
|
+
entry.lastAccessedAt = now;
|
|
2370
|
+
entry.updatedAt = entry.updatedAt || now;
|
|
2371
|
+
}
|
|
2372
|
+
await saveV2Facets(this.v2Paths, all);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
private async reinforceV2SemanticMemories(recalled: SemanticMemory[], all: SemanticMemory[]): Promise<void> {
|
|
2376
|
+
if (!recalled.length) return;
|
|
2377
|
+
const ids = new Set(recalled.map((entry) => entry.id));
|
|
2378
|
+
const now = new Date().toISOString();
|
|
2379
|
+
for (const entry of all) {
|
|
2380
|
+
if (!ids.has(entry.id)) continue;
|
|
2381
|
+
entry.accessCount = (entry.accessCount ?? 0) + 1;
|
|
2382
|
+
entry.lastAccessedAt = now;
|
|
2383
|
+
entry.updatedAt = entry.updatedAt || now;
|
|
2384
|
+
}
|
|
2385
|
+
await saveV2Semantic(this.v2Paths, all);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
private async reconsolidateV2AfterRecall(
|
|
2389
|
+
episodes: EpisodeMemory[],
|
|
2390
|
+
facets: EpisodeFacet[],
|
|
2391
|
+
procedural: ProceduralMemory[],
|
|
2392
|
+
recalledEpisodes: EpisodeMemory[],
|
|
2393
|
+
recalledFacets: EpisodeFacet[],
|
|
2394
|
+
recalledProcedural: ProceduralMemory[],
|
|
2395
|
+
): Promise<void> {
|
|
2396
|
+
const reconsolidated = reconsolidateV2Memories(
|
|
2397
|
+
episodes,
|
|
2398
|
+
facets,
|
|
2399
|
+
procedural,
|
|
2400
|
+
recalledEpisodes.map((entry) => entry.id),
|
|
2401
|
+
recalledFacets.map((entry) => entry.id),
|
|
2402
|
+
recalledProcedural.map((entry) => entry.id),
|
|
2403
|
+
);
|
|
2404
|
+
await Promise.all([
|
|
2405
|
+
saveV2Episodes(this.v2Paths, reconsolidated.episodes),
|
|
2406
|
+
saveV2Facets(this.v2Paths, reconsolidated.facets),
|
|
2407
|
+
saveV2Procedural(this.v2Paths, reconsolidated.procedural),
|
|
2408
|
+
saveV2Meta(this.v2Paths, {
|
|
2409
|
+
...(await loadV2Meta(this.v2Paths)),
|
|
2410
|
+
lastReconsolidationAt: new Date().toISOString(),
|
|
2411
|
+
}),
|
|
2412
|
+
]);
|
|
2413
|
+
await this.rebuildV2Links();
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
private detectAlignmentConflicts = detectAlignmentConflictsFn;
|
|
2417
|
+
|
|
2418
|
+
private suggestConflictAction = suggestConflictActionFn;
|
|
2419
|
+
|
|
2420
|
+
private explainConflictAction(
|
|
2421
|
+
a: MemoryEntry,
|
|
2422
|
+
b: MemoryEntry,
|
|
2423
|
+
action: "merge" | "demote" | "forget" | "mark-situational",
|
|
2424
|
+
): string {
|
|
2425
|
+
switch (action) {
|
|
2426
|
+
case "mark-situational":
|
|
2427
|
+
return "One side looks more temporary than the other, so this conflict likely comes from short-term context rather than true identity drift.";
|
|
2428
|
+
case "forget":
|
|
2429
|
+
return "One side has weak signal compared with the conflict risk, so forgetting the weaker memory is safer than keeping both.";
|
|
2430
|
+
case "demote":
|
|
2431
|
+
return "At least one side is recent enough that it may be an overfit; demoting it reduces the chance of personality drift.";
|
|
2432
|
+
default:
|
|
2433
|
+
return "Both memories may describe the same preference from different angles, so a merged memory is likely safer than keeping them separate.";
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
private async reinforceWork(recalled: WorkEntry[], all: WorkEntry[]): Promise<void> {
|
|
2438
|
+
const ids = new Set(recalled.map((w) => w.id));
|
|
2439
|
+
const now = new Date().toISOString();
|
|
2440
|
+
for (const w of all) {
|
|
2441
|
+
if (ids.has(w.id)) {
|
|
2442
|
+
w.accessCount = (w.accessCount ?? 0) + 1;
|
|
2443
|
+
w.lastAccessed = now;
|
|
2444
|
+
w.strength = (w.strength || 45) * this.cfg.strengthGrowthFactor;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
await saveWork(this.workPath, all, this.cfg.maxEntries.work, (w) =>
|
|
2448
|
+
utilityWork(w, this.cfg.halfLife, this.cfg.evictionWeights),
|
|
2449
|
+
);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
private async reconsolidateIfNeeded(
|
|
2453
|
+
recalled: MemoryEntry[],
|
|
2454
|
+
contextTags: string[],
|
|
2455
|
+
_allEntries: MemoryEntry[],
|
|
2456
|
+
): Promise<void> {
|
|
2457
|
+
if (!this.llmFn) return;
|
|
2458
|
+
const p = PROMPTS[this.cfg.locale];
|
|
2459
|
+
for (const entry of recalled) {
|
|
2460
|
+
const overlap = tagOverlap(entry.tags, contextTags);
|
|
2461
|
+
if (overlap >= 0.3) continue;
|
|
2462
|
+
try {
|
|
2463
|
+
const memText = entry.detail || entry.summary || entry.content || "";
|
|
2464
|
+
const updated = await this.llmFn(
|
|
2465
|
+
p.reconsolidationSystem,
|
|
2466
|
+
`Original memory: ${memText}\n\nCurrent context tags: ${contextTags.join(", ")}`,
|
|
2467
|
+
);
|
|
2468
|
+
if (updated && updated.length > 10) {
|
|
2469
|
+
const trimmed = updated.trim();
|
|
2470
|
+
entry.detail = trimmed;
|
|
2471
|
+
entry.content = trimmed;
|
|
2472
|
+
entry.summary = trimmed.length <= 150 ? trimmed : `${trimmed.slice(0, 147)}...`;
|
|
2473
|
+
entry.tags = extractTags(trimmed);
|
|
2474
|
+
}
|
|
2475
|
+
} catch {
|
|
2476
|
+
/* graceful degradation */
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
private buildProgressiveInjectionText = buildProgressiveInjectionText;
|
|
2482
|
+
|
|
2483
|
+
private isConversationPreference = isConversationPreferenceFn;
|
|
2484
|
+
|
|
2485
|
+
private selectConversationPreferences = selectConversationPreferencesFn;
|
|
2486
|
+
|
|
2487
|
+
private rankConversationPreference = rankConversationPreferenceFn;
|
|
2488
|
+
|
|
2489
|
+
private mergeUniqueEntries(preferred: MemoryEntry[], fallback: MemoryEntry[]): MemoryEntry[] {
|
|
2490
|
+
const merged: MemoryEntry[] = [];
|
|
2491
|
+
const seen = new Set<string>();
|
|
2492
|
+
for (const entry of [...preferred, ...fallback]) {
|
|
2493
|
+
if (seen.has(entry.id)) continue;
|
|
2494
|
+
seen.add(entry.id);
|
|
2495
|
+
merged.push(entry);
|
|
2496
|
+
}
|
|
2497
|
+
return merged;
|
|
2498
|
+
}
|
|
2499
|
+
}
|