clay-server 2.17.0 → 2.18.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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");
@@ -1336,6 +1336,12 @@ function createProjectContext(opts) {
1336
1336
  return;
1337
1337
  }
1338
1338
 
1339
+ // --- @Mention: invoke another Mate inline ---
1340
+ if (msg.type === "mention") {
1341
+ handleMention(ws, msg);
1342
+ return;
1343
+ }
1344
+
1339
1345
  // --- Knowledge file management ---
1340
1346
  if (msg.type === "knowledge_list") {
1341
1347
  var knowledgeDir = path.join(cwd, "knowledge");
@@ -3468,6 +3474,13 @@ function createProjectContext(opts) {
3468
3474
  }
3469
3475
  }
3470
3476
 
3477
+ // Inject pending @mention context so the current agent sees the exchange
3478
+ if (session.pendingMentionContexts && session.pendingMentionContexts.length > 0) {
3479
+ var mentionPrefix = session.pendingMentionContexts.join("\n\n");
3480
+ session.pendingMentionContexts = [];
3481
+ fullText = mentionPrefix + "\n\n" + fullText;
3482
+ }
3483
+
3471
3484
  if (!session.isProcessing) {
3472
3485
  session.isProcessing = true;
3473
3486
  onProcessingChanged();
@@ -3484,6 +3497,320 @@ function createProjectContext(opts) {
3484
3497
  sm.broadcastSessionList();
3485
3498
  }
3486
3499
 
3500
+ // --- @Mention handler ---
3501
+ var MENTION_WINDOW = 15; // turns to check for session continuity
3502
+
3503
+ function getRecentTurns(session, n) {
3504
+ var turns = [];
3505
+ var history = session.history;
3506
+ // Walk backwards through history, collect user/assistant/mention text turns
3507
+ var assistantBuffer = "";
3508
+ for (var i = history.length - 1; i >= 0 && turns.length < n; i--) {
3509
+ var entry = history[i];
3510
+ if (entry.type === "user_message") {
3511
+ if (assistantBuffer) {
3512
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3513
+ assistantBuffer = "";
3514
+ }
3515
+ turns.push({ role: "user", text: entry.text || "" });
3516
+ } else if (entry.type === "delta" || entry.type === "text") {
3517
+ assistantBuffer = (entry.text || "") + assistantBuffer;
3518
+ } else if (entry.type === "mention_response") {
3519
+ if (assistantBuffer) {
3520
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3521
+ assistantBuffer = "";
3522
+ }
3523
+ turns.push({ role: "@" + (entry.mateName || "Mate"), text: entry.text || "", mateId: entry.mateId });
3524
+ } else if (entry.type === "mention_user") {
3525
+ if (assistantBuffer) {
3526
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3527
+ assistantBuffer = "";
3528
+ }
3529
+ turns.push({ role: "user", text: "@" + (entry.mateName || "Mate") + " " + (entry.text || ""), mateId: entry.mateId });
3530
+ }
3531
+ }
3532
+ if (assistantBuffer) {
3533
+ turns.push({ role: "assistant", text: assistantBuffer.trim() });
3534
+ }
3535
+ turns.reverse();
3536
+ return turns;
3537
+ }
3538
+
3539
+ // Check if the given mate has a mention response in the recent window
3540
+ function hasMateInWindow(recentTurns, mateId) {
3541
+ for (var i = 0; i < recentTurns.length; i++) {
3542
+ if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
3543
+ return true;
3544
+ }
3545
+ }
3546
+ return false;
3547
+ }
3548
+
3549
+ // Build the "middle context": conversation turns since the mate's last response
3550
+ function buildMiddleContext(recentTurns, mateId) {
3551
+ // Find the last mention response from this mate
3552
+ var lastIdx = -1;
3553
+ for (var i = recentTurns.length - 1; i >= 0; i--) {
3554
+ if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
3555
+ lastIdx = i;
3556
+ break;
3557
+ }
3558
+ }
3559
+ if (lastIdx === -1 || lastIdx >= recentTurns.length - 1) return "";
3560
+
3561
+ // Collect turns after the last mention response
3562
+ var lines = ["[Conversation since your last response:]", "---"];
3563
+ for (var j = lastIdx + 1; j < recentTurns.length; j++) {
3564
+ var turn = recentTurns[j];
3565
+ lines.push(turn.role + ": " + turn.text);
3566
+ }
3567
+ lines.push("---");
3568
+ return lines.join("\n");
3569
+ }
3570
+
3571
+ function buildMentionContext(userName, recentTurns) {
3572
+ var lines = [
3573
+ "You were @mentioned in a project session by " + userName + ".",
3574
+ "You are responding inline in their conversation. Keep your response focused on what was asked.",
3575
+ "You have read-only access to the project files but cannot make changes.",
3576
+ "",
3577
+ "Recent conversation context:",
3578
+ "---",
3579
+ ];
3580
+ for (var i = 0; i < recentTurns.length; i++) {
3581
+ var turn = recentTurns[i];
3582
+ lines.push(turn.role + ": " + turn.text);
3583
+ }
3584
+ lines.push("---");
3585
+ return lines.join("\n");
3586
+ }
3587
+
3588
+ function digestMentionSession(session, mateId, mateCtx, mateResponse, userQuestion) {
3589
+ if (!session._mentionSessions || !session._mentionSessions[mateId]) return;
3590
+ var mentionSession = session._mentionSessions[mateId];
3591
+ if (!mentionSession.isAlive()) return;
3592
+
3593
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
3594
+ var knowledgeDir = path.join(mateDir, "knowledge");
3595
+
3596
+ var digestPrompt = [
3597
+ "[SYSTEM: Session Digest Request]",
3598
+ "The conversation has ended. Summarize this session from YOUR perspective for your long-term memory.",
3599
+ "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
3600
+ "Schema:",
3601
+ "{",
3602
+ ' "date": "YYYY-MM-DD",',
3603
+ ' "topic": "short topic description",',
3604
+ ' "my_position": "what I said/recommended",',
3605
+ ' "other_perspectives": "other mates or user perspectives if relevant",',
3606
+ ' "decisions": "what was decided, or null if pending",',
3607
+ ' "open_items": "what remains unresolved",',
3608
+ ' "user_sentiment": "how the user seemed to feel about this topic"',
3609
+ "}",
3610
+ "",
3611
+ "IMPORTANT: Output ONLY the JSON object. Nothing else.",
3612
+ ].join("\n");
3613
+
3614
+ var digestText = "";
3615
+ mentionSession.pushMessage(digestPrompt, {
3616
+ onActivity: function () {},
3617
+ onDelta: function (delta) {
3618
+ digestText += delta;
3619
+ },
3620
+ onDone: function () {
3621
+ var digestObj = null;
3622
+ try {
3623
+ var cleaned = digestText.trim();
3624
+ if (cleaned.indexOf("```") === 0) {
3625
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
3626
+ }
3627
+ digestObj = JSON.parse(cleaned);
3628
+ } catch (e) {
3629
+ console.error("[digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
3630
+ digestObj = {
3631
+ date: new Date().toISOString().slice(0, 10),
3632
+ topic: "parse_failed",
3633
+ raw: digestText.substring(0, 500),
3634
+ };
3635
+ }
3636
+
3637
+ try {
3638
+ fs.mkdirSync(knowledgeDir, { recursive: true });
3639
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
3640
+ fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
3641
+ } catch (e) {
3642
+ console.error("[digest] Failed to write digest for mate " + mateId + ":", e.message);
3643
+ }
3644
+ },
3645
+ onError: function (err) {
3646
+ console.error("[digest] Digest generation failed for mate " + mateId + ":", err);
3647
+ },
3648
+ });
3649
+ }
3650
+
3651
+ function handleMention(ws, msg) {
3652
+ if (!msg.mateId || !msg.text) return;
3653
+
3654
+ var session = getSessionForWs(ws);
3655
+ if (!session) return;
3656
+
3657
+ // Check if a mention is already in progress for this session
3658
+ if (session._mentionInProgress) {
3659
+ sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "A mention is already in progress." });
3660
+ return;
3661
+ }
3662
+
3663
+ var userId = ws._clayUser ? ws._clayUser.id : null;
3664
+ var mateCtx = matesModule.buildMateCtx(userId);
3665
+ var mate = matesModule.getMate(mateCtx, msg.mateId);
3666
+ if (!mate) {
3667
+ sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Mate not found" });
3668
+ return;
3669
+ }
3670
+
3671
+ var mateName = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
3672
+ var avatarColor = (mate.profile && mate.profile.avatarColor) || "#6c5ce7";
3673
+ var avatarStyle = (mate.profile && mate.profile.avatarStyle) || "bottts";
3674
+ var avatarSeed = (mate.profile && mate.profile.avatarSeed) || mate.id;
3675
+
3676
+ // Save mention user message to session history
3677
+ var mentionUserEntry = { type: "mention_user", text: msg.text, mateId: msg.mateId, mateName: mateName };
3678
+ session.history.push(mentionUserEntry);
3679
+ sm.appendToSessionFile(session, mentionUserEntry);
3680
+ sendToSessionOthers(ws, session.localId, mentionUserEntry);
3681
+
3682
+ // Extract recent turns for continuity check
3683
+ var recentTurns = getRecentTurns(session, MENTION_WINDOW);
3684
+
3685
+ // Determine user name for context
3686
+ var userName = "User";
3687
+ if (ws._clayUser) {
3688
+ var p = ws._clayUser.profile || {};
3689
+ userName = p.name || ws._clayUser.displayName || ws._clayUser.username || "User";
3690
+ }
3691
+
3692
+ session._mentionInProgress = true;
3693
+
3694
+ // Send mention start indicator
3695
+ sendToSession(session.localId, {
3696
+ type: "mention_start",
3697
+ mateId: msg.mateId,
3698
+ mateName: mateName,
3699
+ avatarColor: avatarColor,
3700
+ avatarStyle: avatarStyle,
3701
+ avatarSeed: avatarSeed,
3702
+ });
3703
+
3704
+ // Shared callbacks for both new and continued sessions
3705
+ var mentionCallbacks = {
3706
+ onActivity: function (activity) {
3707
+ sendToSession(session.localId, {
3708
+ type: "mention_activity",
3709
+ mateId: msg.mateId,
3710
+ activity: activity,
3711
+ });
3712
+ },
3713
+ onDelta: function (delta) {
3714
+ sendToSession(session.localId, {
3715
+ type: "mention_stream",
3716
+ mateId: msg.mateId,
3717
+ mateName: mateName,
3718
+ delta: delta,
3719
+ });
3720
+ },
3721
+ onDone: function (fullText) {
3722
+ session._mentionInProgress = false;
3723
+
3724
+ // Save mention response to session history
3725
+ var mentionResponseEntry = {
3726
+ type: "mention_response",
3727
+ mateId: msg.mateId,
3728
+ mateName: mateName,
3729
+ text: fullText,
3730
+ avatarColor: avatarColor,
3731
+ avatarStyle: avatarStyle,
3732
+ avatarSeed: avatarSeed,
3733
+ };
3734
+ session.history.push(mentionResponseEntry);
3735
+ sm.appendToSessionFile(session, mentionResponseEntry);
3736
+
3737
+ // Queue mention context for injection into the current agent's next turn
3738
+ if (!session.pendingMentionContexts) session.pendingMentionContexts = [];
3739
+ session.pendingMentionContexts.push(
3740
+ "[Context: @" + mateName + " was mentioned and responded]\n\n" +
3741
+ "User asked @" + mateName + ": " + msg.text + "\n" +
3742
+ mateName + " responded: " + fullText + "\n\n" +
3743
+ "[End of @mention context. This is for your reference only. Do not re-execute or repeat this response.]"
3744
+ );
3745
+
3746
+ sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
3747
+
3748
+ // Generate session digest for mate's long-term memory
3749
+ digestMentionSession(session, msg.mateId, mateCtx, fullText, msg.text);
3750
+ },
3751
+ onError: function (errMsg) {
3752
+ session._mentionInProgress = false;
3753
+ // Clean up dead session
3754
+ if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
3755
+ delete session._mentionSessions[msg.mateId];
3756
+ }
3757
+ console.error("[mention] Error for mate " + msg.mateId + ":", errMsg);
3758
+ sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: errMsg });
3759
+ },
3760
+ };
3761
+
3762
+ // Initialize mention sessions map if needed
3763
+ if (!session._mentionSessions) session._mentionSessions = {};
3764
+
3765
+ // Session continuity: check if this mate has a response in the recent window
3766
+ var existingSession = session._mentionSessions[msg.mateId];
3767
+ var canContinue = existingSession && existingSession.isAlive() && hasMateInWindow(recentTurns, msg.mateId);
3768
+
3769
+ if (canContinue) {
3770
+ // Continue existing mention session with middle context
3771
+ var middleContext = buildMiddleContext(recentTurns, msg.mateId);
3772
+ var continuationText = middleContext ? middleContext + "\n\n" + msg.text : msg.text;
3773
+ existingSession.pushMessage(continuationText, mentionCallbacks);
3774
+ } else {
3775
+ // Clean up old session if it exists
3776
+ if (existingSession) {
3777
+ existingSession.close();
3778
+ delete session._mentionSessions[msg.mateId];
3779
+ }
3780
+
3781
+ // Load Mate CLAUDE.md
3782
+ var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
3783
+ var claudeMd = "";
3784
+ try {
3785
+ claudeMd = fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
3786
+ } catch (e) {
3787
+ // CLAUDE.md may not exist for new mates
3788
+ }
3789
+
3790
+ // Build initial mention context
3791
+ var mentionContext = buildMentionContext(userName, recentTurns);
3792
+
3793
+ // Create new persistent mention session
3794
+ sdk.createMentionSession({
3795
+ claudeMd: claudeMd,
3796
+ initialContext: mentionContext,
3797
+ initialMessage: msg.text,
3798
+ onActivity: mentionCallbacks.onActivity,
3799
+ onDelta: mentionCallbacks.onDelta,
3800
+ onDone: mentionCallbacks.onDone,
3801
+ onError: mentionCallbacks.onError,
3802
+ }).then(function (mentionSession) {
3803
+ if (mentionSession) {
3804
+ session._mentionSessions[msg.mateId] = mentionSession;
3805
+ }
3806
+ }).catch(function (err) {
3807
+ session._mentionInProgress = false;
3808
+ console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
3809
+ sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
3810
+ });
3811
+ }
3812
+ }
3813
+
3487
3814
  // --- Session presence (who is viewing which session) ---
