@cogmem/engram 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,5 @@
1
1
  # engram
2
2
 
3
- **Human memory for artificial minds.**
4
-
5
3
  > **Note:** This is an experiment in cognitive memory architecture. It's a research prototype, not production software.
6
4
 
7
5
  Every AI agent today has amnesia. They process, respond, and forget. engram fixes this — not with a smarter key-value store, but with a cognitive memory system modeled on how the human brain actually forms, stores, recalls, and forgets information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogmem/engram",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Human memory for artificial minds — a cognitive memory system modeled on neuroscience",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,9 @@ export interface CognitiveConfig {
14
14
  reconsolidationBlendRate: number;
15
15
  chunkingSimilarityThreshold: number;
16
16
  semanticExtractionThreshold: number;
17
+ temporalContextWindow: number;
18
+ recallSpreadingDepth: number;
19
+ workingMemoryPrimingWeight: number;
17
20
  dbPath: string;
18
21
  }
19
22
 
@@ -33,6 +36,9 @@ export const DEFAULT_CONFIG: CognitiveConfig = {
33
36
  reconsolidationBlendRate: 0.1,
34
37
  chunkingSimilarityThreshold: 0.6,
35
38
  semanticExtractionThreshold: 3,
39
+ temporalContextWindow: 10,
40
+ recallSpreadingDepth: 3,
41
+ workingMemoryPrimingWeight: 0.5,
36
42
  dbPath: "~/.engram/memory.db",
37
43
  };
38
44
 
@@ -1,9 +1,21 @@
1
1
  import type { CognitiveConfig } from "../config/defaults.ts";
2
2
  import type { EngramStorage } from "../storage/sqlite.ts";
3
- import type { Association, AssociationType, Memory } from "./memory.ts";
3
+ import type { Association, AssociationType, Emotion, Memory } from "./memory.ts";
4
4
  import { generateId } from "./memory.ts";
5
5
  import { extractKeywords } from "./search.ts";
6
6
 
