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 +85 -6
- package/lib/project.js +339 -2
- package/lib/public/app.js +50 -14
- package/lib/public/css/base.css +1 -0
- package/lib/public/css/input.css +5 -0
- package/lib/public/css/mates.css +3 -8
- package/lib/public/css/mention.css +226 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/index.html +1 -0
- package/lib/public/modules/input.js +40 -0
- package/lib/public/modules/mention.js +627 -0
- package/lib/public/modules/notifications.js +9 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +185 -0
- package/lib/sessions.js +13 -0
- package/package.json +1 -1
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
|
|
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 (
|
|
307
|
-
existing = afterTeam.substring(0,
|
|
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 =
|
|
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() +
|
|
398
|
+
content = content.substring(0, crisisPos).trimEnd() + SESSION_MEMORY_SECTION + "\n\n" + content.substring(crisisPos);
|
|
322
399
|
} else {
|
|
323
|
-
content = content.trimEnd() +
|
|
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
|
|
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
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
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,
|
package/lib/public/css/base.css
CHANGED
package/lib/public/css/input.css
CHANGED
|
@@ -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
|
/* ==========================================================================
|
package/lib/public/css/mates.css
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
/* ==========================================================================
|