@kinqs/brainrouter-mcp-server 0.3.4 → 0.3.6

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.
Files changed (65) hide show
  1. package/.env.example +121 -71
  2. package/README.md +88 -15
  3. package/dist/__tests__/cognitive-extractor.test.js +112 -0
  4. package/dist/__tests__/crypto.test.js +8 -1
  5. package/dist/__tests__/working-memory.test.js +67 -0
  6. package/dist/env-loader.js +47 -0
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +12 -1
  9. package/dist/init.d.ts +1 -0
  10. package/dist/init.js +64 -0
  11. package/dist/memory/engine.js +21 -1
  12. package/dist/memory/pipeline/cognitive-extractor.js +19 -1
  13. package/dist/memory/recall.d.ts +3 -1
  14. package/dist/memory/recall.js +48 -3
  15. package/dist/memory/store/relevance-judge.d.ts +51 -0
  16. package/dist/memory/store/relevance-judge.js +196 -0
  17. package/dist/memory/working/canvas.js +11 -0
  18. package/package.json +2 -2
  19. package/dist/memory/config.d.ts +0 -2
  20. package/dist/memory/config.js +0 -3
  21. package/dist/memory/pipeline/l1-contradiction.d.ts +0 -7
  22. package/dist/memory/pipeline/l1-contradiction.js +0 -66
  23. package/dist/memory/pipeline/l1-dedup.d.ts +0 -23
  24. package/dist/memory/pipeline/l1-dedup.js +0 -39
  25. package/dist/memory/pipeline/l1-extractor.d.ts +0 -21
  26. package/dist/memory/pipeline/l1-extractor.js +0 -180
  27. package/dist/memory/pipeline/l2-direction-shift.d.ts +0 -10
  28. package/dist/memory/pipeline/l2-direction-shift.js +0 -27
  29. package/dist/memory/pipeline/l2-scene.d.ts +0 -15
  30. package/dist/memory/pipeline/l2-scene.js +0 -140
  31. package/dist/memory/pipeline/l3-distiller.d.ts +0 -15
  32. package/dist/memory/pipeline/l3-distiller.js +0 -40
  33. package/dist/memory/pipeline/task-queue.d.ts +0 -54
  34. package/dist/memory/pipeline/task-queue.js +0 -117
  35. package/dist/memory/prompts/graph-extraction-batch.d.ts +0 -14
  36. package/dist/memory/prompts/graph-extraction-batch.js +0 -54
  37. package/dist/memory/prompts/l1-contradiction-batch.d.ts +0 -16
  38. package/dist/memory/prompts/l1-contradiction-batch.js +0 -47
  39. package/dist/memory/prompts/l1-contradiction.d.ts +0 -1
  40. package/dist/memory/prompts/l1-contradiction.js +0 -25
  41. package/dist/memory/prompts/l1-extraction.d.ts +0 -10
  42. package/dist/memory/prompts/l1-extraction.js +0 -114
  43. package/dist/memory/prompts/l2-direction-shift.d.ts +0 -5
  44. package/dist/memory/prompts/l2-direction-shift.js +0 -32
  45. package/dist/memory/prompts/l2-scene-cluster.d.ts +0 -2
  46. package/dist/memory/prompts/l2-scene-cluster.js +0 -33
  47. package/dist/memory/prompts/l2-scene.d.ts +0 -7
  48. package/dist/memory/prompts/l2-scene.js +0 -40
  49. package/dist/memory/prompts/l3-persona.d.ts +0 -6
  50. package/dist/memory/prompts/l3-persona.js +0 -60
  51. package/dist/memory/store/types.d.ts +0 -101
  52. package/dist/memory/types.d.ts +0 -207
  53. package/dist/memory/types.js +0 -7
  54. package/dist/memory/validation.d.ts +0 -441
  55. package/dist/memory/validation.js +0 -129
  56. package/dist/tools/agent_memory_tools.d.ts +0 -485
  57. package/dist/tools/agent_memory_tools.js +0 -793
  58. package/dist/tools/get_doc.d.ts +0 -21
  59. package/dist/tools/get_doc.js +0 -24
  60. package/dist/tools/list_docs.d.ts +0 -15
  61. package/dist/tools/list_docs.js +0 -16
  62. package/dist/tools/update_doc.d.ts +0 -24
  63. package/dist/tools/update_doc.js +0 -35
  64. /package/dist/__tests__/{agent_mode.test.d.ts → cognitive-extractor.test.d.ts} +0 -0
  65. /package/dist/{memory/store/types.js → env-loader.d.ts} +0 -0