7
+ type ArousalTier = "high" | "medium" | "low";
8
+
9
+ const AROUSAL_TIERS: Record<Emotion, ArousalTier> = {
10
+ anxiety: "high",
11
+ surprise: "high",
12
+ joy: "medium",
13
+ frustration: "medium",
14
+ satisfaction: "low",
15
+ curiosity: "low",
16
+ neutral: "low",
17
+ };
18
+
7
19
  export function formAssociation(
8
20
  storage: EngramStorage,
9
21
  sourceId: string,
@@ -36,33 +48,62 @@ export function strengthenAssociation(
36
48
  export function formTemporalAssociations(
37
49
  storage: EngramStorage,
38
50
  memory: Memory,
39
- windowMs: number = 300000,
51
+ config: CognitiveConfig,
40
52
  now?: number,
41
53
  ): Association[] {
42
54
  const currentTime = now ?? Date.now();
43
- const allMemories = storage.getAllMemories();
44
55
  const formed: Association[] = [];
45
56
 
46
- for (const other of allMemories) {
47
- if (other.id === memory.id) continue;
57
+ if (memory.context) {
58
+ const contextMemories = storage
59
+ .getMemoriesByContext(memory.context, undefined, config.temporalContextWindow)
60
+ .filter((m) => m.id !== memory.id)
61
+ .sort((a, b) => b.encodedAt - a.encodedAt);
62
+
63
+ for (let i = 0; i < contextMemories.length; i++) {
64
+ const other = contextMemories[i]!;
65
+ const positionGap = i + 1;
66
+ const strength = 1 / (1 + positionGap);
48
67
 
49
- const timeDiff = Math.abs(memory.encodedAt - other.encodedAt);
50
- if (timeDiff <= windowMs) {
51
68
  const existing = storage.getAssociationsFrom(memory.id);
52
69
  const alreadyLinked = existing.some((a) => a.targetId === other.id);
53
70
  if (alreadyLinked) continue;
54
71
 
55
- const strength = 1 - timeDiff / windowMs;
56
72
  const assoc = formAssociation(
57
73
  storage,
58
74
  memory.id,
59
75
  other.id,
60
76
  "temporal",
61
- Math.max(0.1, strength * 0.8),
77
+ Math.max(0.1, strength),
62
78
  currentTime,
63
79
  );
64
80
  formed.push(assoc);
65
81
  }
82
+ } else {
83
+ const windowMs = 300000;
84
+ const allMemories = storage.getAllMemories();
85
+
86
+ for (const other of allMemories) {
87
+ if (other.id === memory.id) continue;
88
+
89
+ const timeDiff = Math.abs(memory.encodedAt - other.encodedAt);
90
+ if (timeDiff <= windowMs) {
91
+ const existing = storage.getAssociationsFrom(memory.id);
92
+ const alreadyLinked = existing.some((a) => a.targetId === other.id);
93
+ if (alreadyLinked) continue;
94
+
95
+ const strength = 1 - timeDiff / windowMs;
96
+ const assoc = formAssociation(
97
+ storage,
98
+ memory.id,
99
+ other.id,
100
+ "temporal",
101
+ Math.max(0.1, strength * 0.8),
102
+ currentTime,
103
+ );
104
+ formed.push(assoc);
105
+ }
106
+ }
66
107
  }
67
108
 
68
109
  return formed;
@@ -111,6 +152,86 @@ export function formSemanticAssociations(
111
152
  return formed;
112
153
  }
113
154
 
155
+ export function formEmotionalAssociations(
156
+ storage: EngramStorage,
157
+ memory: Memory,
158
+ now?: number,
159
+ ): Association[] {
160
+ if (memory.emotion === "neutral" || memory.emotionWeight <= 0.3) return [];
161
+
162
+ const currentTime = now ?? Date.now();
163
+ const allMemories = storage.getAllMemories();
164
+ const formed: Association[] = [];
165
+ const memoryTier = AROUSAL_TIERS[memory.emotion];
166
+
167
+ for (const other of allMemories) {
168
+ if (other.id === memory.id) continue;
169
+ if (other.emotion === "neutral" || other.emotionWeight <= 0.3) continue;
170
+
171
+ const existing = storage.getAssociations(memory.id);
172
+ const alreadyLinked = existing.some(
173
+ (a) =>
174
+ a.type === "emotional" &&
175
+ ((a.sourceId === memory.id && a.targetId === other.id) ||
176
+ (a.sourceId === other.id && a.targetId === memory.id)),
177
+ );
178
+ if (alreadyLinked) continue;
179
+
180
+ let strength: number;
181
+ if (memory.emotion === other.emotion) {
182
+ strength = 1 - Math.abs(memory.emotionWeight - other.emotionWeight);
183
+ } else if (AROUSAL_TIERS[other.emotion] === memoryTier) {
184
+ strength = 0.3 * (1 - Math.abs(memory.emotionWeight - other.emotionWeight));
185
+ } else {
186
+ continue;
187
+ }
188
+
189
+ if (strength < 0.1) continue;
190
+
191
+ const assoc = formAssociation(storage, memory.id, other.id, "emotional", strength, currentTime);
192
+ formed.push(assoc);
193
+ }
194
+
195
+ return formed;
196
+ }
197
+
198
+ export function formCausalAssociations(
199
+ storage: EngramStorage,
200
+ memory: Memory,
201
+ config: CognitiveConfig,
202
+ now?: number,
203
+ ): Association[] {
204
+ if (!memory.context) return [];
205
+
206
+ const currentTime = now ?? Date.now();
207
+ const contextMemories = storage
208
+ .getMemoriesByContext(memory.context, undefined, config.temporalContextWindow)
209
+ .filter((m) => m.id !== memory.id && m.encodedAt < memory.encodedAt)
210
+ .sort((a, b) => b.encodedAt - a.encodedAt);
211
+
212
+ const formed: Association[] = [];
213
+
214
+ for (let i = 0; i < contextMemories.length; i++) {
215
+ const source = contextMemories[i]!;
216
+ const sequenceGap = i + 1;
217
+ const strength = 1 / (1 + sequenceGap);
218
+
219
+ const existing = storage.getAssociations(memory.id);
220
+ const alreadyLinked = existing.some(
221
+ (a) =>
222
+ a.type === "causal" &&
223
+ ((a.sourceId === source.id && a.targetId === memory.id) ||
224
+ (a.sourceId === memory.id && a.targetId === source.id)),
225
+ );
226
+ if (alreadyLinked) continue;
227
+
228
+ const assoc = formAssociation(storage, source.id, memory.id, "causal", strength, currentTime);
229
+ formed.push(assoc);
230
+ }
231
+
232
+ return formed;
233
+ }
234
+
114
235
  export function recordCoRecall(
115
236
  storage: EngramStorage,
116
237
  memoryIds: string[],
@@ -147,7 +268,7 @@ export function getSpreadingActivationTargets(
147
268
  storage: EngramStorage,
148
269
  sourceId: string,
149
270
  config: CognitiveConfig,
150
- maxDepth: number = 2,
271
+ maxDepth: number = config.recallSpreadingDepth,
151
272
  ): { memoryId: string; activationBoost: number; depth: number }[] {
152
273
  const visited = new Set<string>([sourceId]);
153
274
  const results: { memoryId: string; activationBoost: number; depth: number }[] = [];
@@ -3,7 +3,12 @@ import type { EngramStorage } from "../storage/sqlite.ts";
3
3
  import type { Memory, ConsolidationLog } from "./memory.ts";
4
4
  import { generateId } from "./memory.ts";
5
5
  import { refreshActivations } from "./forgetting.ts";
6
- import { formSemanticAssociations, formTemporalAssociations } from "./associations.ts";
6
+ import {
7
+ formSemanticAssociations,
8
+ formTemporalAssociations,
9
+ formEmotionalAssociations,
10
+ formCausalAssociations,
11
+ } from "./associations.ts";
7
12
  import { encode } from "./encoder.ts";
8
13
  import { extractKeywords, tokenize } from "./search.ts";
9
14
 
@@ -63,10 +68,12 @@ export function consolidate(
63
68
 
64
69
  const remainingMemories = storage.getAllMemories();
65
70
  for (const memory of remainingMemories) {
66
- const temporalAssocs = formTemporalAssociations(storage, memory, 300000, currentTime);
71
+ const temporalAssocs = formTemporalAssociations(storage, memory, config, currentTime);
67
72
  const semanticAssocs = formSemanticAssociations(storage, memory, currentTime);
73
+ const emotionalAssocs = formEmotionalAssociations(storage, memory, currentTime);
74
+ const causalAssocs = formCausalAssociations(storage, memory, config, currentTime);
68
75
 
69
- for (const assoc of [...temporalAssocs, ...semanticAssocs]) {
76
+ for (const assoc of [...temporalAssocs, ...semanticAssocs, ...emotionalAssocs, ...causalAssocs]) {
70
77
  result.associationsDiscovered++;
71
78
  result.discoveredAssociationPairs.push([assoc.sourceId, assoc.targetId]);
72
79
  }
@@ -4,6 +4,7 @@ import type { Memory, EncodeInput } from "./memory.ts";
4
4
  import { generateMemoryId } from "./memory.ts";
5
5
  import { defaultEmotionWeight } from "./emotional-tag.ts";
6
6
  import { baseLevelActivation } from "./activation.ts";
7
+ import { formEmotionalAssociations, formCausalAssociations } from "./associations.ts";
7
8
 
8
9
  export function encode(
9
10
  storage: EngramStorage,
@@ -43,5 +44,8 @@ export function encode(
43
44
  storage.logAccess(id, "encode", currentTime);
44
45
  });
45
46
 
47
+ formEmotionalAssociations(storage, memory, currentTime);
48
+ formCausalAssociations(storage, memory, config, currentTime);
49
+
46
50
  return memory;
47
51
  }
@@ -29,6 +29,8 @@ export const AssociationType = {
29
29
  Temporal: "temporal",
30
30
  Semantic: "semantic",
31
31
  CoRecall: "co-recall",
32
+ Emotional: "emotional",
33
+ Causal: "causal",
32
34
  } as const;
33
35
  export type AssociationType = (typeof AssociationType)[keyof typeof AssociationType];
34
36
 
@@ -2,6 +2,8 @@ import type { CognitiveConfig } from "../config/defaults.ts";
2
2
  import type { EngramStorage } from "../storage/sqlite.ts";
3
3
  import type { Memory, RecallResult } from "./memory.ts";
4
4
  import { computeActivation, spreadingActivationStrength } from "./activation.ts";
5
+ import { getSpreadingActivationTargets } from "./associations.ts";
6
+ import { getWorkingMemoryIds } from "./working-memory.ts";
5
7
 
6
8
  export function recall(
7
9
  storage: EngramStorage,
@@ -20,22 +22,17 @@ export function recall(
20
22
  const limit = options?.limit ?? 10;
21
23
  const associative = options?.associative ?? true;
22
24
 
23
- const candidateMap = new Map<string, Memory>();
25
+ const seedIds = new Set<string>();
26
+
27
+ const wmIds = getWorkingMemoryIds(storage);
28
+ for (const id of wmIds) seedIds.add(id);
24
29
 
25
30
  const ftsIds = storage.searchFTS(cue, limit * 2);
26
- for (const id of ftsIds) {
27
- const m = storage.getMemory(id);
28
- if (!m) continue;
29
- if (options?.type && m.type !== options.type) continue;
30
- if (options?.context && (!m.context || !m.context.startsWith(options.context))) continue;
31
- candidateMap.set(m.id, m);
32
- }
31
+ for (const id of ftsIds) seedIds.add(id);
33
32
 
34
33
  if (options?.context) {
35
34
  const contextMatches = storage.getMemoriesByContext(options.context, options?.type, limit * 2);
36
- for (const m of contextMatches) {
37
- candidateMap.set(m.id, m);
38
- }
35
+ for (const m of contextMatches) seedIds.add(m.id);
39
36
  }
40
37
 
41
38
  const allCandidates = options?.type
@@ -49,6 +46,32 @@ export function recall(
49
46
 
50
47
  const sorted = filtered.sort((a, b) => b.activation - a.activation);
51
48
  for (const m of sorted.slice(0, limit)) {
49
+ seedIds.add(m.id);
50
+ }
51
+
52
+ const candidateIds = new Set<string>(seedIds);
53
+ const graphBoosts = new Map<string, number>();
54
+
55
+ if (associative) {
56
+ for (const seedId of seedIds) {
57
+ const targets = getSpreadingActivationTargets(storage, seedId, config);
58
+ const isWmSeed = wmIds.includes(seedId);
59
+ const primingWeight = isWmSeed ? config.workingMemoryPrimingWeight : 1.0;
60
+
61
+ for (const t of targets) {
62
+ candidateIds.add(t.memoryId);
63
+ const existing = graphBoosts.get(t.memoryId) ?? 0;
64
+ graphBoosts.set(t.memoryId, existing + t.activationBoost * primingWeight);
65
+ }
66
+ }
67
+ }
68
+
69
+ const candidateMap = new Map<string, Memory>();
70
+ for (const id of candidateIds) {
71
+ const m = storage.getMemory(id);
72
+ if (!m) continue;
73
+ if (options?.type && m.type !== options.type) continue;
74
+ if (options?.context && (!m.context || !m.context.startsWith(options.context))) continue;
52
75
  candidateMap.set(m.id, m);
53
76
  }
54
77
 
@@ -59,8 +82,8 @@ export function recall(
59
82
  for (const memory of candidateMap.values()) {
60
83
  const timestamps = storage.getAccessTimestamps(memory.id);
61
84
 
62
- let spreadingSum = 0;
63
- if (associative) {
85
+ let spreadingSum = graphBoosts.get(memory.id) ?? 0;
86
+ if (associative && spreadingSum === 0) {
64
87
  const assocFrom = storage.getAssociationsFrom(memory.id);
65
88
  const assocTo = storage.getAssociationsTo(memory.id);
66
89
  const allAssocs = [...assocFrom, ...assocTo];
@@ -54,6 +54,13 @@ export function clearFocus(storage: EngramStorage): number {
54
54
  return count;
55
55
  }
56
56
 
57
+ export function getWorkingMemoryIds(storage: EngramStorage): string[] {
58
+ return storage
59
+ .getWorkingMemory()
60
+ .filter((s) => s.memoryRef !== null)
61
+ .map((s) => s.memoryRef!);
62
+ }
63
+
57
64
  export function focusUtilization(
58
65
  storage: EngramStorage,
59
66
  config: CognitiveConfig,
package/src/index.ts CHANGED
@@ -10,11 +10,14 @@ export {
10
10
  getFocus,
11
11
  clearFocus,
12
12
  focusUtilization,
13
+ getWorkingMemoryIds,
13
14
  } from "./core/working-memory.ts";
14
15
  export {
15
16
  formAssociation,
16
17
  formTemporalAssociations,
17
18
  formSemanticAssociations,
19
+ formEmotionalAssociations,
20
+ formCausalAssociations,
18
21
  recordCoRecall,
19
22
  getSpreadingActivationTargets,
20
23
  } from "./core/associations.ts";
package/src/mcp/server.ts CHANGED
@@ -62,7 +62,7 @@ server.registerTool(
62
62
  description: `Actions: recall(cue) — cue-based retrieval | list — browse without activation effects | inspect(id) — full lifecycle | stats — system overview. Optional: limit, type, context, format, verbose.`,
63
63
  inputSchema: z.discriminatedUnion("action", [
64
64
  z.object({
65
- action: z.literal("recall").optional().default("recall"),
65
+ action: z.literal("recall"),
66
66
  cue: z.string().describe("Recall cue"),
67
67
  limit: z.number().optional().describe("Max results (default: 5)"),
68
68
  type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),