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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: reconsolidateV2Memories
|
|
3
|
+
* [FROM]: Depends on ./scoring.js, ./types-v2.js
|
|
4
|
+
* [TO]: Consumed by packages/mem-core/src/index.ts
|
|
5
|
+
* [HERE]: packages/mem-core/src/reconsolidate-v2.ts - lightweight reconsolidation rules for NanoMem v2 memories
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { extractTags, tagOverlap } from "./scoring.js";
|
|
9
|
+
import type { EpisodeFacet, EpisodeMemory, ProceduralMemory } from "./types-v2.js";
|
|
10
|
+
|
|
11
|
+
function mergeUnique<T>(values: T[]): T[] {
|
|
12
|
+
return [...new Set(values)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function textForProcedure(entry: ProceduralMemory): string {
|
|
16
|
+
return [entry.name, entry.searchText, entry.summary, entry.contextText ?? "", entry.boundaries ?? ""]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(" ")
|
|
19
|
+
.toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function shouldMergeProcedures(a: ProceduralMemory, b: ProceduralMemory): boolean {
|
|
23
|
+
if (a.id === b.id) return false;
|
|
24
|
+
const tagScore = tagOverlap(a.tags, b.tags);
|
|
25
|
+
if (tagScore >= 0.7) return true;
|
|
26
|
+
const aText = textForProcedure(a);
|
|
27
|
+
const bText = textForProcedure(b);
|
|
28
|
+
return aText.includes(b.searchText.toLowerCase()) || bText.includes(a.searchText.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function reconsolidateV2Memories(
|
|
32
|
+
episodes: EpisodeMemory[],
|
|
33
|
+
facets: EpisodeFacet[],
|
|
34
|
+
procedural: ProceduralMemory[],
|
|
35
|
+
recalledEpisodeIds: string[],
|
|
36
|
+
recalledFacetIds: string[],
|
|
37
|
+
recalledProcedureIds: string[],
|
|
38
|
+
now = new Date().toISOString(),
|
|
39
|
+
): {
|
|
40
|
+
episodes: EpisodeMemory[];
|
|
41
|
+
facets: EpisodeFacet[];
|
|
42
|
+
procedural: ProceduralMemory[];
|
|
43
|
+
changes: { promotedProcedures: number; mergedProcedures: number; stabilizedEpisodes: number; stabilizedFacets: number };
|
|
44
|
+
} {
|
|
45
|
+
let promotedProcedures = 0;
|
|
46
|
+
let mergedProcedures = 0;
|
|
47
|
+
let stabilizedEpisodes = 0;
|
|
48
|
+
let stabilizedFacets = 0;
|
|
49
|
+
|
|
50
|
+
const recalledEpisodeSet = new Set(recalledEpisodeIds);
|
|
51
|
+
const recalledFacetSet = new Set(recalledFacetIds);
|
|
52
|
+
const recalledProcedureSet = new Set(recalledProcedureIds);
|
|
53
|
+
|
|
54
|
+
for (const episode of episodes) {
|
|
55
|
+
if (!recalledEpisodeSet.has(episode.id)) continue;
|
|
56
|
+
episode.reconsolidatedAt = now;
|
|
57
|
+
if (episode.accessCount >= 3 && episode.retention !== "key-event") {
|
|
58
|
+
episode.retention = "key-event";
|
|
59
|
+
stabilizedEpisodes++;
|
|
60
|
+
}
|
|
61
|
+
if (episode.accessCount >= 4 && episode.stability !== "stable") {
|
|
62
|
+
episode.stability = "stable";
|
|
63
|
+
stabilizedEpisodes++;
|
|
64
|
+
}
|
|
65
|
+
episode.tags = mergeUnique(extractTags(`${episode.title ?? ""} ${episode.summary} ${episode.userGoal ?? ""} ${episode.outcome ?? ""}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const facet of facets) {
|
|
69
|
+
if (!recalledFacetSet.has(facet.id)) continue;
|
|
70
|
+
if (facet.accessCount >= 3 && facet.retention !== "key-event") {
|
|
71
|
+
facet.retention = "key-event";
|
|
72
|
+
stabilizedFacets++;
|
|
73
|
+
}
|
|
74
|
+
if (facet.accessCount >= 4 && facet.stability !== "stable") {
|
|
75
|
+
facet.stability = "stable";
|
|
76
|
+
stabilizedFacets++;
|
|
77
|
+
}
|
|
78
|
+
facet.tags = mergeUnique(extractTags(`${facet.searchText} ${facet.summary ?? ""} ${facet.anchorText ?? ""}`));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sortedProcedural = [...procedural].sort((a, b) => (b.accessCount ?? 0) - (a.accessCount ?? 0));
|
|
82
|
+
const keptProcedural: ProceduralMemory[] = [];
|
|
83
|
+
const supersededProcedural: ProceduralMemory[] = [];
|
|
84
|
+
|
|
85
|
+
for (const procedure of sortedProcedural) {
|
|
86
|
+
if (recalledProcedureSet.has(procedure.id) && procedure.status === "draft" && procedure.accessCount >= 2) {
|
|
87
|
+
procedure.status = "active";
|
|
88
|
+
procedure.version = Math.max(1, procedure.version);
|
|
89
|
+
procedure.confidence = Math.max(procedure.confidence, 0.82);
|
|
90
|
+
promotedProcedures++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
procedure.tags = mergeUnique(
|
|
94
|
+
extractTags(
|
|
95
|
+
`${procedure.name} ${procedure.searchText} ${procedure.summary} ${procedure.contextText ?? ""} ${procedure.steps.map((step) => step.text).join(" ")}`,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const keeper = keptProcedural.find((candidate) => shouldMergeProcedures(candidate, procedure));
|
|
100
|
+
if (!keeper) {
|
|
101
|
+
keptProcedural.push(procedure);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
keeper.steps = mergeUnique([...keeper.steps.map((step) => step.text), ...procedure.steps.map((step) => step.text)]).map(
|
|
106
|
+
(text, index) => ({
|
|
107
|
+
id: `${keeper.id}:step:${index + 1}`,
|
|
108
|
+
text,
|
|
109
|
+
kind: keeper.steps[index]?.kind ?? procedure.steps[index]?.kind ?? "step",
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
keeper.tags = mergeUnique([...(keeper.tags ?? []), ...(procedure.tags ?? [])]);
|
|
113
|
+
keeper.sourceEpisodeIds = mergeUnique([...(keeper.sourceEpisodeIds ?? []), ...(procedure.sourceEpisodeIds ?? [])]);
|
|
114
|
+
keeper.sourceFacetIds = mergeUnique([...(keeper.sourceFacetIds ?? []), ...(procedure.sourceFacetIds ?? [])]);
|
|
115
|
+
keeper.supersedesIds = mergeUnique([...(keeper.supersedesIds ?? []), procedure.id]);
|
|
116
|
+
keeper.version = Math.max(keeper.version, procedure.version) + 1;
|
|
117
|
+
keeper.updatedAt = now;
|
|
118
|
+
keeper.confidence = Math.max(keeper.confidence, procedure.confidence);
|
|
119
|
+
keeper.salience = Math.max(keeper.salience, procedure.salience);
|
|
120
|
+
keeper.importance = Math.max(keeper.importance, procedure.importance);
|
|
121
|
+
if (keeper.status === "draft" && procedure.status === "active") {
|
|
122
|
+
keeper.status = "active";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
supersededProcedural.push({
|
|
126
|
+
...procedure,
|
|
127
|
+
status: "superseded",
|
|
128
|
+
supersededById: keeper.id,
|
|
129
|
+
updatedAt: now,
|
|
130
|
+
});
|
|
131
|
+
mergedProcedures++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
episodes,
|
|
136
|
+
facets,
|
|
137
|
+
procedural: [...keptProcedural, ...supersededProcedural],
|
|
138
|
+
changes: {
|
|
139
|
+
promotedProcedures,
|
|
140
|
+
mergedProcedures,
|
|
141
|
+
stabilizedEpisodes,
|
|
142
|
+
stabilizedFacets,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/scoring.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: daysSince, decay, extractTags, tagOverlap, scoreEntry, scoreWork, ScoreWeights
|
|
3
|
+
* [FROM]: Depends on ./config.js, ./types.js
|
|
4
|
+
* [TO]: Consumed by packages/mem-core/src/index.ts
|
|
5
|
+
* [HERE]: packages/mem-core/src/scoring.ts - core ranking algorithm (Recency + Importance + Relevance), Ebbinghaus spaced repetition
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import type { Episode, InjectionLevel, MemoryEntry, WorkEntry } from "./types.js";
|
|
10
|
+
import type { ProgressiveRecallConfig } from "./config.js";
|
|
11
|
+
|
|
12
|
+
export function daysSince(iso: string): number {
|
|
13
|
+
return Math.max(0, (Date.now() - new Date(iso).getTime()) / 86_400_000);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Ebbinghaus decay: R = e^(-t * ln2 / S) where S = per-entry strength */
|
|
17
|
+
export function decay(ageDays: number, strength: number): number {
|
|
18
|
+
return Math.exp((-ageDays * Math.LN2) / Math.max(1, strength));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function extractTags(text: string): string[] {
|
|
22
|
+
return [...new Set(text.toLowerCase().match(/[a-z0-9\u4e00-\u9fff_.-]{2,}/g) || [])].slice(0, 30);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function tagOverlap(a: string[], b: string[]): number {
|
|
26
|
+
if (!a.length || !b.length) return 0;
|
|
27
|
+
const setB = new Set(b);
|
|
28
|
+
return a.filter((t) => setB.has(t)).length / Math.max(a.length, b.length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ScoreWeights {
|
|
32
|
+
recency: number;
|
|
33
|
+
importance: number;
|
|
34
|
+
relevance: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function scoreEntry(
|
|
38
|
+
e: MemoryEntry,
|
|
39
|
+
project: string,
|
|
40
|
+
ctx: string[],
|
|
41
|
+
defaultHalfLife: Record<string, number>,
|
|
42
|
+
weights: ScoreWeights,
|
|
43
|
+
): number {
|
|
44
|
+
const strength = e.strength || defaultHalfLife[e.type] || 30;
|
|
45
|
+
const recency = decay(daysSince(e.created), strength);
|
|
46
|
+
const importanceNorm = Math.min(1, e.importance / 10);
|
|
47
|
+
const salienceNorm = Math.min(1, (e.salience ?? e.importance) / 10);
|
|
48
|
+
const projectMatch = e.project === project ? 1 : 0.5;
|
|
49
|
+
const relevance = projectMatch * (0.3 + 0.7 * tagOverlap(e.tags, ctx));
|
|
50
|
+
const retentionBoost =
|
|
51
|
+
e.retention === "core" ? 0.2 : e.retention === "key-event" ? 0.25 : 0;
|
|
52
|
+
const stabilityBoost = e.stability === "stable" ? 0.08 : -0.12;
|
|
53
|
+
const situationalPenalty =
|
|
54
|
+
e.stability === "situational" && daysSince(e.created) > 10 ? 0.18 : 0;
|
|
55
|
+
return (
|
|
56
|
+
weights.recency * recency +
|
|
57
|
+
weights.importance * importanceNorm +
|
|
58
|
+
weights.relevance * relevance +
|
|
59
|
+
salienceNorm * 0.25 +
|
|
60
|
+
retentionBoost +
|
|
61
|
+
stabilityBoost -
|
|
62
|
+
situationalPenalty
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function scoreEpisode(
|
|
67
|
+
ep: Episode,
|
|
68
|
+
project: string,
|
|
69
|
+
ctx: string[],
|
|
70
|
+
defaultHalfLife: Record<string, number>,
|
|
71
|
+
weights: ScoreWeights,
|
|
72
|
+
): number {
|
|
73
|
+
const strength = defaultHalfLife.episode || 14;
|
|
74
|
+
const recency = decay(daysSince(ep.date), strength);
|
|
75
|
+
const importanceNorm = Math.min(1, ep.importance / 10);
|
|
76
|
+
const projectMatch = ep.project === project ? 1 : 0.5;
|
|
77
|
+
const relevance = projectMatch * (0.3 + 0.7 * tagOverlap(ep.tags, ctx));
|
|
78
|
+
return weights.recency * recency + weights.importance * importanceNorm + weights.relevance * relevance;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function scoreWorkEntry(
|
|
82
|
+
w: WorkEntry,
|
|
83
|
+
project: string,
|
|
84
|
+
ctx: string[],
|
|
85
|
+
defaultHalfLife: Record<string, number>,
|
|
86
|
+
weights: ScoreWeights,
|
|
87
|
+
): number {
|
|
88
|
+
const strength = w.strength || defaultHalfLife.work || 45;
|
|
89
|
+
const recency = decay(daysSince(w.created), strength);
|
|
90
|
+
const importanceNorm = Math.min(1, w.importance / 10);
|
|
91
|
+
const projectMatch = w.project === project ? 1 : 0.5;
|
|
92
|
+
const relevance = projectMatch * (0.3 + 0.7 * tagOverlap(w.tags, ctx));
|
|
93
|
+
return weights.recency * recency + weights.importance * importanceNorm + weights.relevance * relevance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Budget-constrained top-k selection by score */
|
|
97
|
+
export function pickTop<T>(items: T[], scoreFn: (t: T) => number, lenFn: (t: T) => number, budget: number): T[] {
|
|
98
|
+
const scored = items.map((t) => ({ item: t, score: scoreFn(t) })).sort((a, b) => b.score - a.score);
|
|
99
|
+
const result: T[] = [];
|
|
100
|
+
let used = 0;
|
|
101
|
+
for (const { item } of scored) {
|
|
102
|
+
const len = lenFn(item);
|
|
103
|
+
if (used + len > budget) continue;
|
|
104
|
+
used += len;
|
|
105
|
+
result.push(item);
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Progressive Recall ─────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** Determine the injection tier for a single entry based on its score and config */
|
|
113
|
+
export function getInjectionLevel(
|
|
114
|
+
entry: MemoryEntry,
|
|
115
|
+
score: number,
|
|
116
|
+
cfg: ProgressiveRecallConfig,
|
|
117
|
+
): InjectionLevel {
|
|
118
|
+
const hoursSinceCreation = (Date.now() - new Date(entry.created).getTime()) / 3_600_000;
|
|
119
|
+
|
|
120
|
+
// Force Active for very recent or critical entries
|
|
121
|
+
if (hoursSinceCreation <= cfg.forceRecentHours) return "active";
|
|
122
|
+
if (entry.importance >= cfg.forceImportanceMin) return "active";
|
|
123
|
+
if (entry.retention === "key-event" && (entry.salience ?? entry.importance) >= 7) return "active";
|
|
124
|
+
if (entry.retention === "core" && score >= cfg.thresholdCue) return "active";
|
|
125
|
+
if (score >= cfg.thresholdActive) return "active";
|
|
126
|
+
if (score >= cfg.thresholdCue) return "cue";
|
|
127
|
+
return "dormant";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Classify entries into Active / Cue / Dormant tiers, each sorted by score descending */
|
|
131
|
+
export function tierEntries(
|
|
132
|
+
entries: MemoryEntry[],
|
|
133
|
+
project: string,
|
|
134
|
+
contextTags: string[],
|
|
135
|
+
halfLife: Record<string, number>,
|
|
136
|
+
weights: ScoreWeights,
|
|
137
|
+
prCfg: ProgressiveRecallConfig,
|
|
138
|
+
): { active: MemoryEntry[]; cue: MemoryEntry[]; dormant: MemoryEntry[] } {
|
|
139
|
+
const active: Array<{ entry: MemoryEntry; score: number }> = [];
|
|
140
|
+
const cue: Array<{ entry: MemoryEntry; score: number }> = [];
|
|
141
|
+
const dormant: Array<{ entry: MemoryEntry; score: number }> = [];
|
|
142
|
+
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const score = scoreEntry(entry, project, contextTags, halfLife, weights);
|
|
145
|
+
const level = getInjectionLevel(entry, score, prCfg);
|
|
146
|
+
|
|
147
|
+
if (level === "active") active.push({ entry, score });
|
|
148
|
+
else if (level === "cue") cue.push({ entry, score });
|
|
149
|
+
else dormant.push({ entry, score });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sortDesc = (arr: Array<{ entry: MemoryEntry; score: number }>) =>
|
|
153
|
+
arr.sort((a, b) => b.score - a.score).map((x) => x.entry);
|
|
154
|
+
|
|
155
|
+
return { active: sortDesc(active), cue: sortDesc(cue), dormant: sortDesc(dormant) };
|
|
156
|
+
}
|
package/src/store-v2.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: V2_PATHS, loadV2*, saveV2*, appendV2Episode, appendV2Facet
|
|
3
|
+
* [FROM]: Depends on browser-fs.ts, ./store.js, ./types.js, ./types-v2.js
|
|
4
|
+
* [TO]: Consumed by packages/mem-core/src/index.ts
|
|
5
|
+
* [HERE]: packages/mem-core/src/store-v2.ts - persistence layer for NanoMem v2 layered memory (episode/facet/semantic/procedural/state/link stores)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "./browser-fs.js";
|
|
9
|
+
import { readJson, writeJson } from "./store.js";
|
|
10
|
+
import type {
|
|
11
|
+
EpisodeFacet,
|
|
12
|
+
EpisodeMemory,
|
|
13
|
+
MemoryLink,
|
|
14
|
+
NanoMemV2Snapshot,
|
|
15
|
+
ProceduralMemory,
|
|
16
|
+
SemanticMemory,
|
|
17
|
+
StateMemory,
|
|
18
|
+
V2Meta,
|
|
19
|
+
} from "./types-v2.js";
|
|
20
|
+
|
|
21
|
+
export interface NanoMemV2Paths {
|
|
22
|
+
rootDir: string;
|
|
23
|
+
episodesPath: string;
|
|
24
|
+
facetsPath: string;
|
|
25
|
+
semanticPath: string;
|
|
26
|
+
proceduralPath: string;
|
|
27
|
+
statePath: string;
|
|
28
|
+
linksPath: string;
|
|
29
|
+
metaPath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_V2_META: V2Meta = {
|
|
33
|
+
version: 2,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getV2Paths(memoryDir: string): NanoMemV2Paths {
|
|
37
|
+
const rootDir = join(memoryDir, "v2");
|
|
38
|
+
return {
|
|
39
|
+
rootDir,
|
|
40
|
+
episodesPath: join(rootDir, "episodes.json"),
|
|
41
|
+
facetsPath: join(rootDir, "facets.json"),
|
|
42
|
+
semanticPath: join(rootDir, "semantic.json"),
|
|
43
|
+
proceduralPath: join(rootDir, "procedural.json"),
|
|
44
|
+
statePath: join(rootDir, "state.json"),
|
|
45
|
+
linksPath: join(rootDir, "links.json"),
|
|
46
|
+
metaPath: join(rootDir, "meta.json"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function loadV2Episodes(paths: NanoMemV2Paths): Promise<EpisodeMemory[]> {
|
|
51
|
+
return readJson<EpisodeMemory[]>(paths.episodesPath, []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function saveV2Episodes(paths: NanoMemV2Paths, episodes: EpisodeMemory[]): Promise<void> {
|
|
55
|
+
await writeJson(paths.episodesPath, episodes);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function loadV2Facets(paths: NanoMemV2Paths): Promise<EpisodeFacet[]> {
|
|
59
|
+
return readJson<EpisodeFacet[]>(paths.facetsPath, []);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function saveV2Facets(paths: NanoMemV2Paths, facets: EpisodeFacet[]): Promise<void> {
|
|
63
|
+
await writeJson(paths.facetsPath, facets);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function loadV2Semantic(paths: NanoMemV2Paths): Promise<SemanticMemory[]> {
|
|
67
|
+
return readJson<SemanticMemory[]>(paths.semanticPath, []);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function saveV2Semantic(paths: NanoMemV2Paths, semantic: SemanticMemory[]): Promise<void> {
|
|
71
|
+
await writeJson(paths.semanticPath, semantic);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function loadV2Procedural(paths: NanoMemV2Paths): Promise<ProceduralMemory[]> {
|
|
75
|
+
return readJson<ProceduralMemory[]>(paths.proceduralPath, []);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function saveV2Procedural(paths: NanoMemV2Paths, procedural: ProceduralMemory[]): Promise<void> {
|
|
79
|
+
await writeJson(paths.proceduralPath, procedural);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function loadV2State(paths: NanoMemV2Paths): Promise<StateMemory[]> {
|
|
83
|
+
return readJson<StateMemory[]>(paths.statePath, []);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function saveV2State(paths: NanoMemV2Paths, state: StateMemory[]): Promise<void> {
|
|
87
|
+
await writeJson(paths.statePath, state);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function loadV2Links(paths: NanoMemV2Paths): Promise<MemoryLink[]> {
|
|
91
|
+
return readJson<MemoryLink[]>(paths.linksPath, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function saveV2Links(paths: NanoMemV2Paths, links: MemoryLink[]): Promise<void> {
|
|
95
|
+
await writeJson(paths.linksPath, links);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function loadV2Meta(paths: NanoMemV2Paths): Promise<V2Meta> {
|
|
99
|
+
return readJson<V2Meta>(paths.metaPath, DEFAULT_V2_META);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function saveV2Meta(paths: NanoMemV2Paths, meta: V2Meta): Promise<void> {
|
|
103
|
+
await writeJson(paths.metaPath, meta);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function loadV2Snapshot(paths: NanoMemV2Paths): Promise<NanoMemV2Snapshot> {
|
|
107
|
+
const [episodes, facets, semantic, procedural, state, links, meta] = await Promise.all([
|
|
108
|
+
loadV2Episodes(paths),
|
|
109
|
+
loadV2Facets(paths),
|
|
110
|
+
loadV2Semantic(paths),
|
|
111
|
+
loadV2Procedural(paths),
|
|
112
|
+
loadV2State(paths),
|
|
113
|
+
loadV2Links(paths),
|
|
114
|
+
loadV2Meta(paths),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
episodes,
|
|
119
|
+
facets,
|
|
120
|
+
semantic,
|
|
121
|
+
procedural,
|
|
122
|
+
state,
|
|
123
|
+
links,
|
|
124
|
+
meta,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function saveV2Snapshot(paths: NanoMemV2Paths, snapshot: NanoMemV2Snapshot): Promise<void> {
|
|
129
|
+
await Promise.all([
|
|
130
|
+
saveV2Episodes(paths, snapshot.episodes),
|
|
131
|
+
saveV2Facets(paths, snapshot.facets),
|
|
132
|
+
saveV2Semantic(paths, snapshot.semantic),
|
|
133
|
+
saveV2Procedural(paths, snapshot.procedural),
|
|
134
|
+
saveV2State(paths, snapshot.state),
|
|
135
|
+
saveV2Links(paths, snapshot.links),
|
|
136
|
+
saveV2Meta(paths, snapshot.meta),
|
|
137
|
+
]);
|
|
138
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: readJson, writeJson, deriveNameFromContent, deriveSummaryFromContent, ensureDir
|
|
3
|
+
* [FROM]: Depends on browser-fs.ts and ./types.js
|
|
4
|
+
* [TO]: Consumed by packages/mem-core/src/index.ts
|
|
5
|
+
* [HERE]: packages/mem-core/src/store.ts - persistence layer for JSON storage, all other modules go through here
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import { mkdir, readdir, readFile, writeFile, dirname, join } from "./browser-fs.js";
|
|
10
|
+
import type { Episode, MemoryEntry, Meta, WorkEntry } from "./types.js";
|
|
11
|
+
|
|
12
|
+
async function ensureDir(dir: string): Promise<void> {
|
|
13
|
+
await mkdir(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readJson<T>(path: string, fallback: T): Promise<T> {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(path);
|
|
19
|
+
return JSON.parse(raw) as T;
|
|
20
|
+
} catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function writeJson(path: string, data: unknown): Promise<void> {
|
|
26
|
+
await ensureDir(dirname(path));
|
|
27
|
+
await writeFile(path, JSON.stringify(data, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Migration helpers ──────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/** Derive a short name (≤30 chars) from content text */
|
|
33
|
+
function deriveNameFromContent(content: string): string {
|
|
34
|
+
if (!content) return "untitled";
|
|
35
|
+
const words = content.split(/\s+/).filter((w) => w.length > 0);
|
|
36
|
+
const name = words.slice(0, 5).join(" ");
|
|
37
|
+
return name.length > 30 ? `${name.slice(0, 27)}...` : name || "untitled";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Derive a summary (≤150 chars) from content text, breaking at sentence boundary */
|
|
41
|
+
function deriveSummaryFromContent(content: string): string {
|
|
42
|
+
if (!content) return "";
|
|
43
|
+
if (content.length <= 150) return content;
|
|
44
|
+
// Try to break at a sentence boundary
|
|
45
|
+
const cut = content.slice(0, 160).search(/[。.!!??]\s*/);
|
|
46
|
+
if (cut > 50) return content.slice(0, cut + 1);
|
|
47
|
+
return `${content.slice(0, 147)}...`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Auto-migrate old-format entry (content only) to name/summary/detail */
|
|
51
|
+
function migrateEntry(entry: MemoryEntry): MemoryEntry {
|
|
52
|
+
if (entry.name && entry.summary) return entry;
|
|
53
|
+
const content = entry.content || entry.detail || "";
|
|
54
|
+
return {
|
|
55
|
+
...entry,
|
|
56
|
+
name: entry.name || deriveNameFromContent(content),
|
|
57
|
+
summary: entry.summary || deriveSummaryFromContent(content),
|
|
58
|
+
detail: entry.detail || content,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── MemoryEntry CRUD ───────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export async function loadEntries(path: string): Promise<MemoryEntry[]> {
|
|
65
|
+
const entries = await readJson<unknown>(path, []);
|
|
66
|
+
if (!Array.isArray(entries)) return [];
|
|
67
|
+
return entries.filter(isMemoryEntryLike).map(migrateEntry);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isMemoryEntryLike(value: unknown): value is MemoryEntry {
|
|
71
|
+
if (typeof value !== "object" || value === null) return false;
|
|
72
|
+
const entry = value as Partial<MemoryEntry>;
|
|
73
|
+
return (
|
|
74
|
+
typeof entry.id === "string" &&
|
|
75
|
+
typeof entry.type === "string" &&
|
|
76
|
+
Array.isArray(entry.tags) &&
|
|
77
|
+
typeof entry.project === "string" &&
|
|
78
|
+
typeof entry.importance === "number" &&
|
|
79
|
+
typeof entry.created === "string" &&
|
|
80
|
+
typeof entry.accessCount === "number"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function saveEntries(
|
|
85
|
+
path: string,
|
|
86
|
+
entries: MemoryEntry[],
|
|
87
|
+
max: number,
|
|
88
|
+
utilityFn: (e: MemoryEntry) => number,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
if (entries.length > max) {
|
|
91
|
+
entries.sort((a, b) => utilityFn(b) - utilityFn(a));
|
|
92
|
+
entries.length = max;
|
|
93
|
+
}
|
|
94
|
+
// Keep content as alias of detail for backward compat with older NanoMem versions
|
|
95
|
+
const persisted = entries.map((e) => ({ ...e, content: e.detail || e.content || e.summary || "" }));
|
|
96
|
+
await writeJson(path, persisted);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function loadEpisodes(episodesDir: string): Promise<Episode[]> {
|
|
100
|
+
await ensureDir(episodesDir);
|
|
101
|
+
const files = await readdir(episodesDir);
|
|
102
|
+
const results: Episode[] = [];
|
|
103
|
+
for (const f of files) {
|
|
104
|
+
if (!f.endsWith(".json")) continue;
|
|
105
|
+
const ep = await readJson<Episode | null>(join(episodesDir, f), null);
|
|
106
|
+
if (ep) results.push(ep);
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function saveEpisode(episodesDir: string, ep: Episode): Promise<void> {
|
|
112
|
+
await ensureDir(episodesDir);
|
|
113
|
+
await writeJson(join(episodesDir, `${ep.date}-${ep.sessionId.slice(0, 8)}.json`), ep);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function loadWork(path: string): Promise<WorkEntry[]> {
|
|
117
|
+
return readJson<WorkEntry[]>(path, []);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function saveWork(
|
|
121
|
+
path: string,
|
|
122
|
+
entries: WorkEntry[],
|
|
123
|
+
max: number,
|
|
124
|
+
utilityFn: (w: WorkEntry) => number,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
if (entries.length > max) {
|
|
127
|
+
entries.sort((a, b) => utilityFn(b) - utilityFn(a));
|
|
128
|
+
entries.length = max;
|
|
129
|
+
}
|
|
130
|
+
await writeJson(path, entries);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function loadMeta(path: string): Promise<Meta> {
|
|
134
|
+
return readJson<Meta>(path, { totalSessions: 0, version: 1 });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { deriveNameFromContent, deriveSummaryFromContent };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [WHO]: Provides TurnContext, MemoryRecallRecord, getTurnContext(), setTurnContext(), TURN_CONTEXT_GLOBAL_KEY
|
|
3
|
+
* [FROM]: Depends on nothing; reads/writes globalThis using the documented key
|
|
4
|
+
* [TO]: Consumed by engine-scoring-v2.ts (reads structuralAnchor), engine.ts (writes memoryRecallSnapshot)
|
|
5
|
+
* [HERE]: packages/mem-core/src/turn-context.ts - structural mirror of core/runtime/turn-context; mem-core cannot reverse-import the main app, so the contract is shared via the globalThis key string and schema, not the source file
|
|
6
|
+
*
|
|
7
|
+
* Keep this file structurally aligned with core/runtime/turn-context.ts.
|
|
8
|
+
* If the schema changes there, mirror the change here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface MemoryRecallRecord {
|
|
12
|
+
memoryId: string;
|
|
13
|
+
memoryKind: string;
|
|
14
|
+
anchorModule?: string;
|
|
15
|
+
anchorFile?: string;
|
|
16
|
+
scoreBreakdownStatus: "available" | "unavailable";
|
|
17
|
+
scoreRecency?: number;
|
|
18
|
+
scoreImportance?: number;
|
|
19
|
+
scoreRelevance?: number;
|
|
20
|
+
scoreStructural?: number;
|
|
21
|
+
scoreFinal: number;
|
|
22
|
+
wasInjected: boolean;
|
|
23
|
+
injectRank?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TurnContext {
|
|
27
|
+
structuralAnchor?: {
|
|
28
|
+
modulePath?: string;
|
|
29
|
+
filePath?: string;
|
|
30
|
+
candidatePaths?: string[];
|
|
31
|
+
};
|
|
32
|
+
memoryRecallSnapshot?: MemoryRecallRecord[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TURN_CONTEXT_GLOBAL_KEY = "__catuiTurnContext";
|
|
36
|
+
|
|
37
|
+
function store(): TurnContext {
|
|
38
|
+
const g = globalThis as unknown as Record<string, TurnContext | undefined>;
|
|
39
|
+
if (!g[TURN_CONTEXT_GLOBAL_KEY]) g[TURN_CONTEXT_GLOBAL_KEY] = {};
|
|
40
|
+
return g[TURN_CONTEXT_GLOBAL_KEY] as TurnContext;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read the current value of a turn-context channel. Returns undefined when no producer has published. */
|
|
44
|
+
export function getTurnContext<K extends keyof TurnContext>(key: K): TurnContext[K] {
|
|
45
|
+
return store()[key];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Publish (or clear) a turn-context channel. mem-core uses this to publish memoryRecallSnapshot. */
|
|
49
|
+
export function setTurnContext<K extends keyof TurnContext>(key: K, value: TurnContext[K]): void {
|
|
50
|
+
const s = store();
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
delete s[key];
|
|
53
|
+
} else {
|
|
54
|
+
s[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|