@cogmem/engram 0.0.0 → 0.2.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 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.
@@ -167,7 +165,7 @@ Add to your MCP client configuration (e.g., Claude Code `settings.json`):
167
165
  "mcpServers": {
168
166
  "engram": {
169
167
  "command": "bunx",
170
- "args": ["@cogmem/engram-mcp"]
168
+ "args": ["-p", "@cogmem/engram", "engram-mcp"]
171
169
  }
172
170
  }
173
171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogmem/engram",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
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,15 +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);
31
+ for (const id of ftsIds) seedIds.add(id);
32
+
33
+ if (options?.context) {
34
+ const contextMatches = storage.getMemoriesByContext(options.context, options?.type, limit * 2);
35
+ for (const m of contextMatches) seedIds.add(m.id);
32
36
  }
33
37
 
34
38
  const allCandidates = options?.type
@@ -42,6 +46,32 @@ export function recall(
42
46
 
43
47
  const sorted = filtered.sort((a, b) => b.activation - a.activation);
44
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;
45
75
  candidateMap.set(m.id, m);
46
76
  }
47
77
 
@@ -52,8 +82,8 @@ export function recall(
52
82
  for (const memory of candidateMap.values()) {
53
83
  const timestamps = storage.getAccessTimestamps(memory.id);
54
84
 
55
- let spreadingSum = 0;
56
- if (associative) {
85
+ let spreadingSum = graphBoosts.get(memory.id) ?? 0;
86
+ if (associative && spreadingSum === 0) {
57
87
  const assocFrom = storage.getAssociationsFrom(memory.id);
58
88
  const assocTo = storage.getAssociationsTo(memory.id);
59
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
@@ -17,11 +17,7 @@ server.registerTool(
17
17
  "memory_store",
18
18
  {
19
19
  title: "Store Memory",
20
- description: `Store memories. Requires "action" field.
21
-
22
- Actions:
23
- - action:"encode" — Create a new memory. Params: content (required), type? (semantic|episodic|procedural, default semantic), emotion? (neutral|curiosity|surprise|anxiety|satisfaction|frustration|confusion|excitement|calm|urgency), emotionWeight? (0-1), context? (e.g. "project:acme")
24
- - action:"reconsolidate" — Update an existing memory during recall. Params: id (required), newContext?, currentEmotion?, currentEmotionWeight?`,
20
+ description: `Actions: encode(content) — store memory | encode_batch(memories[]) — store multiple | reconsolidate(id) — update during recall. Optional: type, emotion, emotionWeight, context.`,
25
21
  inputSchema: z.discriminatedUnion("action", [
26
22
  z.object({
27
23
  action: z.literal("encode"),
@@ -31,6 +27,22 @@ Actions:
31
27
  emotionWeight: z.number().min(0).max(1).optional().describe("Emotion intensity 0-1"),
32
28
  context: z.string().optional().describe("Context tag (e.g. project:acme)"),
33
29
  }),
30
+ z.object({
31
+ action: z.literal("encode_batch"),
32
+ memories: z
33
+ .array(
34
+ z.object({
35
+ content: z.string(),
36
+ type: z.nativeEnum(MemoryType).optional(),
37
+ emotion: z.nativeEnum(Emotion).optional(),
38
+ emotionWeight: z.number().min(0).max(1).optional(),
39
+ context: z.string().optional(),
40
+ }),
41
+ )
42
+ .min(1)
43
+ .max(50)
44
+ .describe("Array of memories to encode"),
45
+ }),
34
46
  z.object({
35
47
  action: z.literal("reconsolidate"),
36
48
  id: z.string().describe("Memory ID to update"),
@@ -47,26 +59,30 @@ server.registerTool(
47
59
  "memory_recall",
48
60
  {
49
61
  title: "Recall Memories",
50
- description: `Retrieve memories. Requires "action" field.
51
-
52
- Actions:
53
- - action:"recall" — Cue-based retrieval with spreading activation. Params: cue (required), limit? (default 5), type? (semantic|episodic|procedural), context?, associative? (default true), verbose?
54
- - action:"inspect" — Examine a specific memory's full lifecycle. Params: id (required, full ID or prefix)
55
- - action:"stats" — Memory system overview (counts, working memory usage, last consolidation). No extra params.`,
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.`,
56
63
  inputSchema: z.discriminatedUnion("action", [
57
64
  z.object({
58
- action: z.literal("recall").optional().default("recall"),
65
+ action: z.literal("recall"),
59
66
  cue: z.string().describe("Recall cue"),
60
67
  limit: z.number().optional().describe("Max results (default: 5)"),
61
68
  type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),
62
69
  context: z.string().optional().describe("Filter by context"),
63
70
  associative: z.boolean().optional().describe("Spreading activation (default: true)"),
64
71
  verbose: z.boolean().optional().describe("Full fields"),
72
+ format: z.enum(["full", "content", "ids"]).optional().describe("Response format (default: full)"),
65
73
  }),
66
74
  z.object({
67
75
  action: z.literal("inspect"),
68
76
  id: z.string().describe("Memory ID or prefix"),
69
77
  }),
78
+ z.object({
79
+ action: z.literal("list"),
80
+ type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),
81
+ context: z.string().optional().describe("Filter by context prefix"),
82
+ limit: z.number().optional().describe("Max results (default: 20)"),
83
+ offset: z.number().optional().describe("Skip first N results (default: 0)"),
84
+ format: z.enum(["full", "content", "ids"]).optional().describe("Response format (default: full)"),
85
+ }),
70
86
  z.object({
71
87
  action: z.literal("stats"),
72
88
  }),
@@ -79,14 +95,7 @@ server.registerTool(
79
95
  "memory_manage",
80
96
  {
81
97
  title: "Manage Memory",
82
- description: `Memory maintenance. Requires "action" field.
83
-
84
- Actions:
85
- - action:"consolidate" — Run sleep cycle (strengthen, prune, extract patterns, discover links). No extra params.
86
- - action:"focus_push" — Push to working memory buffer. Params: content (required), memoryRef?
87
- - action:"focus_pop" — Remove most recent item from working memory. No extra params.
88
- - action:"focus_get" — View current working memory contents. No extra params.
89
- - action:"focus_clear" — Clear all working memory. No extra params.`,
98
+ description: `Actions: consolidate run sleep cycle | recall_to_focus(cue) — recall and load to working memory | focus_push(content) — push to buffer | focus_pop — pop newest | focus_get — view buffer | focus_clear — empty buffer.`,
90
99
  inputSchema: z.discriminatedUnion("action", [
91
100
  z.object({
92
101
  action: z.literal("consolidate"),
@@ -105,6 +114,13 @@ Actions:
105
114
  z.object({
106
115
  action: z.literal("focus_clear"),
107
116
  }),
117
+ z.object({
118
+ action: z.literal("recall_to_focus"),
119
+ cue: z.string().describe("Recall cue"),
120
+ limit: z.number().optional().describe("Max memories to load (default: 3)"),
121
+ type: z.nativeEnum(MemoryType).optional().describe("Filter by type"),
122
+ context: z.string().optional().describe("Filter by context"),
123
+ }),
108
124
  ]),
109
125
  },
110
126
  async (args) => handleManage(engine, args),
package/src/mcp/tools.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { CognitiveConfig } from "../config/defaults.ts";
2
2
  import type { EngramStorage } from "../storage/sqlite.ts";
3
3
  import type { EngramEngine } from "../core/engine.ts";
4
- import { isValidMemoryType } from "../core/memory.ts";
4
+ import { isValidMemoryType, MemoryType, Emotion } from "../core/memory.ts";
5
5
  import { encode } from "../core/encoder.ts";
6
6
  import { recall } from "../core/recall.ts";
7
7
  import {
@@ -38,6 +38,8 @@ export function handleStore(
38
38
  switch (args.action) {
39
39
  case "encode":
40
40
  return handleEncode(engine.storage, engine.config, args as any, engine.projectContext);
41
+ case "encode_batch":
42
+ return handleEncodeBatch(engine.storage, engine.config, args as any, engine.projectContext);
41
43
  case "reconsolidate":
42
44
  return handleReconsolidate(engine.storage, engine.config, args as any);
43
45
  default:
@@ -55,6 +57,8 @@ export function handleRecall(
55
57
  return handleRecallQuery(engine.storage, engine.config, args as any, engine.projectContext);
56
58
  case "inspect":
57
59
  return handleInspect(engine.storage, args as any);
60
+ case "list":
61
+ return handleList(engine.storage, args as any, engine.projectContext);
58
62
  case "stats":
59
63
  return handleStats(engine.storage, engine.config);
60
64
  default:
@@ -77,6 +81,8 @@ export function handleManage(
77
81
  return handleFocusGet(engine.storage, engine.config);
78
82
  case "focus_clear":
79
83
  return handleFocusClear(engine.storage);
84
+ case "recall_to_focus":
85
+ return handleRecallToFocus(engine.storage, engine.config, args as any, engine.projectContext);
80
86
  default:
81
87
  return errorResult(`Unknown manage action: ${args.action}`);
82
88
  }
@@ -96,12 +102,12 @@ function handleEncode(
96
102
  ): ToolResult {
97
103
  const typeStr = args.type ?? "semantic";
98
104
  if (!isValidMemoryType(typeStr)) {
99
- return errorResult(`Invalid type: ${typeStr}. Must be episodic, semantic, or procedural.`);
105
+ return errorResult(`Invalid type '${typeStr}'. Valid: ${Object.values(MemoryType).join(", ")}`);
100
106
  }
101
107
 
102
108
  const emotionStr = args.emotion ?? "neutral";
103
109
  if (!isValidEmotion(emotionStr)) {
104
- return errorResult(`Invalid emotion: ${emotionStr}.`);
110
+ return errorResult(`Invalid emotion '${emotionStr}'. Valid: ${Object.values(Emotion).join(", ")}`);
105
111
  }
106
112
 
107
113
  const memory = encode(
@@ -116,17 +122,58 @@ function handleEncode(
116
122
  config,
117
123
  );
118
124
 
119
- return textResult(
120
- JSON.stringify({
121
- id: memory.id,
122
- type: memory.type,
123
- content: memory.content,
124
- activation: memory.activation,
125
- emotion: memory.emotion,
126
- emotionWeight: memory.emotionWeight,
127
- context: memory.context,
128
- }),
129
- );
125
+ return textResult(JSON.stringify({ id: memory.id }));
126
+ }
127
+
128
+ function handleEncodeBatch(
129
+ storage: EngramStorage,
130
+ config: CognitiveConfig,
131
+ args: {
132
+ memories: Array<{
133
+ content: string;
134
+ type?: string;
135
+ emotion?: string;
136
+ emotionWeight?: number;
137
+ context?: string;
138
+ }>;
139
+ },
140
+ defaultContext?: string | null,
141
+ ): ToolResult {
142
+ const ids: string[] = [];
143
+ const errors: string[] = [];
144
+
145
+ storage.transaction(() => {
146
+ for (let i = 0; i < args.memories.length; i++) {
147
+ const m = args.memories[i]!;
148
+ const typeStr = m.type ?? "semantic";
149
+ if (!isValidMemoryType(typeStr)) {
150
+ errors.push(`[${i}] Invalid type '${typeStr}'`);
151
+ continue;
152
+ }
153
+ const emotionStr = m.emotion ?? "neutral";
154
+ if (!isValidEmotion(emotionStr)) {
155
+ errors.push(`[${i}] Invalid emotion '${emotionStr}'`);
156
+ continue;
157
+ }
158
+ const memory = encode(
159
+ storage,
160
+ {
161
+ content: m.content,
162
+ type: typeStr,
163
+ emotion: emotionStr,
164
+ emotionWeight: m.emotionWeight,
165
+ context: m.context ?? defaultContext ?? undefined,
166
+ },
167
+ config,
168
+ );
169
+ ids.push(memory.id);
170
+ }
171
+ });
172
+
173
+ if (errors.length > 0) {
174
+ return textResult(JSON.stringify({ stored: ids, errors }));
175
+ }
176
+ return textResult(JSON.stringify({ stored: ids }));
130
177
  }
131
178
 
132
179
  function handleRecallQuery(
@@ -139,6 +186,7 @@ function handleRecallQuery(
139
186
  context?: string;
140
187
  associative?: boolean;
141
188
  verbose?: boolean;
189
+ format?: "full" | "content" | "ids";
142
190
  },
143
191
  defaultContext?: string | null,
144
192
  ): ToolResult {
@@ -155,6 +203,15 @@ function handleRecallQuery(
155
203
  return textResult("No memories found above retrieval threshold.");
156
204
  }
157
205
 
206
+ const format = args.format ?? "full";
207
+
208
+ if (format === "ids") {
209
+ return textResult(JSON.stringify(results.map((r) => r.memory.id)));
210
+ }
211
+ if (format === "content") {
212
+ return textResult(JSON.stringify(results.map((r) => r.memory.content)));
213
+ }
214
+
158
215
  const formatted = args.verbose
159
216
  ? results.map((r) => ({
160
217
  id: r.memory.id,
@@ -178,6 +235,56 @@ function handleRecallQuery(
178
235
  return textResult(JSON.stringify(formatted));
179
236
  }
180
237
 
238
+ function handleList(
239
+ storage: EngramStorage,
240
+ args: {
241
+ type?: string;
242
+ context?: string;
243
+ limit?: number;
244
+ offset?: number;
245
+ format?: "full" | "content" | "ids";
246
+ },
247
+ defaultContext?: string | null,
248
+ ): ToolResult {
249
+ const limit = args.limit ?? 20;
250
+ const offset = args.offset ?? 0;
251
+ const typeFilter = args.type && isValidMemoryType(args.type) ? args.type : undefined;
252
+ const context = args.context ?? defaultContext ?? undefined;
253
+
254
+ let results;
255
+ if (context) {
256
+ results = storage.getMemoriesByContext(context, typeFilter, limit + offset);
257
+ } else {
258
+ results = storage.getAllMemories(typeFilter).slice(0, limit + offset);
259
+ }
260
+ results = results.slice(offset, offset + limit);
261
+
262
+ if (results.length === 0) {
263
+ return textResult("No memories found.");
264
+ }
265
+
266
+ const format = args.format ?? "full";
267
+
268
+ if (format === "ids") {
269
+ return textResult(JSON.stringify(results.map((m) => m.id)));
270
+ }
271
+ if (format === "content") {
272
+ return textResult(JSON.stringify(results.map((m) => m.content)));
273
+ }
274
+
275
+ return textResult(
276
+ JSON.stringify(
277
+ results.map((m) => ({
278
+ id: m.id,
279
+ content: m.content,
280
+ type: m.type,
281
+ context: m.context,
282
+ activation: m.activation,
283
+ })),
284
+ ),
285
+ );
286
+ }
287
+
181
288
  function handleFocusPush(
182
289
  storage: EngramStorage,
183
290
  config: CognitiveConfig,
@@ -222,6 +329,34 @@ function handleFocusClear(storage: EngramStorage): ToolResult {
222
329
  return textResult(`Cleared ${count} items from working memory.`);
223
330
  }
224
331
 
332
+ function handleRecallToFocus(
333
+ storage: EngramStorage,
334
+ config: CognitiveConfig,
335
+ args: { cue: string; limit?: number; type?: string; context?: string },
336
+ defaultContext?: string | null,
337
+ ): ToolResult {
338
+ const typeFilter = args.type && isValidMemoryType(args.type) ? args.type : undefined;
339
+ const limit = args.limit ?? 3;
340
+
341
+ const results = recall(storage, args.cue, config, {
342
+ limit,
343
+ type: typeFilter,
344
+ context: args.context ?? defaultContext ?? undefined,
345
+ associative: true,
346
+ });
347
+
348
+ const loaded: string[] = [];
349
+ for (const r of results) {
350
+ pushFocus(storage, r.memory.content, config, {
351
+ memoryRef: r.memory.id,
352
+ });
353
+ loaded.push(r.memory.id);
354
+ }
355
+
356
+ const { used, capacity } = focusUtilization(storage, config);
357
+ return textResult(JSON.stringify({ loaded, focus: { used, capacity } }));
358
+ }
359
+
225
360
  function handleConsolidate(storage: EngramStorage, config: CognitiveConfig): ToolResult {
226
361
  const result = consolidate(storage, config);
227
362
  const chunks = discoverChunks(storage, config);
@@ -324,9 +459,6 @@ function handleReconsolidate(
324
459
  return textResult(
325
460
  JSON.stringify({
326
461
  id: updated.id,
327
- content: updated.content,
328
- emotion: updated.emotion,
329
- emotionWeight: updated.emotionWeight,
330
462
  context: updated.context,
331
463
  reconsolidationCount: updated.reconsolidationCount,
332
464
  }),