3488
3815
  function broadcastPresence() {
3489
3816
  if (!usersModule.isMultiUser()) return;
@@ -4056,7 +4383,7 @@ function createProjectContext(opts) {
4056
4383
  loopRegistry.stopTimer();
4057
4384
  stopFileWatch();
4058
4385
  stopAllDirWatches();
4059
- // Abort all active sessions
4386
+ // Abort all active sessions and clean up mention sessions
4060
4387
  sm.sessions.forEach(function (session) {
4061
4388
  session.destroying = true;
4062
4389
  if (session.abortController) {
@@ -4065,6 +4392,14 @@ function createProjectContext(opts) {
4065
4392
  if (session.messageQueue) {
4066
4393
  try { session.messageQueue.end(); } catch (e) {}
4067
4394
  }
4395
+ // Close all mention SDK sessions to prevent zombie processes
4396
+ if (session._mentionSessions) {
4397
+ var mateIds = Object.keys(session._mentionSessions);
4398
+ for (var mi = 0; mi < mateIds.length; mi++) {
4399
+ try { session._mentionSessions[mateIds[mi]].close(); } catch (e) {}
4400
+ }
4401
+ session._mentionSessions = {};
4402
+ }
4068
4403
  });
4069
4404
  // Kill all terminals
4070
4405
  tm.destroyAll();
@@ -4149,6 +4484,7 @@ function createProjectContext(opts) {
4149
4484
  var claudeMdPath = path.join(cwd, "CLAUDE.md");
4150
4485
  // Enforce immediately on startup
4151
4486
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
4487
+ try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
4152
4488
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
4153
4489
  // Watch for changes
4154
4490
  try {
@@ -4157,6 +4493,7 @@ function createProjectContext(opts) {
4157
4493
  crisisDebounce = setTimeout(function () {
4158
4494
  crisisDebounce = null;
4159
4495
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
4496
+ try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
4160
4497
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
4161
4498
  }, 500);
4162
4499
  });
package/lib/public/app.js CHANGED
@@ -28,6 +28,7 @@ import { initTooltips, registerTooltip } from './modules/tooltip.js';
28
28
  import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } from './modules/mate-wizard.js';
29
29
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
30
30
  import { initLongPress } from './modules/longpress.js';
31
+ import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
31
32
 
32
33
  // --- Base path for multi-project routing ---
33
34
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -693,9 +694,9 @@ import { initLongPress } from './modules/longpress.js';
693
694
  }
694
695
  }
695
696
 
696
- // Hide user-island (my avatar behind it becomes visible)
697
+ // Hide user-island in human DM, keep visible in Mate DM
697
698
  var userIsland = document.getElementById("user-island");
698
- if (userIsland) userIsland.classList.add("dm-hidden");
699
+ if (userIsland && !isMate) userIsland.classList.add("dm-hidden");
699
700
 
700
701
  // Render DM messages
701
702
  messagesEl.innerHTML = "";
@@ -2121,18 +2122,9 @@ import { initLongPress } from './modules/longpress.js';
2121
2122
  if (!activityEl) {
2122
2123
  activityEl = document.createElement("div");
2123
2124
  activityEl.className = "activity-inline";
2124
- var isMateDmActive = dmMode && dmTargetUser && dmTargetUser.isMate;
2125
- if (isMateDmActive) {
2126
- activityEl.classList.add("mate-activity");
2127
- var mateAvUrl = document.body.dataset.mateAvatarUrl || "";
2128
- activityEl.innerHTML =
2129
- '<img class="mate-activity-avatar" src="' + mateAvUrl + '" alt="">' +
2130
- '<span class="activity-text"></span>';
2131
- } else {
2132
- activityEl.innerHTML =
2133
- '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
2134
- '<span class="activity-text"></span>';
2135
- }
2125
+ activityEl.innerHTML =
2126
+ '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
2127
+ '<span class="activity-text"></span>';
2136
2128
  addToMessages(activityEl);
2137
2129
  refreshIcons();
2138
2130
  }
@@ -4638,6 +4630,38 @@ import { initLongPress } from './modules/longpress.js';
4638
4630
  showToast(msg.error || "Mate operation failed", "error");
4639
4631
  break;
4640
4632
 
4633
+ // --- @Mention ---
4634
+ case "mention_start":
4635
+ handleMentionStart(msg);
4636
+ break;
4637
+
4638
+ case "mention_activity":
4639
+ handleMentionActivity(msg);
4640
+ break;
4641
+
4642
+ case "mention_stream":
4643
+ handleMentionStream(msg);
4644
+ break;
4645
+
4646
+ case "mention_done":
4647
+ handleMentionDone(msg);
4648
+ break;
4649
+
4650
+ case "mention_error":
4651
+ handleMentionError(msg);
4652
+ if (msg.error) showToast("@Mention: " + msg.error, "error");
4653
+ break;
4654
+
4655
+ case "mention_user":
4656
+ // History replay: render mention user message from another client
4657
+ renderMentionUser(msg);
4658
+ break;
4659
+
4660
+ case "mention_response":
4661
+ // History replay: render mention response from another client
4662
+ renderMentionResponse(msg);
4663
+ break;
4664
+
4641
4665
  case "daemon_config":
4642
4666
  updateDaemonConfig(msg.config);
4643
4667
  break;
@@ -4958,6 +4982,18 @@ import { initLongPress } from './modules/longpress.js';
4958
4982
  showMatePreThinking: function () { showMatePreThinking(); },
4959
4983
  });
