clay-server 2.26.0-beta.4 → 2.26.0-beta.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.
@@ -0,0 +1,677 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+
4
+ function attachMemory(ctx) {
5
+ var cwd = ctx.cwd;
6
+ var sm = ctx.sm;
7
+ var sdk = ctx.sdk;
8
+ var sendTo = ctx.sendTo;
9
+ var matesModule = ctx.matesModule;
10
+ var sessionSearch = ctx.sessionSearch;
11
+ var getAllProjectSessions = ctx.getAllProjectSessions;
12
+ var projectOwnerId = ctx.projectOwnerId;
13
+ var handleMessage = ctx.handleMessage;
14
+
15
+ function formatRawDigests(rawLines, headerLabel) {
16
+ if (!rawLines || rawLines.length === 0) return "";
17
+ var lines = ["\n\n" + (headerLabel || "Your recent session memories:")];
18
+ for (var i = 0; i < rawLines.length; i++) {
19
+ try {
20
+ var d = JSON.parse(rawLines[i]);
21
+ if (d.type === "debate" && d.my_role) {
22
+ // Debate memories are role-played positions, not genuine opinions
23
+ lines.push("- [" + (d.date || "?") + "] DEBATE (role: " + d.my_role + ") " + (d.topic || "unknown") +
24
+ ": argued " + (d.my_position || "N/A") + " (assigned role, not my actual opinion)" +
25
+ (d.outcome ? " | Outcome: " + d.outcome : "") +
26
+ (d.open_items ? " | Open: " + d.open_items : ""));
27
+ } else {
28
+ lines.push("- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
29
+ (d.decisions ? " | Decisions: " + d.decisions : "") +
30
+ (d.open_items ? " | Open: " + d.open_items : ""));
31
+ }
32
+ } catch (e) {}
33
+ }
34
+ return lines.join("\n");
35
+ }
36
+
37
+ function loadMateDigests(mateCtx, mateId, query) {
38
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
39
+ var knowledgeDir = path.join(mateDir, "knowledge");
40
+ var mate = matesModule.getMate(mateCtx, mateId);
41
+ var hasGlobalSearch = mate && mate.globalSearch;
42
+
43
+ // Load shared user profile (available to ALL mates)
44
+ var userProfileResult = "";
45
+ try {
46
+ var matesRoot = matesModule.resolveMatesRoot(mateCtx);
47
+ var userProfilePath = path.join(matesRoot, "user-profile.md");
48
+ if (fs.existsSync(userProfilePath)) {
49
+ var profileContent = fs.readFileSync(userProfilePath, "utf8").trim();
50
+ if (profileContent && profileContent.length > 50) {
51
+ userProfileResult = "\n\n" + profileContent;
52
+ }
53
+ }
54
+ } catch (e) {}
55
+
56
+ // Check for memory-summary.md first
57
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
58
+ var hasSummary = false;
59
+ var summaryContent = "";
60
+ try {
61
+ if (fs.existsSync(summaryFile)) {
62
+ summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
63
+ if (summaryContent) hasSummary = true;
64
+ }
65
+ } catch (e) {}
66
+
67
+ // Load raw digests
68
+ var allLines = [];
69
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
70
+ try {
71
+ if (fs.existsSync(digestFile)) {
72
+ allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
73
+ }
74
+ } catch (e) {}
75
+
76
+ var result = userProfileResult;
77
+
78
+ if (hasSummary) {
79
+ // Load summary + latest 5 raw digests for richer context
80
+ var recent = allLines.slice(-5);
81
+ result = "\n\nYour memory summary:\n" + summaryContent;
82
+ if (recent.length > 0) {
83
+ result += formatRawDigests(recent, "Latest raw session memories:");
84
+ }
85
+ } else {
86
+ // Backward compatible: latest 8 raw digests
87
+ var recent = allLines.slice(-8);
88
+ result = formatRawDigests(recent, "Your recent session memories:");
89
+ }
90
+
91
+ // Global search: always load team memory summaries for globalSearch mates
92
+ var otherDigests = [];
93
+ if (hasGlobalSearch) {
94
+ try {
95
+ var allMates = matesModule.getAllMates(mateCtx);
96
+ var teamSummaries = [];
97
+ for (var mi = 0; mi < allMates.length; mi++) {
98
+ if (allMates[mi].id === mateId) continue;
99
+ var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
100
+ var mateName = allMates[mi].name || allMates[mi].id;
101
+
102
+ // Collect digest files for BM25 search
103
+ var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
104
+ if (fs.existsSync(otherDigest)) {
105
+ otherDigests.push({ path: otherDigest, mateName: mateName });
106
+ }
107
+
108
+ // Collect memory summaries for direct context injection
109
+ var otherSummary = path.join(otherDir, "knowledge", "memory-summary.md");
110
+ try {
111
+ if (fs.existsSync(otherSummary)) {
112
+ var summaryText = fs.readFileSync(otherSummary, "utf8").trim();
113
+ if (summaryText && summaryText.length > 50) {
114
+ teamSummaries.push({ mateName: mateName, summary: summaryText });
115
+ }
116
+ }
117
+ } catch (e) {}
118
+ }
119
+
120
+ // Inject team memory summaries into context
121
+ if (teamSummaries.length > 0) {
122
+ result += "\n\nTeam memory summaries (other mates' accumulated context):";
123
+ for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
124
+ var ts = teamSummaries[tsi];
125
+ // Cap each summary to avoid context overflow
126
+ var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
127
+ result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
128
+ }
129
+ }
130
+ } catch (e) {}
131
+
132
+ // Inject recent user observations from all mates (newest first, max 15)
133
+ try {
134
+ var allObservations = [];
135
+ var allMatesForObs = matesModule.getAllMates(mateCtx);
136
+ for (var moi = 0; moi < allMatesForObs.length; moi++) {
137
+ var moDir = matesModule.getMateDir(mateCtx, allMatesForObs[moi].id);
138
+ var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
139
+ try {
140
+ if (fs.existsSync(moFile)) {
141
+ var moLines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
142
+ for (var mli = 0; mli < moLines.length; mli++) {
143
+ try {
144
+ var moEntry = JSON.parse(moLines[mli]);
145
+ moEntry._mateName = moEntry.mateName || allMatesForObs[moi].name || allMatesForObs[moi].id;
146
+ allObservations.push(moEntry);
147
+ } catch (e) {}
148
+ }
149
+ }
150
+ } catch (e) {}
151
+ }
152
+ if (allObservations.length > 0) {
153
+ // Sort by date descending
154
+ allObservations.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
155
+ var recentObs = allObservations.slice(0, 15);
156
+ result += "\n\nRecent user observations from all mates:";
157
+ for (var roi = 0; roi < recentObs.length; roi++) {
158
+ var ro = recentObs[roi];
159
+ result += "\n- [" + (ro.date || "?") + "] [@" + ro._mateName + "] [" + (ro.category || "?") + "] " + (ro.observation || "") + (ro.evidence ? " (evidence: " + ro.evidence + ")" : "");
160
+ }
161
+ }
162
+ } catch (e) {}
163
+
164
+ // Inject recent activity timeline across all projects (chronological)
165
+ try {
166
+ var timelineEntries = [];
167
+
168
+ // Own sessions
169
+ sm.sessions.forEach(function (s) {
170
+ if (s.hidden || !s.history || s.history.length === 0) return;
171
+ timelineEntries.push({
172
+ title: s.title || "New Session",
173
+ project: null,
174
+ ts: s.lastActivity || s.createdAt || 0
175
+ });
176
+ });
177
+
178
+ // Cross-project sessions
179
+ var crossForTimeline = getAllProjectSessions();
180
+ for (var cti = 0; cti < crossForTimeline.length; cti++) {
181
+ var cs = crossForTimeline[cti];
182
+ timelineEntries.push({
183
+ title: cs.title || "New Session",
184
+ project: cs._projectTitle || null,
185
+ ts: cs.lastActivity || cs.createdAt || 0
186
+ });
187
+ }
188
+
189
+ // Sort by time descending, take latest 20
190
+ timelineEntries.sort(function (a, b) { return b.ts - a.ts; });
191
+ timelineEntries = timelineEntries.slice(0, 20);
192
+
193
+ if (timelineEntries.length > 0) {
194
+ result += "\n\nRecent activity timeline (newest first):";
195
+ for (var ti = 0; ti < timelineEntries.length; ti++) {
196
+ var te = timelineEntries[ti];
197
+ var dateStr = te.ts ? new Date(te.ts).toISOString().replace("T", " ").substring(0, 16) : "?";
198
+ var line = "- [" + dateStr + "] " + te.title;
199
+ if (te.project) line += " (project: " + te.project + ")";
200
+ result += "\n" + line;
201
+ }
202
+ }
203
+ } catch (e) {}
204
+ }
205
+
206
+ // BM25 unified search: digests + session history for current topic
207
+ // globalSearch mates always search (they see everything); others need enough digests
208
+ if (query && (hasGlobalSearch || allLines.length > 5)) {
209
+ try {
210
+ // Collect mate's own sessions
211
+ var mateSessions = [];
212
+ sm.sessions.forEach(function (s) {
213
+ if (!s.hidden && s.history && s.history.length > 0) {
214
+ mateSessions.push(s);
215
+ }
216
+ });
217
+
218
+ // globalSearch: also collect sessions from all other projects + knowledge files
219
+ var knowledgeFiles = [];
220
+ if (hasGlobalSearch) {
221
+ var crossSessions = getAllProjectSessions();
222
+ for (var cs = 0; cs < crossSessions.length; cs++) {
223
+ mateSessions.push(crossSessions[cs]);
224
+ }
225
+
226
+ // Collect knowledge files from all mates
227
+ try {
228
+ var allMatesForKnowledge = matesModule.getAllMates(mateCtx);
229
+ for (var mk = 0; mk < allMatesForKnowledge.length; mk++) {
230
+ var mkDir = matesModule.getMateDir(mateCtx, allMatesForKnowledge[mk].id);
231
+ var mkName = allMatesForKnowledge[mk].name || allMatesForKnowledge[mk].id;
232
+ var mkKnowledgeDir = path.join(mkDir, "knowledge");
233
+ try {
234
+ var kFiles = fs.readdirSync(mkKnowledgeDir);
235
+ for (var kfi = 0; kfi < kFiles.length; kfi++) {
236
+ var kfName = kFiles[kfi];
237
+ // Skip system files (digests, identity, base-template)
238
+ if (kfName === "session-digests.jsonl" || kfName === "memory-summary.md" ||
239
+ kfName === "identity-backup.md" || kfName === "identity-history.jsonl" ||
240
+ kfName === "base-template.md") continue;
241
+ knowledgeFiles.push({
242
+ filePath: path.join(mkKnowledgeDir, kfName),
243
+ name: kfName,
244
+ mateName: mkName
245
+ });
246
+ }
247
+ } catch (e) {}
248
+ }
249
+ } catch (e) {}
250
+ }
251
+
252
+ var searchResults = sessionSearch.searchMate({
253
+ digestFilePath: digestFile,
254
+ otherDigests: otherDigests,
255
+ sessions: mateSessions,
256
+ knowledgeFiles: knowledgeFiles,
257
+ query: query,
258
+ maxResults: hasGlobalSearch ? 12 : 5,
259
+ minScore: 1.0
260
+ });
261
+ var contextStr = sessionSearch.formatForContext(searchResults);
262
+ if (contextStr) result += contextStr;
263
+ } catch (e) {
264
+ console.error("[session-search] Mate search failed:", e.message);
265
+ }
266
+ }
267
+
268
+ return result;
269
+ }
270
+
271
+ // Gate check: ask Haiku whether this conversation contains anything worth remembering
272
+ function gateMemory(mateCtx, mateId, conversationContent, callback, opts) {
273
+ opts = opts || {};
274
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
275
+ var knowledgeDir = path.join(mateDir, "knowledge");
276
+
277
+ // Load mate role/activities from mate.yaml (lightweight, no full CLAUDE.md)
278
+ var mateRole = "";
279
+ var mateActivities = "";
280
+ try {
281
+ var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
282
+ var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
283
+ var actMatch = yamlRaw.match(/^activities:\s*(.+)$/m);
284
+ if (roleMatch) mateRole = roleMatch[1].trim();
285
+ if (actMatch) mateActivities = actMatch[1].trim();
286
+ } catch (e) {}
287
+
288
+ // Load existing memory summary if available
289
+ var summaryContent = "";
290
+ try {
291
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
292
+ if (fs.existsSync(summaryFile)) {
293
+ summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
294
+ }
295
+ } catch (e) {}
296
+
297
+ // Cap conversation content for gate
298
+ var cappedContent = conversationContent;
299
+ if (cappedContent.length > 3000) {
300
+ cappedContent = cappedContent.substring(0, 3000) + "...";
301
+ }
302
+
303
+ var gateContext = [
304
+ "[SYSTEM: Memory Gate]",
305
+ "You are a memory filter for an AI Mate.",
306
+ "",
307
+ "Mate role: " + (mateRole || "assistant"),
308
+ "Mate activities: " + (mateActivities || "general"),
309
+ "",
310
+ "Current memory summary:",
311
+ summaryContent || "No memory summary yet.",
312
+ "",
313
+ "Conversation just ended:",
314
+ cappedContent,
315
+ ].join("\n");
316
+
317
+ var gatePrompt = opts.gatePrompt || [
318
+ 'Should this conversation be saved to long-term memory?',
319
+ 'Answer "yes" if ANY of these apply:',
320
+ "- A new decision, commitment, or direction",
321
+ "- A change in position or strategy",
322
+ "- New information relevant to this Mate's role",
323
+ "- A user preference, opinion, or pattern not already in the summary",
324
+ "- The user shared personal context, project details, or goals",
325
+ "- The user expressed what they like, dislike, or care about",
326
+ "- The user gave instructions on how they want things done",
327
+ "- Anything the user would reasonably expect to be remembered next time",
328
+ "",
329
+ 'Answer "no" ONLY if:',
330
+ "- It exactly duplicates what is already in the memory summary",
331
+ "- The entire conversation is a single trivial exchange (e.g. just 'hi' / 'hello')",
332
+ "",
333
+ "When in doubt, answer yes. It is better to remember too much than to forget something important.",
334
+ "",
335
+ 'Answer with ONLY "yes" or "no". Nothing else.',
336
+ ].join("\n");
337
+ var defaultOnError = opts.defaultYes !== undefined ? !!opts.defaultYes : true;
338
+
339
+ var gateText = "";
340
+ var _gateSession = null;
341
+ sdk.createMentionSession({
342
+ claudeMd: "",
343
+ model: "haiku",
344
+ initialContext: gateContext,
345
+ initialMessage: gatePrompt,
346
+ onActivity: function () {},
347
+ onDelta: function (delta) {
348
+ gateText += delta;
349
+ },
350
+ onDone: function () {
351
+ var answer = gateText.trim().toLowerCase();
352
+ var shouldRemember = answer.indexOf("yes") !== -1;
353
+ if (_gateSession) try { _gateSession.close(); } catch (e) {}
354
+ callback(shouldRemember);
355
+ },
356
+ onError: function (err) {
357
+ console.error("[memory-gate] Gate check failed for mate " + mateId + ":", err);
358
+ if (_gateSession) try { _gateSession.close(); } catch (e) {}
359
+ callback(defaultOnError);
360
+ },
361
+ }).then(function (gs) {
362
+ _gateSession = gs;
363
+ if (!gs) callback(defaultOnError);
364
+ }).catch(function (err) {
365
+ console.error("[memory-gate] Failed to create gate session for mate " + mateId + ":", err);
366
+ callback(defaultOnError);
367
+ });
368
+ }
369
+
370
+ // Update (or create) memory-summary.md based on a new digest
371
+ function updateMemorySummary(mateCtx, mateId, digestObj) {
372
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
373
+ var knowledgeDir = path.join(mateDir, "knowledge");
374
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
375
+
376
+ // Check if summary exists; if not, try initial generation first
377
+ var summaryExists = false;
378
+ var summaryContent = "";
379
+ try {
380
+ if (fs.existsSync(summaryFile)) {
381
+ summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
382
+ if (summaryContent) summaryExists = true;
383
+ }
384
+ } catch (e) {}
385
+
386
+ if (!summaryExists) {
387
+ // Try initial summary generation from existing digests (migration)
388
+ initMemorySummary(mateCtx, mateId, function () {
389
+ // After init, do incremental update with the new digest
390
+ doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
391
+ });
392
+ } else {
393
+ doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
394
+ }
395
+ }
396
+
397
+ // Incremental update of memory-summary.md with a single new digest
398
+ function doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj) {
399
+ var existingSummary = "";
400
+ try {
401
+ if (fs.existsSync(summaryFile)) {
402
+ existingSummary = fs.readFileSync(summaryFile, "utf8").trim();
403
+ }
404
+ } catch (e) {}
405
+
406
+ var updateContext = [
407
+ "[SYSTEM: Memory Summary Update]",
408
+ "You are updating an AI Mate's long-term memory summary.",
409
+ "",
410
+ "Current summary:",
411
+ existingSummary || "(empty, this is the first entry)",
412
+ "",
413
+ "New session digest to incorporate:",
414
+ JSON.stringify(digestObj, null, 2),
415
+ ].join("\n");
416
+
417
+ var updatePrompt = [
418
+ "Update the summary by:",
419
+ "1. Adding new information from this session",
420
+ "2. Updating existing entries if positions changed",
421
+ "3. Moving resolved open threads out of \"Open Threads\"",
422
+ "4. Adding to \"My Track Record\" if a past prediction/recommendation can now be evaluated",
423
+ "5. Removing outdated or redundant information",
424
+ "6. Preserving important user quotes and context from key_quotes and user_context fields",
425
+ "",
426
+ "Maintain this structure:",
427
+ "",
428
+ "# Memory Summary",
429
+ "Last updated: YYYY-MM-DD (session count: N+1)",
430
+ "",
431
+ "## User Context",
432
+ "(who they are, what they work on, project details, goals)",
433
+ "## User Patterns",
434
+ "(preferences, work style, communication style, likes/dislikes)",
435
+ "## Key Decisions",
436
+ "## Notable Quotes",
437
+ "(important things the user said, verbatim when possible)",
438
+ "## My Track Record",
439
+ "## Open Threads",
440
+ "## Recurring Topics",
441
+ "",
442
+ "Keep it concise. Each section should have at most 10 bullet points.",
443
+ "Drop the oldest/least relevant if needed.",
444
+ "The Notable Quotes section is valuable for preserving the user's voice and intent.",
445
+ "Output ONLY the updated markdown. Nothing else.",
446
+ ].join("\n");
447
+
448
+ var updateText = "";
449
+ var _updateSession = null;
450
+ sdk.createMentionSession({
451
+ claudeMd: "",
452
+ model: "haiku",
453
+ initialContext: updateContext,
454
+ initialMessage: updatePrompt,
455
+ onActivity: function () {},
456
+ onDelta: function (delta) {
457
+ updateText += delta;
458
+ },
459
+ onDone: function () {
460
+ try {
461
+ var cleaned = updateText.trim();
462
+ if (cleaned.indexOf("```") === 0) {
463
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
464
+ }
465
+ fs.mkdirSync(knowledgeDir, { recursive: true });
466
+ fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
467
+ console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
468
+ } catch (e) {
469
+ console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
470
+ }
471
+ if (_updateSession) try { _updateSession.close(); } catch (e) {}
472
+ },
473
+ onError: function (err) {
474
+ console.error("[memory-summary] Summary update failed for mate " + mateId + ":", err);
475
+ if (_updateSession) try { _updateSession.close(); } catch (e) {}
476
+ },
477
+ }).then(function (us) {
478
+ _updateSession = us;
479
+ }).catch(function (err) {
480
+ console.error("[memory-summary] Failed to create summary update session for mate " + mateId + ":", err);
481
+ });
482
+ }
483
+
484
+ // Initial summary generation (migration): read latest 20 digests and generate first summary
485
+ function initMemorySummary(mateCtx, mateId, callback) {
486
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
487
+ var knowledgeDir = path.join(mateDir, "knowledge");
488
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
489
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
490
+
491
+ // Check if digests exist
492
+ var allLines = [];
493
+ try {
494
+ if (fs.existsSync(digestFile)) {
495
+ allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
496
+ }
497
+ } catch (e) {}
498
+
499
+ if (allLines.length === 0) {
500
+ // No digests to summarize, just callback
501
+ callback();
502
+ return;
503
+ }
504
+
505
+ var recent = allLines.slice(-20);
506
+ var digestsText = [];
507
+ for (var i = 0; i < recent.length; i++) {
508
+ try {
509
+ var d = JSON.parse(recent[i]);
510
+ digestsText.push(JSON.stringify(d));
511
+ } catch (e) {}
512
+ }
513
+
514
+ if (digestsText.length === 0) {
515
+ callback();
516
+ return;
517
+ }
518
+
519
+ var initContext = [
520
+ "[SYSTEM: Initial Memory Summary]",
521
+ "You are creating the first long-term memory summary for an AI Mate.",
522
+ "",
523
+ "Here are the most recent session digests (up to 20):",
524
+ digestsText.join("\n"),
525
+ ].join("\n");
526
+
527
+ var initPrompt = [
528
+ "Create a memory summary from these sessions.",
529
+ "",
530
+ "Structure:",
531
+ "",
532
+ "# Memory Summary",
533
+ "Last updated: YYYY-MM-DD (session count: N)",
534
+ "",
535
+ "## User Context",
536
+ "(who they are, what they work on, project details, goals)",
537
+ "## User Patterns",
538
+ "(preferences, work style, communication style, likes/dislikes)",
539
+ "## Key Decisions",
540
+ "## Notable Quotes",
541
+ "(important things the user said, verbatim when possible)",
542
+ "## My Track Record",
543
+ "## Open Threads",
544
+ "## Recurring Topics",
545
+ "",
546
+ "Keep it concise. Focus on patterns, decisions, and the user's own words.",
547
+ "Each section should have at most 10 bullet points.",
548
+ "Preserve key_quotes from digests in the Notable Quotes section.",
549
+ "Set session count to " + digestsText.length + ".",
550
+ "Output ONLY the markdown. Nothing else.",
551
+ ].join("\n");
552
+
553
+ var initText = "";
554
+ var _initSession = null;
555
+ sdk.createMentionSession({
556
+ claudeMd: "",
557
+ model: "haiku",
558
+ initialContext: initContext,
559
+ initialMessage: initPrompt,
560
+ onActivity: function () {},
561
+ onDelta: function (delta) {
562
+ initText += delta;
563
+ },
564
+ onDone: function () {
565
+ try {
566
+ var cleaned = initText.trim();
567
+ if (cleaned.indexOf("```") === 0) {
568
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
569
+ }
570
+ fs.mkdirSync(knowledgeDir, { recursive: true });
571
+ fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
572
+ console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
573
+ } catch (e) {
574
+ console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
575
+ }
576
+ if (_initSession) try { _initSession.close(); } catch (e) {}
577
+ callback();
578
+ },
579
+ onError: function (err) {
580
+ console.error("[memory-summary] Initial summary generation failed for mate " + mateId + ":", err);
581
+ if (_initSession) try { _initSession.close(); } catch (e) {}
582
+ callback();
583
+ },
584
+ }).then(function (is) {
585
+ _initSession = is;
586
+ if (!is) callback();
587
+ }).catch(function (err) {
588
+ console.error("[memory-summary] Failed to create init summary session for mate " + mateId + ":", err);
589
+ callback();
590
+ });
591
+ }
592
+
593
+ // --- Message handlers for memory management UI ---
594
+
595
+ function handleMemoryList(ws) {
596
+ var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
597
+ var summaryFile = path.join(cwd, "knowledge", "memory-summary.md");
598
+ var entries = [];
599
+ var summary = "";
600
+ try {
601
+ var raw = fs.readFileSync(digestFile, "utf8").trim();
602
+ if (raw) {
603
+ var lines = raw.split("\n");
604
+ for (var mi = 0; mi < lines.length; mi++) {
605
+ try {
606
+ var obj = JSON.parse(lines[mi]);
607
+ obj.index = mi;
608
+ entries.push(obj);
609
+ } catch (e) {}
610
+ }
611
+ }
612
+ } catch (e) { /* file may not exist */ }
613
+ try {
614
+ if (fs.existsSync(summaryFile)) {
615
+ summary = fs.readFileSync(summaryFile, "utf8").trim();
616
+ }
617
+ } catch (e) {}
618
+ // Return newest first
619
+ entries.reverse();
620
+ sendTo(ws, { type: "memory_list", entries: entries, summary: summary });
621
+ }
622
+
623
+ function handleMemorySearch(ws, msg) {
624
+ if (!msg.query || typeof msg.query !== "string") {
625
+ sendTo(ws, { type: "memory_search_results", results: [], query: "" });
626
+ return;
627
+ }
628
+ var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
629
+ try {
630
+ var results = sessionSearch.searchDigests(digestFile, msg.query, {
631
+ maxResults: msg.maxResults || 10,
632
+ minScore: msg.minScore || 0.5,
633
+ dateFrom: msg.dateFrom || null,
634
+ dateTo: msg.dateTo || null
635
+ });
636
+ sendTo(ws, {
637
+ type: "memory_search_results",
638
+ results: sessionSearch.formatForMemoryUI(results),
639
+ query: msg.query
640
+ });
641
+ } catch (e) {
642
+ console.error("[session-search] Search failed:", e.message);
643
+ sendTo(ws, { type: "memory_search_results", results: [], query: msg.query });
644
+ }
645
+ }
646
+
647
+ function handleMemoryDelete(ws, msg) {
648
+ if (typeof msg.index !== "number") return;
649
+ var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
650
+ try {
651
+ var raw = fs.readFileSync(digestFile, "utf8").trim();
652
+ var lines = raw ? raw.split("\n") : [];
653
+ if (msg.index >= 0 && msg.index < lines.length) {
654
+ lines.splice(msg.index, 1);
655
+ if (lines.length === 0) {
656
+ fs.unlinkSync(digestFile);
657
+ } else {
658
+ fs.writeFileSync(digestFile, lines.join("\n") + "\n");
659
+ }
660
+ }
661
+ } catch (e) {}
662
+ sendTo(ws, { type: "memory_deleted", index: msg.index });
663
+ handleMessage(ws, { type: "memory_list" });
664
+ }
665
+
666
+ return {
667
+ loadMateDigests: loadMateDigests,
668
+ gateMemory: gateMemory,
669
+ updateMemorySummary: updateMemorySummary,
670
+ initMemorySummary: initMemorySummary,
671
+ handleMemoryList: handleMemoryList,
672
+ handleMemorySearch: handleMemorySearch,
673
+ handleMemoryDelete: handleMemoryDelete,
674
+ };
675
+ }
676
+
677
+ module.exports = { attachMemory };