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 +2 -2
- package/lib/mates.js +85 -6
- package/lib/project.js +409 -4
- package/lib/public/app.js +307 -18
- package/lib/public/css/base.css +1 -0
- package/lib/public/css/input.css +5 -0
- package/lib/public/css/mates.css +5 -6
- 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 +41 -0
- package/lib/public/modules/mention.js +652 -0
- package/lib/public/modules/notifications.js +9 -3
- package/lib/public/modules/scheduler.js +47 -14
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +191 -0
- package/lib/sessions.js +13 -0
- package/package.json +1 -1
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
|
|
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");
|
|
@@ -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
|
});
|