clay-server 2.17.0 → 2.18.0-beta.10

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/lib/daemon.js CHANGED
@@ -979,7 +979,7 @@ if (usersModule.isMultiUser()) {
979
979
  var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
980
980
  if (fs.existsSync(mateDir)) {
981
981
  console.log("[daemon] Adding mate project:", mateSlug);
982
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true });
982
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName });
983
983
  }
984
984
  }
985
985
  }
@@ -994,7 +994,7 @@ if (usersModule.isMultiUser()) {
994
994
  var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
995
995
  if (fs.existsSync(mateDir)) {
996
996
  console.log("[daemon] Adding mate project:", mateSlug);
997
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true });
997
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName });
998
998
  }
999
999
  }
1000
1000
  }
package/lib/mates.js CHANGED
@@ -135,6 +135,7 @@ function createMate(ctx, seedData) {
135
135
  }
136
136
  claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
137
137
  claudeMd += TEAM_SECTION;
138
+ claudeMd += SESSION_MEMORY_SECTION;
138
139
  claudeMd += crisisSafety.getSection();
139
140
  fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
140
141
 
@@ -299,28 +300,104 @@ function enforceTeamAwareness(filePath) {
299
300
  // Check if already present and correct
300
301
  var teamIdx = content.indexOf(TEAM_MARKER);
301
302
  if (teamIdx !== -1) {
302
- // Extract existing team section (up to next system marker or ## heading)
303
+ // Extract existing team section (up to next system marker or end)
303
304
  var afterTeam = content.substring(teamIdx);
305
+ // Find the nearest following system marker (session memory or crisis safety)
306
+ var nextMarkerIdx = -1;
307
+ var memIdx = afterTeam.indexOf(SESSION_MEMORY_MARKER);
304
308
  var crisisIdx = afterTeam.indexOf(crisisSafety.MARKER);
309
+ if (memIdx !== -1 && (crisisIdx === -1 || memIdx < crisisIdx)) {
310
+ nextMarkerIdx = memIdx;
311
+ } else if (crisisIdx !== -1) {
312
+ nextMarkerIdx = crisisIdx;
313
+ }
305
314
  var existing;
306
- if (crisisIdx !== -1) {
307
- existing = afterTeam.substring(0, crisisIdx).trimEnd();
315
+ if (nextMarkerIdx !== -1) {
316
+ existing = afterTeam.substring(0, nextMarkerIdx).trimEnd();
308
317
  } else {
309
318
  existing = afterTeam.trimEnd();
310
319
  }
311
320
  if (existing === TEAM_SECTION.trimStart().trimEnd()) return false; // already correct
312
321
 
313
322
  // Strip the existing team section
314
- var endOfTeam = crisisIdx !== -1 ? teamIdx + crisisIdx : content.length;
323
+ var endOfTeam = nextMarkerIdx !== -1 ? teamIdx + nextMarkerIdx : content.length;
315
324
  content = content.substring(0, teamIdx).trimEnd() + content.substring(endOfTeam);
316
325
  }
317
326
 
327
+ // Insert before session memory or crisis safety section if present, otherwise append
328
+ var sessionMemPos = content.indexOf(SESSION_MEMORY_MARKER);
329
+ var crisisPos = content.indexOf(crisisSafety.MARKER);
330
+ var insertBefore = -1;
331
+ if (sessionMemPos !== -1) {
332
+ insertBefore = sessionMemPos;
333
+ } else if (crisisPos !== -1) {
334
+ insertBefore = crisisPos;
335
+ }
336
+ if (insertBefore !== -1) {
337
+ content = content.substring(0, insertBefore).trimEnd() + TEAM_SECTION + "\n\n" + content.substring(insertBefore);
338
+ } else {
339
+ content = content.trimEnd() + TEAM_SECTION;
340
+ }
341
+
342
+ fs.writeFileSync(filePath, content, "utf8");
343
+ return true;
344
+ }
345
+
346
+ // --- Session memory ---
347
+
348
+ var SESSION_MEMORY_MARKER = "<!-- SESSION_MEMORY_MANAGED_BY_SYSTEM -->";
349
+
350
+ var SESSION_MEMORY_SECTION =
351
+ "\n\n" + SESSION_MEMORY_MARKER + "\n" +
352
+ "## Session Memory\n\n" +
353
+ "**This section is managed by the system and cannot be removed.**\n\n" +
354
+ "Your `knowledge/session-digests.jsonl` file may contain summaries of your participation " +
355
+ "in project sessions via @mentions. Each line is a JSON object recording your perspective " +
356
+ "from that session, including your positions, disagreements with other mates, decisions made, " +
357
+ "and open items.\n\n" +
358
+ "When a user references a previous conversation, asks what you discussed before, or when " +
359
+ "continuity with a past session is relevant, read this file to recall context. " +
360
+ "Do not read this file proactively on every conversation start.\n";
361
+
362
+ function hasSessionMemory(content) {
363
+ return content.indexOf(SESSION_MEMORY_MARKER) !== -1;
364
+ }
365
+
366
+ /**
367
+ * Enforce the session memory section on a mate's CLAUDE.md.
368
+ * Inserts after team awareness section and before crisis safety section.
369
+ * Returns true if the file was modified.
370
+ */
371
+ function enforceSessionMemory(filePath) {
372
+ if (!fs.existsSync(filePath)) return false;
373
+
374
+ var content = fs.readFileSync(filePath, "utf8");
375
+
376
+ // Check if already present and correct
377
+ var memIdx = content.indexOf(SESSION_MEMORY_MARKER);
378
+ if (memIdx !== -1) {
379
+ // Extract existing section (up to next system marker or end)
380
+ var afterMem = content.substring(memIdx);
381
+ var crisisIdx = afterMem.indexOf(crisisSafety.MARKER);
382
+ var existing;
383
+ if (crisisIdx !== -1) {
384
+ existing = afterMem.substring(0, crisisIdx).trimEnd();
385
+ } else {
386
+ existing = afterMem.trimEnd();
387
+ }
388
+ if (existing === SESSION_MEMORY_SECTION.trimStart().trimEnd()) return false; // already correct
389
+
390
+ // Strip the existing session memory section
391
+ var endOfMem = crisisIdx !== -1 ? memIdx + crisisIdx : content.length;
392
+ content = content.substring(0, memIdx).trimEnd() + content.substring(endOfMem);
393
+ }
394
+
318
395
  // Insert before crisis safety section if present, otherwise append
319
396
  var crisisPos = content.indexOf(crisisSafety.MARKER);
320
397
  if (crisisPos !== -1) {
321
- content = content.substring(0, crisisPos).trimEnd() + TEAM_SECTION + "\n\n" + content.substring(crisisPos);
398
+ content = content.substring(0, crisisPos).trimEnd() + SESSION_MEMORY_SECTION + "\n\n" + content.substring(crisisPos);
322
399
  } else {
323
- content = content.trimEnd() + TEAM_SECTION;
400
+ content = content.trimEnd() + SESSION_MEMORY_SECTION;
324
401
  }
325
402
 
326
403
  fs.writeFileSync(filePath, content, "utf8");
@@ -471,6 +548,8 @@ module.exports = {
471
548
  formatSeedContext: formatSeedContext,
472
549
  enforceTeamAwareness: enforceTeamAwareness,
473
550
  TEAM_MARKER: TEAM_MARKER,
551
+ enforceSessionMemory: enforceSessionMemory,
552
+ SESSION_MEMORY_MARKER: SESSION_MEMORY_MARKER,
474
553
  loadCommonKnowledge: loadCommonKnowledge,
475
554
  promoteKnowledge: promoteKnowledge,
476
555
  depromoteKnowledge: depromoteKnowledge,
package/lib/project.js CHANGED
@@ -3,7 +3,7 @@ var path = require("path");
3
3
  var os = require("os");
4
4
  var crypto = require("crypto");
5
5
  var { createSessionManager } = require("./sessions");
6
- var { createSDKBridge } = require("./sdk-bridge");
6
+ var { createSDKBridge, createMessageQueue } = require("./sdk-bridge");
7
7
  var { createTerminalManager } = require("./terminal-manager");
8
8
  var { createNotesManager } = require("./notes");
9
9
  var { fetchLatestVersion, fetchVersion, isNewer } = require("./updater");
@@ -415,6 +415,7 @@ function createProjectContext(opts) {
415
415
  pushModule: pushModule,
416
416
  getSDK: getSDK,
417
417
  mateDisplayName: opts.mateDisplayName || "",
418
+ isMate: isMate,
418
419
  dangerouslySkipPermissions: dangerouslySkipPermissions,
419
420
  onProcessingChanged: onProcessingChanged,
420
421
  });
@@ -687,8 +688,9 @@ function createProjectContext(opts) {
687
688
  console.error("[loop-registry] PROMPT.md missing for " + loopId);
688
689
  return;
689
690
  }
690
- // Set the loopId and start
691
+ // Set the loopId and start — clear wizardData so stale crafting names don't leak into session titles
691
692
  loopState.loopId = loopId;
693
+ loopState.wizardData = null;
692
694
  activeRegistryId = record.id;
693
695
  console.log("[loop-registry] Auto-starting loop: " + record.name + " (" + loopId + ")");
694
696
  send({ type: "schedule_run_started", recordId: record.id });
@@ -780,8 +782,8 @@ function createProjectContext(opts) {
780
782
  }
781
783
 
782
784
  var session = sm.createSession();
783
- var loopName = (loopState.wizardData && loopState.wizardData.name) || "";
784
785
  var loopSource = loopRegistry.getById(loopState.loopId);
786
+ var loopName = (loopState.wizardData && loopState.wizardData.name) || (loopSource && loopSource.name) || "";
785
787
  var loopSourceTag = (loopSource && loopSource.source) || null;
786
788
  var isRalphLoop = loopSourceTag === "ralph";
787
789
  session.loop = { active: true, iteration: loopState.iteration, role: "coder", loopId: loopState.loopId, name: loopName, source: loopSourceTag, startedAt: loopState.startedAt };
@@ -1336,6 +1338,12 @@ function createProjectContext(opts) {
1336
1338
  return;
1337
1339
  }
1338
1340
 
1341
+ // --- @Mention: invoke another Mate inline ---
1342
+ if (msg.type === "mention") {
1343
+ handleMention(ws, msg);
1344
+ return;
1345
+ }
1346
+
1339
1347
  // --- Knowledge file management ---
1340
1348
  if (msg.type === "knowledge_list") {
1341
1349
  var knowledgeDir = path.join(cwd, "knowledge");
@@ -3468,6 +3476,13 @@ function createProjectContext(opts) {
3468
3476
  }
3469
3477
  }
3470
3478
 
3479
+ // Inject pending @mention context so the current agent sees the exchange
3480
+ if (session.pendingMentionContexts && session.pendingMentionContexts.length > 0) {
3481
+ var mentionPrefix = session.pendingMentionContexts.join("\n\n");
3482
+ session.pendingMentionContexts = [];
3483
+ fullText = mentionPrefix + "\n\n" + fullText;
3484
+ }
3485
+
3471
3486
  if (!session.isProcessing) {
3472
3487
  session.isProcessing = true;
3473
3488
  onProcessingChanged();
@@ -3484,6 +3499,386 @@ function createProjectContext(opts) {
3484
3499
  sm.broadcastSessionList();
3485
3500
  }
3486
3501
 
3502
+ // --- @Mention handler ---
3503
+ var MENTION_WINDOW = 15; // turns to check for session continuity
3504
+
3505
+ function getRecentTurns(session, n) {
3506
+ var turns = [];
3507
+ var history = session.history;
3508
+ // Walk backwards through history, collect user/assistant/mention text turns
3509
+ var assistantBuffer = "";
3510
+ for (var i = history.length - 1; i >= 0 && turns.length < n; i--) {
3511
+ var entry = history[i];
3512
+ if (entry.type === "user_message") {
3513
+ if (assistantBuffer) {
3514
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3515
+ assistantBuffer = "";
3516
+ }
3517
+ turns.push({ role: "user", text: entry.text || "" });
3518
+ } else if (entry.type === "delta" || entry.type === "text") {
3519
+ assistantBuffer = (entry.text || "") + assistantBuffer;
3520
+ } else if (entry.type === "mention_response") {
3521
+ if (assistantBuffer) {
3522
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3523
+ assistantBuffer = "";
3524
+ }
3525
+ turns.push({ role: "@" + (entry.mateName || "Mate"), text: entry.text || "", mateId: entry.mateId });
3526
+ } else if (entry.type === "mention_user") {
3527
+ if (assistantBuffer) {
3528
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3529
+ assistantBuffer = "";
3530
+ }
3531
+ turns.push({ role: "user", text: "@" + (entry.mateName || "Mate") + " " + (entry.text || ""), mateId: entry.mateId });
3532
+ }
3533
+ }
3534
+ if (assistantBuffer) {
3535
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3536
+ }
3537
+ turns.reverse();
3538
+ return turns;
3539
+ }
3540
+
3541
+ // Check if the given mate has a mention response in the recent window
3542
+ function hasMateInWindow(recentTurns, mateId) {
3543
+ for (var i = 0; i < recentTurns.length; i++) {
3544
+ if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
3545
+ return true;
3546
+ }
3547
+ }
3548
+ return false;
3549
+ }
3550
+
3551
+ // Build the "middle context": conversation turns since the mate's last response
3552
+ function buildMiddleContext(recentTurns, mateId) {
3553
+ // Find the last mention response from this mate
3554
+ var lastIdx = -1;
3555
+ for (var i = recentTurns.length - 1; i >= 0; i--) {
3556
+ if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
3557
+ lastIdx = i;
3558
+ break;
3559
+ }
3560
+ }
3561
+ if (lastIdx === -1 || lastIdx >= recentTurns.length - 1) return "";
3562
+
3563
+ // Collect turns after the last mention response
3564
+ var lines = ["[Conversation since your last response:]", "---"];
3565
+ for (var j = lastIdx + 1; j < recentTurns.length; j++) {
3566
+ var turn = recentTurns[j];
3567
+ lines.push(turn.role + ": " + turn.text);
3568
+ }
3569
+ lines.push("---");
3570
+ return lines.join("\n");
3571
+ }
3572
+
3573
+ function buildMentionContext(userName, recentTurns) {
3574
+ var lines = [
3575
+ "You were @mentioned in a project session by " + userName + ".",
3576
+ "You are responding inline in their conversation. Keep your response focused on what was asked.",
3577
+ "You have read-only access to the project files but cannot make changes.",
3578
+ "",
3579
+ "Recent conversation context:",
3580
+ "---",
3581
+ ];
3582
+ for (var i = 0; i < recentTurns.length; i++) {
3583
+ var turn = recentTurns[i];
3584
+ lines.push(turn.role + ": " + turn.text);
3585
+ }
3586
+ lines.push("---");
3587
+ return lines.join("\n");
3588
+ }
3589
+
3590
+ function digestMentionSession(session, mateId, mateCtx, mateResponse, userQuestion) {
3591
+ if (!session._mentionSessions || !session._mentionSessions[mateId]) return;
3592
+ var mentionSession = session._mentionSessions[mateId];
3593
+ if (!mentionSession.isAlive()) return;
3594
+
3595
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
3596
+ var knowledgeDir = path.join(mateDir, "knowledge");
3597
+
3598
+ var digestPrompt = [
3599
+ "[SYSTEM: Session Digest Request]",
3600
+ "The conversation has ended. Summarize this session from YOUR perspective for your long-term memory.",
3601
+ "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
3602
+ "Schema:",
3603
+ "{",
3604
+ ' "date": "YYYY-MM-DD",',
3605
+ ' "topic": "short topic description",',
3606
+ ' "my_position": "what I said/recommended",',
3607
+ ' "other_perspectives": "other mates or user perspectives if relevant",',
3608
+ ' "decisions": "what was decided, or null if pending",',
3609
+ ' "open_items": "what remains unresolved",',
3610
+ ' "user_sentiment": "how the user seemed to feel about this topic"',
3611
+ "}",
3612
+ "",
3613
+ "IMPORTANT: Output ONLY the JSON object. Nothing else.",
3614
+ ].join("\n");
3615
+
3616
+ var digestText = "";
3617
+ mentionSession.pushMessage(digestPrompt, {
3618
+ onActivity: function () {},
3619
+ onDelta: function (delta) {
3620
+ digestText += delta;
3621
+ },
3622
+ onDone: function () {
3623
+ var digestObj = null;
3624
+ try {
3625
+ var cleaned = digestText.trim();
3626
+ if (cleaned.indexOf("```") === 0) {
3627
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
3628
+ }
3629
+ digestObj = JSON.parse(cleaned);
3630
+ } catch (e) {
3631
+ console.error("[digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
3632
+ digestObj = {
3633
+ date: new Date().toISOString().slice(0, 10),
3634
+ topic: "parse_failed",
3635
+ raw: digestText.substring(0, 500),
3636
+ };
3637
+ }
3638
+
3639
+ try {
3640
+ fs.mkdirSync(knowledgeDir, { recursive: true });
3641
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
3642
+ fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
3643
+ } catch (e) {
3644
+ console.error("[digest] Failed to write digest for mate " + mateId + ":", e.message);
3645
+ }
3646
+ },
3647
+ onError: function (err) {
3648
+ console.error("[digest] Digest generation failed for mate " + mateId + ":", err);
3649
+ },
3650
+ });
3651
+ }
3652
+
3653
+ function handleMention(ws, msg) {
3654
+ if (!msg.mateId || !msg.text) return;
3655
+
3656
+ var session = getSessionForWs(ws);
3657
+ if (!session) return;
3658
+
3659
+ // Check if a mention is already in progress for this session
3660
+ if (session._mentionInProgress) {
3661
+ sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "A mention is already in progress." });
3662
+ return;
3663
+ }
3664
+
3665
+ var userId = ws._clayUser ? ws._clayUser.id : null;
3666
+ var mateCtx = matesModule.buildMateCtx(userId);
3667
+ var mate = matesModule.getMate(mateCtx, msg.mateId);
3668
+ if (!mate) {
3669
+ sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Mate not found" });
3670
+ return;
3671
+ }
3672
+
3673
+ var mateName = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
3674
+ var avatarColor = (mate.profile && mate.profile.avatarColor) || "#6c5ce7";
3675
+ var avatarStyle = (mate.profile && mate.profile.avatarStyle) || "bottts";
3676
+ var avatarSeed = (mate.profile && mate.profile.avatarSeed) || mate.id;
3677
+
3678
+ // Build full mention text (include pasted content)
3679
+ var mentionFullInput = msg.text;
3680
+ if (msg.pastes && msg.pastes.length > 0) {
3681
+ for (var pi = 0; pi < msg.pastes.length; pi++) {
3682
+ if (mentionFullInput) mentionFullInput += "\n\n";
3683
+ mentionFullInput += msg.pastes[pi];
3684
+ }
3685
+ }
3686
+
3687
+ // Save mention user message to session history
3688
+ var mentionUserEntry = { type: "mention_user", text: msg.text, mateId: msg.mateId, mateName: mateName };
3689
+ if (msg.pastes && msg.pastes.length > 0) mentionUserEntry.pastes = msg.pastes;
3690
+ session.history.push(mentionUserEntry);
3691
+ sm.appendToSessionFile(session, mentionUserEntry);
3692
+ sendToSessionOthers(ws, session.localId, mentionUserEntry);
3693
+
3694
+ // Extract recent turns for continuity check
3695
+ var recentTurns = getRecentTurns(session, MENTION_WINDOW);
3696
+
3697
+ // Determine user name for context
3698
+ var userName = "User";
3699
+ if (ws._clayUser) {
3700
+ var p = ws._clayUser.profile || {};
3701
+ userName = p.name || ws._clayUser.displayName || ws._clayUser.username || "User";
3702
+ }
3703
+
3704
+ session._mentionInProgress = true;
3705
+
3706
+ // Send mention start indicator
3707
+ sendToSession(session.localId, {
3708
+ type: "mention_start",
3709
+ mateId: msg.mateId,
3710
+ mateName: mateName,
3711
+ avatarColor: avatarColor,
3712
+ avatarStyle: avatarStyle,
3713
+ avatarSeed: avatarSeed,
3714
+ });
3715
+
3716
+ // Shared callbacks for both new and continued sessions
3717
+ var mentionCallbacks = {
3718
+ onActivity: function (activity) {
3719
+ sendToSession(session.localId, {
3720
+ type: "mention_activity",
3721
+ mateId: msg.mateId,
3722
+ activity: activity,
3723
+ });
3724
+ },
3725
+ onDelta: function (delta) {
3726
+ sendToSession(session.localId, {
3727
+ type: "mention_stream",
3728
+ mateId: msg.mateId,
3729
+ mateName: mateName,
3730
+ delta: delta,
3731
+ });
3732
+ },
3733
+ onDone: function (fullText) {
3734
+ session._mentionInProgress = false;
3735
+
3736
+ // Save mention response to session history
3737
+ var mentionResponseEntry = {
3738
+ type: "mention_response",
3739
+ mateId: msg.mateId,
3740
+ mateName: mateName,
3741
+ text: fullText,
3742
+ avatarColor: avatarColor,
3743
+ avatarStyle: avatarStyle,
3744
+ avatarSeed: avatarSeed,
3745
+ };
3746
+ session.history.push(mentionResponseEntry);
3747
+ sm.appendToSessionFile(session, mentionResponseEntry);
3748
+
3749
+ // Queue mention context for injection into the current agent's next turn
3750
+ if (!session.pendingMentionContexts) session.pendingMentionContexts = [];
3751
+ session.pendingMentionContexts.push(
3752
+ "[Context: @" + mateName + " was mentioned and responded]\n\n" +
3753
+ "User asked @" + mateName + ": " + msg.text + "\n" +
3754
+ mateName + " responded: " + fullText + "\n\n" +
3755
+ "[End of @mention context. This is for your reference only. Do not re-execute or repeat this response.]"
3756
+ );
3757
+
3758
+ sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
3759
+
3760
+ // Generate session digest for mate's long-term memory
3761
+ digestMentionSession(session, msg.mateId, mateCtx, fullText, msg.text);
3762
+ },
3763
+ onError: function (errMsg) {
3764
+ session._mentionInProgress = false;
3765
+ // Clean up dead session
3766
+ if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
3767
+ delete session._mentionSessions[msg.mateId];
3768
+ }
3769
+ console.error("[mention] Error for mate " + msg.mateId + ":", errMsg);
3770
+ sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: errMsg });
3771
+ },
3772
+ };
3773
+
3774
+ // Initialize mention sessions map if needed
3775
+ if (!session._mentionSessions) session._mentionSessions = {};
3776
+
3777
+ // Session continuity: check if this mate has a response in the recent window
3778
+ var existingSession = session._mentionSessions[msg.mateId];
3779
+ var canContinue = existingSession && existingSession.isAlive() && hasMateInWindow(recentTurns, msg.mateId);
3780
+
3781
+ if (canContinue) {
3782
+ // Continue existing mention session with middle context
3783
+ var middleContext = buildMiddleContext(recentTurns, msg.mateId);
3784
+ var continuationText = middleContext ? middleContext + "\n\n" + mentionFullInput : mentionFullInput;
3785
+ existingSession.pushMessage(continuationText, mentionCallbacks);
3786
+ } else {
3787
+ // Clean up old session if it exists
3788
+ if (existingSession) {
3789
+ existingSession.close();
3790
+ delete session._mentionSessions[msg.mateId];
3791
+ }
3792
+
3793
+ // Load Mate CLAUDE.md
3794
+ var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
3795
+ var claudeMd = "";
3796
+ try {
3797
+ claudeMd = fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
3798
+ } catch (e) {
3799
+ // CLAUDE.md may not exist for new mates
3800
+ }
3801
+
3802
+ // Load recent session digests for context continuity
3803
+ var recentDigests = "";
3804
+ try {
3805
+ var digestFile = path.join(mateDir, "knowledge", "session-digests.jsonl");
3806
+ if (fs.existsSync(digestFile)) {
3807
+ var allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n");
3808
+ var recent = allLines.slice(-5); // last 5 digests
3809
+ if (recent.length > 0) {
3810
+ recentDigests = "\n\nYour recent session memories (from past @mentions):\n";
3811
+ for (var di = 0; di < recent.length; di++) {
3812
+ try {
3813
+ var d = JSON.parse(recent[di]);
3814
+ recentDigests += "- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
3815
+ (d.decisions ? " | Decisions: " + d.decisions : "") +
3816
+ (d.open_items ? " | Open: " + d.open_items : "") + "\n";
3817
+ } catch (e) {}
3818
+ }
3819
+ }
3820
+ }
3821
+ } catch (e) {}
3822
+
3823
+ // Build initial mention context
3824
+ var mentionContext = buildMentionContext(userName, recentTurns) + recentDigests;
3825
+
3826
+ // Create new persistent mention session
3827
+ sdk.createMentionSession({
3828
+ claudeMd: claudeMd,
3829
+ initialContext: mentionContext,
3830
+ initialMessage: mentionFullInput,
3831
+ onActivity: mentionCallbacks.onActivity,
3832
+ onDelta: mentionCallbacks.onDelta,
3833
+ onDone: mentionCallbacks.onDone,
3834
+ onError: mentionCallbacks.onError,
3835
+ canUseTool: function (toolName, input, toolOpts) {
3836
+ var autoAllow = { Read: true, Glob: true, Grep: true };
3837
+ if (autoAllow[toolName]) {
3838
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
3839
+ }
3840
+ // Route through the project session's permission system
3841
+ return new Promise(function (resolve) {
3842
+ var requestId = crypto.randomUUID();
3843
+ session.pendingPermissions[requestId] = {
3844
+ resolve: resolve,
3845
+ requestId: requestId,
3846
+ toolName: toolName,
3847
+ toolInput: input,
3848
+ toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
3849
+ decisionReason: (toolOpts && toolOpts.decisionReason) || "",
3850
+ };
3851
+ sendToSession(session.localId, {
3852
+ type: "permission_request",
3853
+ requestId: requestId,
3854
+ toolName: toolName,
3855
+ toolInput: input,
3856
+ toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
3857
+ decisionReason: (toolOpts && toolOpts.decisionReason) || "",
3858
+ });
3859
+ onProcessingChanged();
3860
+ if (toolOpts && toolOpts.signal) {
3861
+ toolOpts.signal.addEventListener("abort", function () {
3862
+ delete session.pendingPermissions[requestId];
3863
+ sendToSession(session.localId, { type: "permission_cancel", requestId: requestId });
3864
+ onProcessingChanged();
3865
+ resolve({ behavior: "deny", message: "Request cancelled" });
3866
+ });
3867
+ }
3868
+ });
3869
+ },
3870
+ }).then(function (mentionSession) {
3871
+ if (mentionSession) {
3872
+ session._mentionSessions[msg.mateId] = mentionSession;
3873
+ }
3874
+ }).catch(function (err) {
3875
+ session._mentionInProgress = false;
3876
+ console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
3877
+ sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
3878
+ });
3879
+ }
3880
+ }
3881
+
3487
3882
  // --- Session presence (who is viewing which session) ---