@@ -1,793 +0,0 @@
1
- import { z } from "zod";
2
- import crypto from "node:crypto";
3
- import { memoryEngine } from "../memory/engine.js";
4
- import { L1CommitPayloadSchema, ContradictionDecisionCommitSchema, GraphCommitSchema, L2CommitSchema, L3CommitSchema, normalizeSceneName, normalizeSkillTag, normalizeMemoryContent, normalizeEntityName, validateL0RecordOwnership, validateL1RecordOwnership, } from "../memory/validation.js";
5
- import { EXTRACT_MEMORIES_SYSTEM_PROMPT, formatExtractionPrompt } from "../memory/prompts/l1-extraction.js";
6
- import { L1_CONTRADICTION_PROMPT } from "../memory/prompts/l1-contradiction.js";
7
- import { GRAPH_EXTRACTION_SYSTEM_PROMPT } from "../memory/prompts/graph-extraction.js";
8
- import { L2_SCENE_SYSTEM_PROMPT } from "../memory/prompts/l2-scene.js";
9
- import { L3_PERSONA_SYSTEM_PROMPT, formatL3PersonaPrompt } from "../memory/prompts/l3-persona.js";
10
- import { deduplicateMemories } from "../memory/pipeline/l1-dedup.js";
11
- import { L2_MAX_SCENES } from "../memory/scheduler.js";
12
- // ============================
13
- // Schemas for MCP Tool Registration
14
- // ============================
15
- export const memoryGetExtractionWorkSchema = {
16
- name: "memory_get_extraction_work",
17
- description: "Get pending conversational L0 messages that need to be distilled into L1 memories.",
18
- inputSchema: {
19
- type: "object",
20
- properties: {
21
- userId: { type: "string", description: "The ID of the user." },
22
- sessionKey: { type: "string", description: "The session key." },
23
- limit: { type: "number", description: "Maximum number of L0 messages to retrieve.", default: 10 },
24
- activeSkill: { type: "string", description: "The currently active skill." }
25
- },
26
- required: ["userId", "sessionKey"]
27
- }
28
- };
29
- export const memoryCommitExtractedL1Schema = {
30
- name: "memory_commit_extracted_l1",
31
- description: "Commit newly extracted L1 memories for a session, automatically generating vector embeddings and running deduplication.",
32
- inputSchema: {
33
- type: "object",
34
- properties: {
35
- userId: { type: "string" },
36
- sessionKey: { type: "string" },
37
- sessionId: { type: "string", default: "" },
38
- sourceL0Ids: { type: "array", items: { type: "string" } },
39
- scenes: {
40
- type: "array",
41
- items: {
42
- type: "object",
43
- properties: {
44
- sceneName: { type: "string" },
45
- memories: {
46
- type: "array",
47
- items: {
48
- type: "object",
49
- properties: {
50
- content: { type: "string" },
51
- type: { type: "string", enum: ["persona", "episodic", "instruction", "skill_context"] },
52
- priority: { type: "number", minimum: 0, maximum: 100, default: 50 },
53
- skillTag: { type: "string", default: "" },
54
- metadata: { type: "object", default: {} }
55
- },
56
- required: ["content", "type"]
57
- }
58
- }
59
- },
60
- required: ["sceneName", "memories"]
61
- }
62
- }
63
- },
64
- required: ["userId", "sessionKey", "scenes"]
65
- }
66
- };
67
- export const memoryGetContradictionWorkSchema = {
68
- name: "memory_get_contradiction_work",
69
- description: "Get pending L1 memories that need to be evaluated for semantic contradictions or temporal updates against existing memories.",
70
- inputSchema: {
71
- type: "object",
72
- properties: {
73
- userId: { type: "string" },
74
- limit: { type: "number", default: 20 }
75
- },
76
- required: ["userId"]
77
- }
78
- };
79
- export const memoryCommitContradictionDecisionsSchema = {
80
- name: "memory_commit_contradiction_decisions",
81
- description: "Commit decisions on contradiction candidates, invalidating/superseding old records or logging genuine conflicts.",
82
- inputSchema: {
83
- type: "object",
84
- properties: {
85
- userId: { type: "string" },
86
- decisions: {
87
- type: "array",
88
- items: {
89
- type: "object",
90
- properties: {
91
- newRecordId: { type: "string" },
92
- existingRecordId: { type: "string" },
93
- decision: { type: "string", enum: ["no_conflict", "temporal_update", "genuine_conflict"] },
94
- reason: { type: "string" },
95
- confidence: { type: "number", minimum: 0, maximum: 1, default: 1.0 }
96
- },
97
- required: ["newRecordId", "existingRecordId", "decision"]
98
- }
99
- }
100
- },
101
- required: ["userId", "decisions"]
102
- }
103
- };
104
- export const memoryGetGraphWorkSchema = {
105
- name: "memory_get_graph_work",
106
- description: "Get pending L1 memories that have not yet had GraphRAG entities and relationships extracted from them.",
107
- inputSchema: {
108
- type: "object",
109
- properties: {
110
- userId: { type: "string" },
111
- limit: { type: "number", default: 20 }
112
- },
113
- required: ["userId"]
114
- }
115
- };
116
- export const memoryCommitGraphSchema = {
117
- name: "memory_commit_graph",
118
- description: "Commit entities and relations extracted from an L1 memory, updating the GraphRAG knowledge graph.",
119
- inputSchema: {
120
- type: "object",
121
- properties: {
122
- userId: { type: "string" },
123
- sourceRecordId: { type: "string" },
124
- entities: {
125
- type: "array",
126
- items: {
127
- type: "object",
128
- properties: {
129
- entity: { type: "string" },
130
- type: { type: "string", default: "concept" },
131
- confidence: { type: "number", minimum: 0, maximum: 1, default: 1.0 },
132
- skillTag: { type: "string", default: "" },
133
- sourceRecordId: { type: "string" }
134
- },
135
- required: ["entity"]
136
- }
137
- },
138
- relations: {
139
- type: "array",
140
- items: {
141
- type: "object",
142
- properties: {
143
- from: { type: "string" },
144
- to: { type: "string" },
145
- relation: { type: "string", default: "relates_to" },
146
- confidence: { type: "number", minimum: 0, maximum: 1, default: 1.0 },
147
- skillTag: { type: "string", default: "" },
148
- sourceRecordId: { type: "string" }
149
- },
150
- required: ["from", "to"]
151
- }
152
- }
153
- },
154
- required: ["userId"]
155
- }
156
- };
157
- export const memoryGetL2WorkSchema = {
158
- name: "memory_get_l2_work",
159
- description: "Get L1 memories grouped by scenes to perform scene narrative summary distillation or cold scene merges.",
160
- inputSchema: {
161
- type: "object",
162
- properties: {
163
- userId: { type: "string" }
164
- },
165
- required: ["userId"]
166
- }
167
- };
168
- export const memoryCommitL2Schema = {
169
- name: "memory_commit_l2",
170
- description: "Commit scene summaries, scene name renames (clustering), or cold scene merge distillations.",
171
- inputSchema: {
172
- type: "object",
173
- properties: {
174
- userId: { type: "string" },
175
- renames: {
176
- type: "array",
177
- items: {
178
- type: "object",
179
- properties: {
180
- oldName: { type: "string" },
181
- newName: { type: "string" }
182
- },
183
- required: ["oldName", "newName"]
184
- }
185
- },
186
- scenes: {
187
- type: "array",
188
- items: {
189
- type: "object",
190
- properties: {
191
- sceneName: { type: "string" },
192
- summaryMd: { type: "string" },
193
- heatScore: { type: "number", minimum: 0, maximum: 100 }
194
- },
195
- required: ["sceneName", "summaryMd"]
196
- }
197
- },
198
- merges: {
199
- type: "array",
200
- items: {
201
- type: "object",
202
- properties: {
203
- sceneIds: { type: "array", items: { type: "string" } },
204
- mergedSummaryMd: { type: "string" }
205
- },
206
- required: ["sceneIds", "mergedSummaryMd"]
207
- }
208
- }
209
- },
210
- required: ["userId"]
211
- }
212
- };
213
- export const memoryGetL3WorkSchema = {
214
- name: "memory_get_l3_work",
215
- description: "Get L1 persona and instruction memories across all sessions to compile a synthesized L3 Narrative Profile.",
216
- inputSchema: {
217
- type: "object",
218
- properties: {
219
- userId: { type: "string" }
220
- },
221
- required: ["userId"]
222
- }
223
- };
224
- export const memoryCommitL3Schema = {
225
- name: "memory_commit_l3",
226
- description: "Commit a synthesized L3 Narrative Profile summary, invalidating persona caches.",
227
- inputSchema: {
228
- type: "object",
229
- properties: {
230
- userId: { type: "string" },
231
- personaMd: { type: "string" }
232
- },
233
- required: ["userId", "personaMd"]
234
- }
235
- };
236
- // ============================
237
- // Tool Dispatcher Handlers
238
- // ============================
239
- export async function handleMemoryGetExtractionWork(args) {
240
- const params = z.object({
241
- userId: z.string(),
242
- sessionKey: z.string(),
243
- limit: z.number().optional().default(10),
244
- activeSkill: z.string().optional(),
245
- }).parse(args);
246
- const store = memoryEngine.store;
247
- try {
248
- const unextractedCount = store.getUnextractedL0Count(params.userId, params.sessionKey);
249
- if (unextractedCount === 0) {
250
- return {
251
- content: [{
252
- type: "text",
253
- text: JSON.stringify({ status: "no_work", message: "No unextracted L0 messages in this session." }, null, 2)
254
- }]
255
- };
256
- }
257
- const messages = store.getRecentL0Messages(params.userId, params.sessionKey, params.limit, "", true);
258
- const existingSceneNames = store.getTopL2Scenes(params.userId, 20).map((s) => s.sceneName);
259
- const skillHints = params.activeSkill ? (store.getSkillHints(params.activeSkill) ?? undefined) : undefined;
260
- // Build the instruction templates
261
- const qualifiedMessages = messages.filter((m) => {
262
- const clean = m.messageText.trim();
263
- return clean.length >= 3 && !/^[^a-zA-Z\u4e00-\u9fa5]+$/.test(clean);
264
- });
265
- const userPrompt = formatExtractionPrompt({
266
- newMessages: qualifiedMessages,
267
- backgroundMessages: [],
268
- previousSceneName: "None",
269
- existingSceneNames,
270
- activeSkill: params.activeSkill,
271
- skillHints
272
- });
273
- return {
274
- content: [{
275
- type: "text",
276
- text: JSON.stringify({
277
- status: "work_available",
278
- userId: params.userId,
279
- sessionKey: params.sessionKey,
280
- messages,
281
- unextractedCount,
282
- remainingUnextractedCount: Math.max(0, unextractedCount - messages.length),
283
- existingSceneNames,
284
- skillHints,
285
- prompts: {
286
- systemPrompt: EXTRACT_MEMORIES_SYSTEM_PROMPT,
287
- userPrompt
288
- }
289
- }, null, 2)
290
- }]
291
- };
292
- }
293
- catch (err) {
294
- return {
295
- isError: true,
296
- content: [{ type: "text", text: `Failed to retrieve extraction work: ${err.message}` }]
297
- };
298
- }
299
- }
300
- export async function handleMemoryCommitExtractedL1(args) {
301
- const payload = L1CommitPayloadSchema.parse(args);
302
- const store = memoryEngine.store;
303
- const embeddingService = memoryEngine.capturePipeline.embeddingService;
304
- try {
305
- if (payload.sourceL0Ids.length > 0) {
306
- validateL0RecordOwnership(store, payload.userId, payload.sourceL0Ids);
307
- }
308
- const records = [];
309
- const now = new Date().toISOString();
310
- for (const group of payload.scenes) {
311
- const sceneName = normalizeSceneName(group.sceneName);
312
- for (const mem of group.memories) {
313
- records.push({
314
- id: `l1_${payload.sessionKey}_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`,
315
- userId: payload.userId,
316
- sessionKey: payload.sessionKey,
317
- sessionId: payload.sessionId,
318
- content: normalizeMemoryContent(mem.content),
319
- type: mem.type,
320
- priority: mem.priority,
321
- sceneName,
322
- skillTag: normalizeSkillTag(mem.skillTag),
323
- halfLifeDays: mem.type === "instruction" ? null : (mem.type === "persona" ? 180 : (mem.type === "skill_context" ? 7 : 30)),
324
- supersededBy: null,
325
- timestampStr: "",
326
- timestampStart: "",
327
- timestampEnd: "",
328
- createdTime: now,
329
- updatedTime: now,
330
- metadata: mem.metadata,
331
- citationCount: 0,
332
- lastCitedAt: null,
333
- neverCitedCount: 0,
334
- archived: false,
335
- });
336
- }
337
- }
338
- // Deduplication check
339
- const { uniqueRecords, droppedCount } = await deduplicateMemories({
340
- records,
341
- store,
342
- userId: payload.userId
343
- });
344
- const entries = [];
345
- for (const record of uniqueRecords) {
346
- let embedding;
347
- if (embeddingService.isReady()) {
348
- try {
349
- embedding = await embeddingService.embed(record.content);
350
- }
351
- catch (err) {
352
- console.error(`[BrainRouter] Embedding failed during commit for ${record.id}:`, err.message);
353
- }
354
- }
355
- entries.push({ record, embedding });
356
- }
357
- if (entries.length > 0) {
358
- store.upsertL1Batch(entries);
359
- store.incrementSchedulerL1Count(payload.userId, entries.length);
360
- }
361
- if (payload.sourceL0Ids.length > 0) {
362
- store.markL0ExtractedByIds(payload.userId, payload.sourceL0Ids);
363
- }
364
- return {
365
- content: [{
366
- type: "text",
367
- text: JSON.stringify({
368
- committedCount: entries.length,
369
- committedIds: uniqueRecords.map(r => r.id),
370
- droppedDuplicateCount: droppedCount,
371
- validationErrors: []
372
- }, null, 2)
373
- }]
374
- };
375
- }
376
- catch (err) {
377
- return {
378
- isError: true,
379
- content: [{ type: "text", text: `Failed to commit L1 memories: ${err.message}` }]
380
- };
381
- }
382
- }
383
- export async function handleMemoryGetContradictionWork(args) {
384
- const params = z.object({
385
- userId: z.string(),
386
- limit: z.number().optional().default(20),
387
- }).parse(args);
388
- const store = memoryEngine.store;
389
- try {
390
- const pendingL1s = store.getPendingContradictionL1s(params.userId, params.limit);
391
- if (pendingL1s.length === 0) {
392
- return {
393
- content: [{
394
- type: "text",
395
- text: JSON.stringify({ status: "no_work", message: "No pending contradiction checks." }, null, 2)
396
- }]
397
- };
398
- }
399
- const pairs = [];
400
- for (const newRecord of pendingL1s) {
401
- const candidates = store.searchL1Fts(params.userId, newRecord.content, 5);
402
- for (const cand of candidates) {
403
- if (cand.record_id === newRecord.id)
404
- continue;
405
- pairs.push({
406
- newRecord: {
407
- id: newRecord.id,
408
- content: newRecord.content,
409
- type: newRecord.type
410
- },
411
- existingRecord: {
412
- id: cand.record_id,
413
- content: cand.content,
414
- type: cand.type
415
- }
416
- });
417
- }
418
- }
419
- return {
420
- content: [{
421
- type: "text",
422
- text: JSON.stringify({
423
- status: pairs.length > 0 ? "work_available" : "no_work",
424
- userId: params.userId,
425
- pairs,
426
- prompts: {
427
- systemPrompt: L1_CONTRADICTION_PROMPT
428
- }
429
- }, null, 2)
430
- }]
431
- };
432
- }
433
- catch (err) {
434
- return {
435
- isError: true,
436
- content: [{ type: "text", text: `Failed to retrieve contradiction work: ${err.message}` }]
437
- };
438
- }
439
- }
440
- export async function handleMemoryCommitContradictionDecisions(args) {
441
- const payload = ContradictionDecisionCommitSchema.parse(args);
442
- const store = memoryEngine.store;
443
- try {
444
- const allIds = payload.decisions.flatMap(d => [d.newRecordId, d.existingRecordId]);
445
- validateL1RecordOwnership(store, payload.userId, [...new Set(allIds)]);
446
- let processedCount = 0;
447
- const processedIds = [];
448
- for (const decision of payload.decisions) {
449
- const newRec = store.getL1Record(payload.userId, decision.newRecordId);
450
- if (!newRec)
451
- continue;
452
- if (decision.decision === "temporal_update") {
453
- store.invalidateL1Record(payload.userId, decision.existingRecordId, decision.newRecordId, newRec.createdTime);
454
- processedCount++;
455
- processedIds.push(decision.newRecordId);
456
- }
457
- else if (decision.decision === "genuine_conflict") {
458
- store.upsertContradiction({
459
- id: `conflict_${crypto.randomBytes(4).toString("hex")}`,
460
- userId: payload.userId,
461
- recordIdA: decision.existingRecordId,
462
- recordIdB: decision.newRecordId,
463
- reason: decision.reason || "Genuine contradiction",
464
- confidence: decision.confidence
465
- });
466
- processedCount++;
467
- processedIds.push(decision.newRecordId);
468
- }
469
- else {
470
- // no_conflict
471
- processedCount++;
472
- processedIds.push(decision.newRecordId);
473
- }
474
- }
475
- const uniqueNewIds = [...new Set(payload.decisions.map(d => d.newRecordId))];
476
- store.markL1ContradictionChecked(payload.userId, uniqueNewIds);
477
- return {
478
- content: [{
479
- type: "text",
480
- text: JSON.stringify({
481
- committedCount: processedCount,
482
- committedIds: processedIds,
483
- droppedDuplicateCount: 0,
484
- validationErrors: []
485
- }, null, 2)
486
- }]
487
- };
488
- }
489
- catch (err) {
490
- return {
491
- isError: true,
492
- content: [{ type: "text", text: `Failed to commit contradiction decisions: ${err.message}` }]
493
- };
494
- }
495
- }
496
- export async function handleMemoryGetGraphWork(args) {
497
- const params = z.object({
498
- userId: z.string(),
499
- limit: z.number().optional().default(20),
500
- }).parse(args);
501
- const store = memoryEngine.store;
502
- try {
503
- const pendingL1s = store.getPendingGraphL1s(params.userId, params.limit);
504
- if (pendingL1s.length === 0) {
505
- return {
506
- content: [{
507
- type: "text",
508
- text: JSON.stringify({ status: "no_work", message: "No pending graph extractions." }, null, 2)
509
- }]
510
- };
511
- }
512
- return {
513
- content: [{
514
- type: "text",
515
- text: JSON.stringify({
516
- status: "work_available",
517
- userId: params.userId,
518
- records: pendingL1s.map((r) => ({
519
- id: r.id,
520
- content: r.content,
521
- skillTag: r.skillTag
522
- })),
523
- prompts: {
524
- systemPrompt: GRAPH_EXTRACTION_SYSTEM_PROMPT
525
- }
526
- }, null, 2)
527
- }]
528
- };
529
- }
530
- catch (err) {
531
- return {
532
- isError: true,
533
- content: [{ type: "text", text: `Failed to retrieve graph work: ${err.message}` }]
534
- };
535
- }
536
- }
537
- export async function handleMemoryCommitGraph(args) {
538
- const payload = GraphCommitSchema.parse(args);
539
- const store = memoryEngine.store;
540
- try {
541
- if (payload.sourceRecordId) {
542
- validateL1RecordOwnership(store, payload.userId, [payload.sourceRecordId]);
543
- }
544
- const entityMap = new Map();
545
- const committedIds = [];
546
- // 1. Upsert Nodes
547
- for (const ent of payload.entities) {
548
- const entityName = normalizeEntityName(ent.entity);
549
- const existing = store.getGraphNodeByEntity(payload.userId, entityName);
550
- const nodeId = existing?.id ?? `gn_${crypto.randomBytes(6).toString("hex")}`;
551
- entityMap.set(entityName.toLowerCase(), nodeId);
552
- const node = {
553
- id: nodeId,
554
- userId: payload.userId,
555
- entity: ent.entity,
556
- entityType: ent.type || "concept",
557
- skillTag: ent.skillTag || "",
558
- confidence: ent.confidence || 1.0,
559
- sourceRecordId: ent.sourceRecordId || payload.sourceRecordId || "",
560
- createdTime: new Date().toISOString()
561
- };
562
- store.upsertGraphNode(node);
563
- committedIds.push(nodeId);
564
- }
565
- // 2. Upsert Edges
566
- for (const rel of payload.relations) {
567
- const fromName = normalizeEntityName(rel.from);
568
- const toName = normalizeEntityName(rel.to);
569
- let fromNodeId = entityMap.get(fromName.toLowerCase());
570
- if (!fromNodeId) {
571
- fromNodeId = store.getGraphNodeByEntity(payload.userId, fromName)?.id;
572
- }
573
- let toNodeId = entityMap.get(toName.toLowerCase());
574
- if (!toNodeId) {
575
- toNodeId = store.getGraphNodeByEntity(payload.userId, toName)?.id;
576
- }
577
- if (!fromNodeId || !toNodeId)
578
- continue;
579
- const edgeId = `ge_${crypto.randomBytes(6).toString("hex")}`;
580
- const edge = {
581
- id: edgeId,
582
- userId: payload.userId,
583
- fromNodeId,
584
- toNodeId,
585
- relation: rel.relation || "relates_to",
586
- skillTag: rel.skillTag || "",
587
- confidence: rel.confidence || 1.0,
588
- sourceRecordId: rel.sourceRecordId || payload.sourceRecordId || "",
589
- createdTime: new Date().toISOString()
590
- };
591
- store.upsertGraphEdge(edge);
592
- committedIds.push(edgeId);
593
- }
594
- if (payload.sourceRecordId) {
595
- store.markL1GraphExtracted(payload.userId, [payload.sourceRecordId]);
596
- }
597
- return {
598
- content: [{
599
- type: "text",
600
- text: JSON.stringify({
601
- committedCount: committedIds.length,
602
- committedIds,
603
- droppedDuplicateCount: 0,
604
- validationErrors: []
605
- }, null, 2)
606
- }]
607
- };
608
- }
609
- catch (err) {
610
- return {
611
- isError: true,
612
- content: [{ type: "text", text: `Failed to commit graph elements: ${err.message}` }]
613
- };
614
- }
615
- }
616
- export async function handleMemoryGetL2Work(args) {
617
- const params = z.object({
618
- userId: z.string(),
619
- }).parse(args);
620
- const store = memoryEngine.store;
621
- try {
622
- const countState = store.getSchedulerState(params.userId);
623
- const sceneNames = store.getDistinctSceneNames(params.userId);
624
- const topL2Scenes = store.getTopL2Scenes(params.userId, 50);
625
- const scenes = [];
626
- for (const name of sceneNames) {
627
- const l1s = store.getL1sByScene(params.userId, name, 30);
628
- if (l1s.length > 0) {
629
- scenes.push({ sceneName: name, memories: l1s });
630
- }
631
- }
632
- const sceneCount = store.getL2SceneCount(params.userId);
633
- const overflow = sceneCount - L2_MAX_SCENES + 1;
634
- const coldScenes = overflow > 0 ? store.getColdL2Scenes(params.userId, overflow + 3) : [];
635
- return {
636
- content: [{
637
- type: "text",
638
- text: JSON.stringify({
639
- status: scenes.length > 0 ? "work_available" : "no_work",
640
- userId: params.userId,
641
- schedulerState: countState,
642
- scenes,
643
- existingL2Scenes: topL2Scenes,
644
- coldScenesToMerge: coldScenes,
645
- prompts: {
646
- systemPrompt: L2_SCENE_SYSTEM_PROMPT
647
- }
648
- }, null, 2)
649
- }]
650
- };
651
- }
652
- catch (err) {
653
- return {
654
- isError: true,
655
- content: [{ type: "text", text: `Failed to retrieve L2 work: ${err.message}` }]
656
- };
657
- }
658
- }
659
- export async function handleMemoryCommitL2(args) {
660
- const payload = L2CommitSchema.parse(args);
661
- const store = memoryEngine.store;
662
- try {
663
- const committedIds = [];
664
- const now = new Date().toISOString();
665
- // 1. Renames (clustering canonicalization)
666
- for (const r of payload.renames) {
667
- store.renameSceneInL1Records(payload.userId, r.oldName, r.newName);
668
- }
669
- // 2. Main distilled scenes
670
- for (const scene of payload.scenes) {
671
- const existing = store.getL2SceneByName(payload.userId, scene.sceneName);
672
- const record = {
673
- id: existing?.id ?? `l2_${crypto.randomBytes(6).toString("hex")}`,
674
- userId: payload.userId,
675
- sceneName: scene.sceneName,
676
- summaryMd: scene.summaryMd,
677
- heatScore: scene.heatScore !== undefined ? scene.heatScore : (existing ? Math.min(100, existing.heatScore + 30) : 100),
678
- lastActiveTime: now,
679
- createdTime: existing?.createdTime ?? now,
680
- updatedTime: now
681
- };
682
- store.upsertL2Scene(record);
683
- committedIds.push(record.id);
684
- }
685
- // 3. Merges into [Archived]
686
- for (const merge of payload.merges) {
687
- const existingArchive = store.getL2SceneByName(payload.userId, "[Archived]");
688
- const record = {
689
- id: existingArchive?.id ?? `l2_${crypto.randomBytes(6).toString("hex")}`,
690
- userId: payload.userId,
691
- sceneName: "[Archived]",
692
- summaryMd: merge.mergedSummaryMd,
693
- heatScore: 10,
694
- lastActiveTime: now,
695
- createdTime: existingArchive?.createdTime ?? now,
696
- updatedTime: now
697
- };
698
- store.upsertL2Scene(record);
699
- store.deleteL2Scenes(payload.userId, merge.sceneIds);
700
- committedIds.push(record.id);
701
- }
702
- store.resetSchedulerL2Count(payload.userId);
703
- store.decayL2HeatScores(payload.userId);
704
- return {
705
- content: [{
706
- type: "text",
707
- text: JSON.stringify({
708
- committedCount: committedIds.length,
709
- committedIds,
710
- droppedDuplicateCount: 0,
711
- validationErrors: []
712
- }, null, 2)
713
- }]
714
- };
715
- }
716
- catch (err) {
717
- return {
718
- isError: true,
719
- content: [{ type: "text", text: `Failed to commit L2 scenes: ${err.message}` }]
720
- };
721
- }
722
- }
723
- export async function handleMemoryGetL3Work(args) {
724
- const params = z.object({
725
- userId: z.string(),
726
- }).parse(args);
727
- const store = memoryEngine.store;
728
- try {
729
- const memories = store.getPersonaAndInstructionL1s(params.userId, 100);
730
- const existing = store.getL3Persona(params.userId);
731
- return {
732
- content: [{
733
- type: "text",
734
- text: JSON.stringify({
735
- status: memories.length > 0 ? "work_available" : "no_work",
736
- userId: params.userId,
737
- memories,
738
- existingPersona: existing,
739
- prompts: {
740
- systemPrompt: L3_PERSONA_SYSTEM_PROMPT,
741
- userPrompt: formatL3PersonaPrompt(memories)
742
- }
743
- }, null, 2)
744
- }]
745
- };
746
- }
747
- catch (err) {
748
- return {
749
- isError: true,
750
- content: [{ type: "text", text: `Failed to retrieve L3 work: ${err.message}` }]
751
- };
752
- }
753
- }
754
- export async function handleMemoryCommitL3(args) {
755
- const payload = L3CommitSchema.parse(args);
756
- const store = memoryEngine.store;
757
- try {
758
- const memories = store.getPersonaAndInstructionL1s(payload.userId, 100);
759
- const existing = store.getL3Persona(payload.userId);
760
- const now = new Date().toISOString();
761
- const record = {
762
- userId: payload.userId,
763
- personaMd: payload.personaMd,
764
- l1CountAtGeneration: memories.length,
765
- createdTime: existing?.createdTime ?? now,
766
- updatedTime: now
767
- };
768
- store.upsertL3Persona(record);
769
- // Invalidate local in-memory cache
770
- const cache = memoryEngine.personaCache;
771
- if (cache) {
772
- cache.set(payload.userId, { personaMd: payload.personaMd, cachedAt: Date.now() });
773
- }
774
- store.resetSchedulerL3Count(payload.userId);
775
- return {
776
- content: [{
777
- type: "text",
778
- text: JSON.stringify({
779
- committedCount: 1,
780
- committedIds: [payload.userId],
781
- droppedDuplicateCount: 0,
782
- validationErrors: []
783
- }, null, 2)
784
- }]
785
- };
786
- }
787
- catch (err) {
788
- return {
789
- isError: true,
790
- content: [{ type: "text", text: `Failed to commit L3 persona: ${err.message}` }]
791
- };
792
- }
793
- }