@cogmem/engram 0.0.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/README.md +236 -0
- package/drizzle/0000_jittery_ender_wiggin.sql +59 -0
- package/drizzle/meta/0000_snapshot.json +417 -0
- package/drizzle/meta/_journal.json +13 -0
- package/package.json +72 -0
- package/src/cli/commands/encode.ts +71 -0
- package/src/cli/commands/focus.ts +106 -0
- package/src/cli/commands/health.ts +82 -0
- package/src/cli/commands/inspect.ts +36 -0
- package/src/cli/commands/list.ts +58 -0
- package/src/cli/commands/recall.ts +55 -0
- package/src/cli/commands/sleep.ts +102 -0
- package/src/cli/commands/stats.ts +95 -0
- package/src/cli/format.ts +231 -0
- package/src/cli/index.ts +31 -0
- package/src/config/defaults.ts +58 -0
- package/src/core/activation.ts +75 -0
- package/src/core/associations.ts +186 -0
- package/src/core/chunking.ts +108 -0
- package/src/core/consolidation.ts +150 -0
- package/src/core/emotional-tag.ts +19 -0
- package/src/core/encoder.ts +47 -0
- package/src/core/engine.ts +50 -0
- package/src/core/forgetting.ts +58 -0
- package/src/core/memory.ts +94 -0
- package/src/core/procedural-store.ts +36 -0
- package/src/core/recall.ts +102 -0
- package/src/core/reconsolidation.ts +42 -0
- package/src/core/search.ts +24 -0
- package/src/core/working-memory.ts +67 -0
- package/src/index.ts +57 -0
- package/src/mcp/server.ts +122 -0
- package/src/mcp/tools.ts +334 -0
- package/src/storage/schema.ts +97 -0
- package/src/storage/sqlite.ts +402 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface CognitiveConfig {
|
|
2
|
+
decayRate: number;
|
|
3
|
+
retrievalThreshold: number;
|
|
4
|
+
latencyFactor: number;
|
|
5
|
+
latencyExponent: number;
|
|
6
|
+
activationNoise: number;
|
|
7
|
+
workingMemoryCapacity: number;
|
|
8
|
+
maxSpreadingActivation: number;
|
|
9
|
+
minAssociationStrength: number;
|
|
10
|
+
emotionalBoostFactor: number;
|
|
11
|
+
pruningThreshold: number;
|
|
12
|
+
associationFormationThreshold: number;
|
|
13
|
+
retrievalStrengtheningBoost: number;
|
|
14
|
+
reconsolidationBlendRate: number;
|
|
15
|
+
chunkingSimilarityThreshold: number;
|
|
16
|
+
semanticExtractionThreshold: number;
|
|
17
|
+
dbPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_CONFIG: CognitiveConfig = {
|
|
21
|
+
decayRate: 0.5,
|
|
22
|
+
retrievalThreshold: -3.0,
|
|
23
|
+
latencyFactor: 1.0,
|
|
24
|
+
latencyExponent: 1.0,
|
|
25
|
+
activationNoise: 0.25,
|
|
26
|
+
workingMemoryCapacity: 7,
|
|
27
|
+
maxSpreadingActivation: 1.5,
|
|
28
|
+
minAssociationStrength: 0.1,
|
|
29
|
+
emotionalBoostFactor: 2.0,
|
|
30
|
+
pruningThreshold: -2.0,
|
|
31
|
+
associationFormationThreshold: 2,
|
|
32
|
+
retrievalStrengtheningBoost: 0.1,
|
|
33
|
+
reconsolidationBlendRate: 0.1,
|
|
34
|
+
chunkingSimilarityThreshold: 0.6,
|
|
35
|
+
semanticExtractionThreshold: 3,
|
|
36
|
+
dbPath: "~/.engram/memory.db",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function resolveDbPath(dbPath: string): string {
|
|
40
|
+
if (dbPath.startsWith("~")) {
|
|
41
|
+
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
|
42
|
+
return dbPath.replace("~", home);
|
|
43
|
+
}
|
|
44
|
+
return dbPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function loadConfig(overrides?: Partial<CognitiveConfig>): CognitiveConfig {
|
|
48
|
+
const config = { ...DEFAULT_CONFIG, ...overrides };
|
|
49
|
+
|
|
50
|
+
if (process.env.ENGRAM_DB_PATH) config.dbPath = process.env.ENGRAM_DB_PATH;
|
|
51
|
+
if (process.env.ENGRAM_DECAY_RATE) config.decayRate = Number(process.env.ENGRAM_DECAY_RATE);
|
|
52
|
+
if (process.env.ENGRAM_WM_CAPACITY)
|
|
53
|
+
config.workingMemoryCapacity = Number(process.env.ENGRAM_WM_CAPACITY);
|
|
54
|
+
if (process.env.ENGRAM_RETRIEVAL_THRESHOLD)
|
|
55
|
+
config.retrievalThreshold = Number(process.env.ENGRAM_RETRIEVAL_THRESHOLD);
|
|
56
|
+
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
|
+
|
|
3
|
+
// B_i = ln(Σ t_j^{-d})
|
|
4
|
+
export function baseLevelActivation(
|
|
5
|
+
accessTimestamps: number[],
|
|
6
|
+
now: number,
|
|
7
|
+
decayRate: number,
|
|
8
|
+
): number {
|
|
9
|
+
if (accessTimestamps.length === 0) return -Infinity;
|
|
10
|
+
|
|
11
|
+
let sum = 0;
|
|
12
|
+
for (const ts of accessTimestamps) {
|
|
13
|
+
const elapsedSeconds = Math.max((now - ts) / 1000, 0.001);
|
|
14
|
+
sum += Math.pow(elapsedSeconds, -decayRate);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return Math.log(sum);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// S_ji = S - ln(fan_j)
|
|
21
|
+
export function spreadingActivationStrength(maxStrength: number, fanCount: number): number {
|
|
22
|
+
if (fanCount <= 0) return 0;
|
|
23
|
+
return Math.max(0, maxStrength - Math.log(fanCount));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A_i = B_i + Σ(W_j · S_ji) + ε
|
|
27
|
+
export function totalActivation(baseLevel: number, spreadingSum: number, noise: number): number {
|
|
28
|
+
return baseLevel + spreadingSum + noise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function activationNoise(stddev: number): number {
|
|
32
|
+
if (stddev === 0) return 0;
|
|
33
|
+
const u1 = Math.random();
|
|
34
|
+
const u2 = Math.random();
|
|
35
|
+
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
36
|
+
return z * stddev;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function canRetrieve(activation: number, threshold: number): boolean {
|
|
40
|
+
return activation > threshold;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Time = F · e^{-f · A_i}
|
|
44
|
+
export function retrievalLatency(
|
|
45
|
+
activation: number,
|
|
46
|
+
latencyFactor: number,
|
|
47
|
+
latencyExponent: number,
|
|
48
|
+
): number {
|
|
49
|
+
return latencyFactor * Math.exp(-latencyExponent * activation);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function computeActivation(
|
|
53
|
+
accessTimestamps: number[],
|
|
54
|
+
now: number,
|
|
55
|
+
config: CognitiveConfig,
|
|
56
|
+
options?: {
|
|
57
|
+
spreadingSum?: number;
|
|
58
|
+
noiseOverride?: number;
|
|
59
|
+
emotionWeight?: number;
|
|
60
|
+
},
|
|
61
|
+
): { activation: number; baseLevel: number; spreading: number; noise: number; latency: number } {
|
|
62
|
+
const baseLevel = baseLevelActivation(accessTimestamps, now, config.decayRate);
|
|
63
|
+
|
|
64
|
+
const emotionBoost = options?.emotionWeight
|
|
65
|
+
? Math.log(1 + options.emotionWeight * config.emotionalBoostFactor)
|
|
66
|
+
: 0;
|
|
67
|
+
|
|
68
|
+
const spreading = options?.spreadingSum ?? 0;
|
|
69
|
+
const noise = options?.noiseOverride ?? activationNoise(config.activationNoise);
|
|
70
|
+
|
|
71
|
+
const activation = totalActivation(baseLevel + emotionBoost, spreading, noise);
|
|
72
|
+
const latency = retrievalLatency(activation, config.latencyFactor, config.latencyExponent);
|
|
73
|
+
|
|
74
|
+
return { activation, baseLevel: baseLevel + emotionBoost, spreading, noise, latency };
|
|
75
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
|
+
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
|
+
import type { Association, AssociationType, Memory } from "./memory.ts";
|
|
4
|
+
import { generateId } from "./memory.ts";
|
|
5
|
+
import { extractKeywords } from "./search.ts";
|
|
6
|
+
|
|
7
|
+
export function formAssociation(
|
|
8
|
+
storage: EngramStorage,
|
|
9
|
+
sourceId: string,
|
|
10
|
+
targetId: string,
|
|
11
|
+
type: AssociationType,
|
|
12
|
+
strength?: number,
|
|
13
|
+
now?: number,
|
|
14
|
+
): Association {
|
|
15
|
+
const assoc: Association = {
|
|
16
|
+
id: generateId(),
|
|
17
|
+
sourceId,
|
|
18
|
+
targetId,
|
|
19
|
+
strength: strength ?? 0.5,
|
|
20
|
+
formedAt: now ?? Date.now(),
|
|
21
|
+
type,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
storage.insertAssociation(assoc);
|
|
25
|
+
return assoc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function strengthenAssociation(
|
|
29
|
+
storage: EngramStorage,
|
|
30
|
+
associationId: string,
|
|
31
|
+
boost: number = 0.1,
|
|
32
|
+
): void {
|
|
33
|
+
storage.updateAssociationStrength(associationId, Math.min(1.0, boost));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formTemporalAssociations(
|
|
37
|
+
storage: EngramStorage,
|
|
38
|
+
memory: Memory,
|
|
39
|
+
windowMs: number = 300000,
|
|
40
|
+
now?: number,
|
|
41
|
+
): Association[] {
|
|
42
|
+
const currentTime = now ?? Date.now();
|
|
43
|
+
const allMemories = storage.getAllMemories();
|
|
44
|
+
const formed: Association[] = [];
|
|
45
|
+
|
|
46
|
+
for (const other of allMemories) {
|
|
47
|
+
if (other.id === memory.id) continue;
|
|
48
|
+
|
|
49
|
+
const timeDiff = Math.abs(memory.encodedAt - other.encodedAt);
|
|
50
|
+
if (timeDiff <= windowMs) {
|
|
51
|
+
const existing = storage.getAssociationsFrom(memory.id);
|
|
52
|
+
const alreadyLinked = existing.some((a) => a.targetId === other.id);
|
|
53
|
+
if (alreadyLinked) continue;
|
|
54
|
+
|
|
55
|
+
const strength = 1 - timeDiff / windowMs;
|
|
56
|
+
const assoc = formAssociation(
|
|
57
|
+
storage,
|
|
58
|
+
memory.id,
|
|
59
|
+
other.id,
|
|
60
|
+
"temporal",
|
|
61
|
+
Math.max(0.1, strength * 0.8),
|
|
62
|
+
currentTime,
|
|
63
|
+
);
|
|
64
|
+
formed.push(assoc);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return formed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formSemanticAssociations(
|
|
72
|
+
storage: EngramStorage,
|
|
73
|
+
memory: Memory,
|
|
74
|
+
now?: number,
|
|
75
|
+
): Association[] {
|
|
76
|
+
const currentTime = now ?? Date.now();
|
|
77
|
+
const keywords = extractKeywords(memory.content);
|
|
78
|
+
if (keywords.length === 0) return [];
|
|
79
|
+
|
|
80
|
+
const allMemories = storage.getAllMemories();
|
|
81
|
+
const formed: Association[] = [];
|
|
82
|
+
|
|
83
|
+
for (const other of allMemories) {
|
|
84
|
+
if (other.id === memory.id) continue;
|
|
85
|
+
|
|
86
|
+
const otherKeywords = extractKeywords(other.content);
|
|
87
|
+
const overlap = keywords.filter((k) => otherKeywords.includes(k));
|
|
88
|
+
|
|
89
|
+
if (overlap.length > 0) {
|
|
90
|
+
const existing = storage.getAssociations(memory.id);
|
|
91
|
+
const alreadyLinked = existing.some(
|
|
92
|
+
(a) =>
|
|
93
|
+
(a.sourceId === memory.id && a.targetId === other.id) ||
|
|
94
|
+
(a.sourceId === other.id && a.targetId === memory.id),
|
|
95
|
+
);
|
|
96
|
+
if (alreadyLinked) continue;
|
|
97
|
+
|
|
98
|
+
const strength = overlap.length / Math.max(keywords.length, otherKeywords.length);
|
|
99
|
+
const assoc = formAssociation(
|
|
100
|
+
storage,
|
|
101
|
+
memory.id,
|
|
102
|
+
other.id,
|
|
103
|
+
"semantic",
|
|
104
|
+
Math.max(0.1, strength),
|
|
105
|
+
currentTime,
|
|
106
|
+
);
|
|
107
|
+
formed.push(assoc);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return formed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function recordCoRecall(
|
|
115
|
+
storage: EngramStorage,
|
|
116
|
+
memoryIds: string[],
|
|
117
|
+
now?: number,
|
|
118
|
+
): Association[] {
|
|
119
|
+
const currentTime = now ?? Date.now();
|
|
120
|
+
const formed: Association[] = [];
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < memoryIds.length; i++) {
|
|
123
|
+
for (let j = i + 1; j < memoryIds.length; j++) {
|
|
124
|
+
const srcId = memoryIds[i]!;
|
|
125
|
+
const tgtId = memoryIds[j]!;
|
|
126
|
+
const existing = storage.getAssociations(srcId);
|
|
127
|
+
const link = existing.find(
|
|
128
|
+
(a) =>
|
|
129
|
+
(a.sourceId === srcId && a.targetId === tgtId) ||
|
|
130
|
+
(a.sourceId === tgtId && a.targetId === srcId),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (link) {
|
|
134
|
+
const newStrength = Math.min(1.0, link.strength + 0.1);
|
|
135
|
+
storage.updateAssociationStrength(link.id, newStrength);
|
|
136
|
+
} else {
|
|
137
|
+
const assoc = formAssociation(storage, srcId, tgtId, "co-recall", 0.3, currentTime);
|
|
138
|
+
formed.push(assoc);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return formed;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getSpreadingActivationTargets(
|
|
147
|
+
storage: EngramStorage,
|
|
148
|
+
sourceId: string,
|
|
149
|
+
config: CognitiveConfig,
|
|
150
|
+
maxDepth: number = 2,
|
|
151
|
+
): { memoryId: string; activationBoost: number; depth: number }[] {
|
|
152
|
+
const visited = new Set<string>([sourceId]);
|
|
153
|
+
const results: { memoryId: string; activationBoost: number; depth: number }[] = [];
|
|
154
|
+
|
|
155
|
+
let frontier = [{ id: sourceId, boost: 1.0 }];
|
|
156
|
+
|
|
157
|
+
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
158
|
+
const nextFrontier: { id: string; boost: number }[] = [];
|
|
159
|
+
|
|
160
|
+
for (const { id, boost } of frontier) {
|
|
161
|
+
const associations = storage.getAssociations(id);
|
|
162
|
+
|
|
163
|
+
for (const assoc of associations) {
|
|
164
|
+
const targetId = assoc.sourceId === id ? assoc.targetId : assoc.sourceId;
|
|
165
|
+
if (visited.has(targetId)) continue;
|
|
166
|
+
visited.add(targetId);
|
|
167
|
+
|
|
168
|
+
const fanCount = storage.getFanCount(id);
|
|
169
|
+
const spreadStrength = Math.max(
|
|
170
|
+
0,
|
|
171
|
+
config.maxSpreadingActivation - Math.log(Math.max(1, fanCount)),
|
|
172
|
+
);
|
|
173
|
+
const activationBoost = boost * assoc.strength * spreadStrength;
|
|
174
|
+
|
|
175
|
+
if (activationBoost > 0.01) {
|
|
176
|
+
results.push({ memoryId: targetId, activationBoost, depth });
|
|
177
|
+
nextFrontier.push({ id: targetId, boost: activationBoost });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
frontier = nextFrontier;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return results;
|
|
186
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
|
+
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
|
+
import type { Memory } from "./memory.ts";
|
|
4
|
+
import { generateId } from "./memory.ts";
|
|
5
|
+
import { extractKeywords } from "./search.ts";
|
|
6
|
+
|
|
7
|
+
export interface Chunk {
|
|
8
|
+
id: string;
|
|
9
|
+
memberIds: string[];
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
class UnionFind {
|
|
14
|
+
private parent: Map<string, string> = new Map();
|
|
15
|
+
private rank: Map<string, number> = new Map();
|
|
16
|
+
|
|
17
|
+
add(x: string): void {
|
|
18
|
+
if (!this.parent.has(x)) {
|
|
19
|
+
this.parent.set(x, x);
|
|
20
|
+
this.rank.set(x, 0);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
find(x: string): string {
|
|
25
|
+
const p = this.parent.get(x);
|
|
26
|
+
if (p === undefined) return x;
|
|
27
|
+
if (p !== x) {
|
|
28
|
+
const root = this.find(p);
|
|
29
|
+
this.parent.set(x, root);
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
return x;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
union(a: string, b: string): void {
|
|
36
|
+
const rootA = this.find(a);
|
|
37
|
+
const rootB = this.find(b);
|
|
38
|
+
if (rootA === rootB) return;
|
|
39
|
+
|
|
40
|
+
const rankA = this.rank.get(rootA) ?? 0;
|
|
41
|
+
const rankB = this.rank.get(rootB) ?? 0;
|
|
42
|
+
|
|
43
|
+
if (rankA < rankB) {
|
|
44
|
+
this.parent.set(rootA, rootB);
|
|
45
|
+
} else if (rankA > rankB) {
|
|
46
|
+
this.parent.set(rootB, rootA);
|
|
47
|
+
} else {
|
|
48
|
+
this.parent.set(rootB, rootA);
|
|
49
|
+
this.rank.set(rootA, rankA + 1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
components(): Map<string, string[]> {
|
|
54
|
+
const groups = new Map<string, string[]>();
|
|
55
|
+
for (const id of this.parent.keys()) {
|
|
56
|
+
const root = this.find(id);
|
|
57
|
+
const group = groups.get(root) ?? [];
|
|
58
|
+
group.push(id);
|
|
59
|
+
groups.set(root, group);
|
|
60
|
+
}
|
|
61
|
+
return groups;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function discoverChunks(storage: EngramStorage, config: CognitiveConfig): Chunk[] {
|
|
66
|
+
const allMemories = storage.getAllMemories();
|
|
67
|
+
const memoryMap = new Map<string, Memory>();
|
|
68
|
+
const uf = new UnionFind();
|
|
69
|
+
|
|
70
|
+
for (const memory of allMemories) {
|
|
71
|
+
if (memory.chunkId) continue;
|
|
72
|
+
memoryMap.set(memory.id, memory);
|
|
73
|
+
uf.add(memory.id);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const memory of memoryMap.values()) {
|
|
77
|
+
const associations = storage.getAssociations(memory.id);
|
|
78
|
+
for (const assoc of associations) {
|
|
79
|
+
if (assoc.strength < config.chunkingSimilarityThreshold) continue;
|
|
80
|
+
const otherId = assoc.sourceId === memory.id ? assoc.targetId : assoc.sourceId;
|
|
81
|
+
if (!memoryMap.has(otherId)) continue;
|
|
82
|
+
uf.union(memory.id, otherId);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chunks: Chunk[] = [];
|
|
87
|
+
for (const memberIds of uf.components().values()) {
|
|
88
|
+
if (memberIds.length < 2) continue;
|
|
89
|
+
|
|
90
|
+
const members = memberIds.map((id) => memoryMap.get(id)!);
|
|
91
|
+
const chunkId = generateId();
|
|
92
|
+
const keywords = extractKeywords(members.map((m) => m.content).join(" "), 3);
|
|
93
|
+
const label = keywords.join(" + ") || "chunk";
|
|
94
|
+
|
|
95
|
+
for (const member of members) {
|
|
96
|
+
member.chunkId = chunkId;
|
|
97
|
+
storage.updateMemory(member);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
chunks.push({ id: chunkId, memberIds, label });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return chunks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getChunkMembers(storage: EngramStorage, chunkId: string): Memory[] {
|
|
107
|
+
return storage.getAllMemories().filter((m) => m.chunkId === chunkId);
|
|
108
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
|
+
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
|
+
import type { Memory, ConsolidationLog } from "./memory.ts";
|
|
4
|
+
import { generateId } from "./memory.ts";
|
|
5
|
+
import { refreshActivations } from "./forgetting.ts";
|
|
6
|
+
import { formSemanticAssociations, formTemporalAssociations } from "./associations.ts";
|
|
7
|
+
import { encode } from "./encoder.ts";
|
|
8
|
+
import { extractKeywords, tokenize } from "./search.ts";
|
|
9
|
+
|
|
10
|
+
export interface ConsolidationResult {
|
|
11
|
+
memoriesStrengthened: number;
|
|
12
|
+
memoriesPruned: number;
|
|
13
|
+
factsExtracted: number;
|
|
14
|
+
associationsDiscovered: number;
|
|
15
|
+
prunedIds: string[];
|
|
16
|
+
extractedFacts: string[];
|
|
17
|
+
discoveredAssociationPairs: [string, string][];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function consolidate(
|
|
21
|
+
storage: EngramStorage,
|
|
22
|
+
config: CognitiveConfig,
|
|
23
|
+
now?: number,
|
|
24
|
+
): ConsolidationResult {
|
|
25
|
+
const currentTime = now ?? Date.now();
|
|
26
|
+
|
|
27
|
+
const result: ConsolidationResult = {
|
|
28
|
+
memoriesStrengthened: 0,
|
|
29
|
+
memoriesPruned: 0,
|
|
30
|
+
factsExtracted: 0,
|
|
31
|
+
associationsDiscovered: 0,
|
|
32
|
+
prunedIds: [],
|
|
33
|
+
extractedFacts: [],
|
|
34
|
+
discoveredAssociationPairs: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
refreshActivations(storage, config, currentTime);
|
|
38
|
+
|
|
39
|
+
const allMemories = storage.getAllMemories();
|
|
40
|
+
for (const memory of allMemories) {
|
|
41
|
+
if (memory.type === "procedural") continue;
|
|
42
|
+
|
|
43
|
+
const timestamps = storage.getAccessTimestamps(memory.id);
|
|
44
|
+
const recentAccesses = timestamps.filter((t) => currentTime - t < 86400000);
|
|
45
|
+
|
|
46
|
+
if (recentAccesses.length >= 2) {
|
|
47
|
+
storage.logAccess(memory.id, "consolidate", currentTime);
|
|
48
|
+
result.memoriesStrengthened++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
refreshActivations(storage, config, currentTime);
|
|
53
|
+
const weakMemories = storage.getMemoriesBelowActivation(config.pruningThreshold);
|
|
54
|
+
for (const memory of weakMemories) {
|
|
55
|
+
result.prunedIds.push(memory.id);
|
|
56
|
+
storage.deleteMemory(memory.id);
|
|
57
|
+
result.memoriesPruned++;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const extractedFacts = extractSemanticFacts(storage, config, currentTime);
|
|
61
|
+
result.factsExtracted = extractedFacts.length;
|
|
62
|
+
result.extractedFacts = extractedFacts;
|
|
63
|
+
|
|
64
|
+
const remainingMemories = storage.getAllMemories();
|
|
65
|
+
for (const memory of remainingMemories) {
|
|
66
|
+
const temporalAssocs = formTemporalAssociations(storage, memory, 300000, currentTime);
|
|
67
|
+
const semanticAssocs = formSemanticAssociations(storage, memory, currentTime);
|
|
68
|
+
|
|
69
|
+
for (const assoc of [...temporalAssocs, ...semanticAssocs]) {
|
|
70
|
+
result.associationsDiscovered++;
|
|
71
|
+
result.discoveredAssociationPairs.push([assoc.sourceId, assoc.targetId]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
storage.deleteWeakAssociations(config.minAssociationStrength);
|
|
76
|
+
|
|
77
|
+
const logEntry: ConsolidationLog = {
|
|
78
|
+
id: generateId(),
|
|
79
|
+
ranAt: currentTime,
|
|
80
|
+
memoriesStrengthened: result.memoriesStrengthened,
|
|
81
|
+
memoriesPruned: result.memoriesPruned,
|
|
82
|
+
factsExtracted: result.factsExtracted,
|
|
83
|
+
associationsDiscovered: result.associationsDiscovered,
|
|
84
|
+
};
|
|
85
|
+
storage.logConsolidation(logEntry);
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractSemanticFacts(
|
|
91
|
+
storage: EngramStorage,
|
|
92
|
+
config: CognitiveConfig,
|
|
93
|
+
now: number,
|
|
94
|
+
): string[] {
|
|
95
|
+
const episodics = storage.getAllMemories("episodic");
|
|
96
|
+
if (episodics.length < config.semanticExtractionThreshold) return [];
|
|
97
|
+
|
|
98
|
+
const extracted: string[] = [];
|
|
99
|
+
|
|
100
|
+
const keywordGroups = new Map<string, Memory[]>();
|
|
101
|
+
for (const memory of episodics) {
|
|
102
|
+
const keywords = extractKeywords(memory.content, 3);
|
|
103
|
+
for (const keyword of keywords) {
|
|
104
|
+
const group = keywordGroups.get(keyword) ?? [];
|
|
105
|
+
group.push(memory);
|
|
106
|
+
keywordGroups.set(keyword, group);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [keyword, group] of keywordGroups) {
|
|
111
|
+
if (group.length < config.semanticExtractionThreshold) continue;
|
|
112
|
+
|
|
113
|
+
const existing = storage.searchMemories(keyword, 5);
|
|
114
|
+
const alreadyExtracted = existing.some(
|
|
115
|
+
(m) => m.type === "semantic" && m.content.includes(`[extracted]`),
|
|
116
|
+
);
|
|
117
|
+
if (alreadyExtracted) continue;
|
|
118
|
+
|
|
119
|
+
const allTokenSets = group.map((m) => new Set(tokenize(m.content)));
|
|
120
|
+
const sharedTokens: string[] = [];
|
|
121
|
+
const firstSet = allTokenSets[0];
|
|
122
|
+
if (firstSet) {
|
|
123
|
+
for (const token of firstSet) {
|
|
124
|
+
if (allTokenSets.every((s) => s.has(token))) {
|
|
125
|
+
sharedTokens.push(token);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const factContent =
|
|
131
|
+
sharedTokens.length > 1
|
|
132
|
+
? `[extracted] Pattern observed across ${group.length} episodes: ${sharedTokens.join(", ")}`
|
|
133
|
+
: `[extracted] Recurring theme (${group.length}x): ${keyword}`;
|
|
134
|
+
|
|
135
|
+
encode(
|
|
136
|
+
storage,
|
|
137
|
+
{
|
|
138
|
+
content: factContent,
|
|
139
|
+
type: "semantic",
|
|
140
|
+
context: group[0]?.context ?? undefined,
|
|
141
|
+
},
|
|
142
|
+
config,
|
|
143
|
+
now,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
extracted.push(factContent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return extracted;
|
|
150
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Emotion } from "./memory.ts";
|
|
2
|
+
|
|
3
|
+
const EMOTION_WEIGHTS: Record<Emotion, number> = {
|
|
4
|
+
anxiety: 0.8,
|
|
5
|
+
frustration: 0.6,
|
|
6
|
+
surprise: 0.7,
|
|
7
|
+
joy: 0.5,
|
|
8
|
+
satisfaction: 0.4,
|
|
9
|
+
curiosity: 0.3,
|
|
10
|
+
neutral: 0.0,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function defaultEmotionWeight(emotion: Emotion): number {
|
|
14
|
+
return EMOTION_WEIGHTS[emotion] ?? 0.0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isValidEmotion(s: string): s is Emotion {
|
|
18
|
+
return (Object.values(Emotion) as string[]).includes(s);
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
|
+
import type { EngramStorage } from "../storage/sqlite.ts";
|
|
3
|
+
import type { Memory, EncodeInput } from "./memory.ts";
|
|
4
|
+
import { generateMemoryId } from "./memory.ts";
|
|
5
|
+
import { defaultEmotionWeight } from "./emotional-tag.ts";
|
|
6
|
+
import { baseLevelActivation } from "./activation.ts";
|
|
7
|
+
|
|
8
|
+
export function encode(
|
|
9
|
+
storage: EngramStorage,
|
|
10
|
+
input: EncodeInput,
|
|
11
|
+
config: CognitiveConfig,
|
|
12
|
+
now?: number,
|
|
13
|
+
): Memory {
|
|
14
|
+
const currentTime = now ?? Date.now();
|
|
15
|
+
|
|
16
|
+
const emotion = input.emotion ?? "neutral";
|
|
17
|
+
const emotionWeight = input.emotionWeight ?? defaultEmotionWeight(emotion);
|
|
18
|
+
|
|
19
|
+
const id = generateMemoryId(input.content, input.type);
|
|
20
|
+
|
|
21
|
+
const initialActivation = baseLevelActivation([currentTime], currentTime, config.decayRate);
|
|
22
|
+
|
|
23
|
+
const emotionBoost =
|
|
24
|
+
emotionWeight > 0 ? Math.log(1 + emotionWeight * config.emotionalBoostFactor) : 0;
|
|
25
|
+
|
|
26
|
+
const memory: Memory = {
|
|
27
|
+
id,
|
|
28
|
+
type: input.type,
|
|
29
|
+
content: input.content,
|
|
30
|
+
encodedAt: currentTime,
|
|
31
|
+
lastRecalledAt: null,
|
|
32
|
+
recallCount: 0,
|
|
33
|
+
activation: initialActivation + emotionBoost,
|
|
34
|
+
emotion,
|
|
35
|
+
emotionWeight,
|
|
36
|
+
context: input.context ?? null,
|
|
37
|
+
chunkId: null,
|
|
38
|
+
reconsolidationCount: 0,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
storage.transaction(() => {
|
|
42
|
+
storage.insertMemory(memory);
|
|
43
|
+
storage.logAccess(id, "encode", currentTime);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return memory;
|
|
47
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { CognitiveConfig } from "../config/defaults.ts";
|
|
2
|
+
import { loadConfig } from "../config/defaults.ts";
|
|
3
|
+
import { EngramStorage } from "../storage/sqlite.ts";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { basename } from "node:path";
|
|
6
|
+
|
|
7
|
+
export function detectProjectContext(): string | null {
|
|
8
|
+
try {
|
|
9
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12
|
+
}).trim();
|
|
13
|
+
return `project:${basename(root)}`;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class EngramEngine {
|
|
20
|
+
readonly storage: EngramStorage;
|
|
21
|
+
readonly config: CognitiveConfig;
|
|
22
|
+
readonly projectContext: string | null;
|
|
23
|
+
|
|
24
|
+
private constructor(
|
|
25
|
+
storage: EngramStorage,
|
|
26
|
+
config: CognitiveConfig,
|
|
27
|
+
projectContext: string | null,
|
|
28
|
+
) {
|
|
29
|
+
this.storage = storage;
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.projectContext = projectContext;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static create(configOverrides?: Partial<CognitiveConfig>): EngramEngine {
|
|
35
|
+
const config = loadConfig(configOverrides);
|
|
36
|
+
const storage = EngramStorage.open(config.dbPath);
|
|
37
|
+
const projectContext = detectProjectContext();
|
|
38
|
+
return new EngramEngine(storage, config, projectContext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static inMemory(configOverrides?: Partial<CognitiveConfig>): EngramEngine {
|
|
42
|
+
const config = loadConfig(configOverrides);
|
|
43
|
+
const storage = EngramStorage.inMemory();
|
|
44
|
+
return new EngramEngine(storage, config, null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
close(): void {
|
|
48
|
+
this.storage.close();
|
|
49
|
+
}
|
|
50
|
+
}
|