3488
3883
  function broadcastPresence() {
3489
3884
  if (!usersModule.isMultiUser()) return;
@@ -4056,7 +4451,7 @@ function createProjectContext(opts) {
4056
4451
  loopRegistry.stopTimer();
4057
4452
  stopFileWatch();
4058
4453
  stopAllDirWatches();
4059
- // Abort all active sessions
4454
+ // Abort all active sessions and clean up mention sessions
4060
4455
  sm.sessions.forEach(function (session) {
4061
4456
  session.destroying = true;
4062
4457
  if (session.abortController) {
@@ -4065,6 +4460,14 @@ function createProjectContext(opts) {
4065
4460
  if (session.messageQueue) {
4066
4461
  try { session.messageQueue.end(); } catch (e) {}
4067
4462
  }
4463
+ // Close all mention SDK sessions to prevent zombie processes
4464
+ if (session._mentionSessions) {
4465
+ var mateIds = Object.keys(session._mentionSessions);
4466
+ for (var mi = 0; mi < mateIds.length; mi++) {
4467
+ try { session._mentionSessions[mateIds[mi]].close(); } catch (e) {}
4468
+ }
4469
+ session._mentionSessions = {};
4470
+ }
4068
4471
  });
4069
4472
  // Kill all terminals
4070
4473
  tm.destroyAll();
@@ -4149,6 +4552,7 @@ function createProjectContext(opts) {
4149
4552
  var claudeMdPath = path.join(cwd, "CLAUDE.md");
4150
4553
  // Enforce immediately on startup
4151
4554
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
4555
+ try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
4152
4556
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
4153
4557
  // Watch for changes
4154
4558
  try {
@@ -4157,6 +4561,7 @@ function createProjectContext(opts) {
4157
4561
  crisisDebounce = setTimeout(function () {
4158
4562
  crisisDebounce = null;
4159
4563
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
4564
+ try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
4160
4565
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
4161
4566
  }, 500);
4162
4567
  });