4960
4984
 
4985
+ // --- @Mention module ---
4986
+ initMention({
4987
+ get ws() { return ws; },
4988
+ get connected() { return connected; },
4989
+ inputEl: inputEl,
4990
+ messagesEl: messagesEl,
4991
+ matesList: function () { return cachedMatesList || []; },
4992
+ scrollToBottom: scrollToBottom,
4993
+ addUserMessage: addUserMessage,
4994
+ addCopyHandler: addCopyHandler,
4995
+ });
4996
+
4961
4997
  // --- STT module (voice input via Web Speech API) ---
4962
4998
  initSTT({
4963
4999
  inputEl: inputEl,
@@ -201,6 +201,7 @@ html, body {
201
201
  flex-direction: column;
202
202
  height: 100%;
203
203
  height: 100dvh;
204
+ position: relative;
204
205
  }
205
206
 
206
207
  #layout-body {
@@ -629,6 +629,11 @@
629
629
  #input-area {
630
630
  padding-bottom: calc(var(--safe-bottom) + 56px + 8px);
631
631
  }
632
+
633
+ /* When keyboard is open, tab bar is hidden behind keyboard — remove the offset */
634
+ body.keyboard-open #input-area {
635
+ padding-bottom: 8px;
636
+ }
632
637
  }
633
638
 
634
639
  /* ==========================================================================
@@ -1958,10 +1958,7 @@ body.mate-dm-active .mate-permission.resolved .permission-decision-label {
1958
1958
  color: var(--text-dimmer);
1959
1959
  }
1960
1960
 
1961
- /* --- Mate Activity: avatar + text (hidden, dots row is enough) --- */
1962
- body.mate-dm-active .activity-inline {
1963
- display: none;
1964
- }
1961
+ /* --- Mate Activity: avatar + text --- */
1965
1962
 
1966
1963
  /* --- Mate AskUserQuestion: avatar + content layout (matches msg-assistant) --- */
1967
1964
  body.mate-dm-active .mate-ask-user {
@@ -2006,10 +2003,8 @@ body.mate-dm-active .subagent-log,
2006
2003
  body.mate-dm-active .conflict-msg,
2007
2004
  body.mate-dm-active .context-overflow-msg,
2008
2005
  body.mate-dm-active .sys-msg,
2009
- body.mate-dm-active .activity-inline {
2010
- padding-left: 60px;
2011
- margin-left: 0;
2012
- margin-right: 0;
2006
+ body.mate-dm-active .activity-inline:not(.mention-activity-bar) {
2007
+ display: none;
2013
2008
  }
2014
2009
 
2015
2010
  /* ==========================================================================