clay-server 2.26.0-beta.3 → 2.26.0-beta.5

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/project.js CHANGED
@@ -16,8 +16,36 @@ var matesModule = require("./mates");
16
16
  var sessionSearch = require("./session-search");
17
17
  var userPresence = require("./user-presence");
18
18
  var { attachDebate } = require("./project-debate");
19
+ var { attachMemory } = require("./project-memory");
20
+ var { attachMateInteraction } = require("./project-mate-interaction");
19
21
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
20
22
 
23
+ // --- Context Sources persistence ---
24
+ var _ctxSrcConfig = require("./config");
25
+ var _ctxSrcDir = path.join(_ctxSrcConfig.CONFIG_DIR, "context-sources");
26
+
27
+ function loadContextSources(slug) {
28
+ try {
29
+ var filePath = path.join(_ctxSrcDir, slug + ".json");
30
+ var data = JSON.parse(fs.readFileSync(filePath, "utf8"));
31
+ return data.active || [];
32
+ } catch (e) {
33
+ return [];
34
+ }
35
+ }
36
+
37
+ function saveContextSources(slug, activeIds) {
38
+ try {
39
+ if (!fs.existsSync(_ctxSrcDir)) {
40
+ fs.mkdirSync(_ctxSrcDir, { recursive: true });
41
+ }
42
+ var filePath = path.join(_ctxSrcDir, slug + ".json");
43
+ fs.writeFileSync(filePath, JSON.stringify({ active: activeIds }), "utf8");
44
+ } catch (e) {
45
+ console.error("[context-sources] Failed to save:", e.message);
46
+ }
47
+ }
48
+
21
49
  // Validate environment variable string (KEY=VALUE per line)
22
50
  // Returns null if valid, or an error string if invalid
23
51
  function validateEnvString(str) {
@@ -1235,6 +1263,7 @@ function createProjectContext(opts) {
1235
1263
  }
1236
1264
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1237
1265
  sendTo(ws, { type: "term_list", terminals: tm.list() });
1266
+ sendTo(ws, { type: "context_sources_state", active: loadContextSources(slug) });
1238
1267
  sendTo(ws, { type: "notes_list", notes: nm.list() });
1239
1268
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
1240
1269
 
@@ -1383,7 +1412,19 @@ function createProjectContext(opts) {
1383
1412
  }
1384
1413
  sendTo(ws, hydrateImageRefs(_hitem));
1385
1414
  }
1386
- sendTo(ws, { type: "history_done" });
1415
+ // Include last result data + cached context usage for accurate restore
1416
+ var _lastUsage = null, _lastModelUsage = null, _lastCost = null, _lastStreamInputTokens = null;
1417
+ for (var _ri = total - 1; _ri >= 0; _ri--) {
1418
+ if (active.history[_ri].type === "result") {
1419
+ var _r = active.history[_ri];
1420
+ _lastUsage = _r.usage || null;
1421
+ _lastModelUsage = _r.modelUsage || null;
1422
+ _lastCost = _r.cost != null ? _r.cost : null;
1423
+ _lastStreamInputTokens = _r.lastStreamInputTokens || null;
1424
+ break;
1425
+ }
1426
+ }
1427
+ sendTo(ws, { type: "history_done", lastUsage: _lastUsage, lastModelUsage: _lastModelUsage, lastCost: _lastCost, lastStreamInputTokens: _lastStreamInputTokens, contextUsage: active.lastContextUsage || null });
1387
1428
 
1388
1429
  if (active.isProcessing) {
1389
1430
  sendTo(ws, { type: "status", status: "processing" });
@@ -1694,80 +1735,10 @@ function createProjectContext(opts) {
1694
1735
  return;
1695
1736
  }
1696
1737
 
1697
- // --- Memory (session digests) management ---
1698
- if (msg.type === "memory_list") {
1699
- var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1700
- var summaryFile = path.join(cwd, "knowledge", "memory-summary.md");
1701
- var entries = [];
1702
- var summary = "";
1703
- try {
1704
- var raw = fs.readFileSync(digestFile, "utf8").trim();
1705
- if (raw) {
1706
- var lines = raw.split("\n");
1707
- for (var mi = 0; mi < lines.length; mi++) {
1708
- try {
1709
- var obj = JSON.parse(lines[mi]);
1710
- obj.index = mi;
1711
- entries.push(obj);
1712
- } catch (e) {}
1713
- }
1714
- }
1715
- } catch (e) { /* file may not exist */ }
1716
- try {
1717
- if (fs.existsSync(summaryFile)) {
1718
- summary = fs.readFileSync(summaryFile, "utf8").trim();
1719
- }
1720
- } catch (e) {}
1721
- // Return newest first
1722
- entries.reverse();
1723
- sendTo(ws, { type: "memory_list", entries: entries, summary: summary });
1724
- return;
1725
- }
1726
-
1727
- if (msg.type === "memory_search") {
1728
- if (!msg.query || typeof msg.query !== "string") {
1729
- sendTo(ws, { type: "memory_search_results", results: [], query: "" });
1730
- return;
1731
- }
1732
- var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1733
- try {
1734
- var results = sessionSearch.searchDigests(digestFile, msg.query, {
1735
- maxResults: msg.maxResults || 10,
1736
- minScore: msg.minScore || 0.5,
1737
- dateFrom: msg.dateFrom || null,
1738
- dateTo: msg.dateTo || null
1739
- });
1740
- sendTo(ws, {
1741
- type: "memory_search_results",
1742
- results: sessionSearch.formatForMemoryUI(results),
1743
- query: msg.query
1744
- });
1745
- } catch (e) {
1746
- console.error("[session-search] Search failed:", e.message);
1747
- sendTo(ws, { type: "memory_search_results", results: [], query: msg.query });
1748
- }
1749
- return;
1750
- }
1751
-
1752
- if (msg.type === "memory_delete") {
1753
- if (typeof msg.index !== "number") return;
1754
- var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1755
- try {
1756
- var raw = fs.readFileSync(digestFile, "utf8").trim();
1757
- var lines = raw ? raw.split("\n") : [];
1758
- if (msg.index >= 0 && msg.index < lines.length) {
1759
- lines.splice(msg.index, 1);
1760
- if (lines.length === 0) {
1761
- fs.unlinkSync(digestFile);
1762
- } else {
1763
- fs.writeFileSync(digestFile, lines.join("\n") + "\n");
1764
- }
1765
- }
1766
- } catch (e) {}
1767
- sendTo(ws, { type: "memory_deleted", index: msg.index });
1768
- handleMessage(ws, { type: "memory_list" });
1769
- return;
1770
- }
1738
+ // --- Memory (session digests) management (delegated to project-memory.js) ---
1739
+ if (msg.type === "memory_list") { _memory.handleMemoryList(ws); return; }
1740
+ if (msg.type === "memory_search") { _memory.handleMemorySearch(ws, msg); return; }
1741
+ if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
1771
1742
 
1772
1743
  if (msg.type === "push_subscribe") {
1773
1744
  var _pushUserId = ws._clayUser ? ws._clayUser.id : null;
@@ -3374,6 +3345,14 @@ function createProjectContext(opts) {
3374
3345
  if (msg.id) {
3375
3346
  tm.close(msg.id);
3376
3347
  send({ type: "term_list", terminals: tm.list() });
3348
+ // Remove closed terminal from context sources
3349
+ var saved = loadContextSources(slug);
3350
+ var termKey = "term:" + msg.id;
3351
+ var filtered = saved.filter(function(id) { return id !== termKey; });
3352
+ if (filtered.length !== saved.length) {
3353
+ saveContextSources(slug, filtered);
3354
+ send({ type: "context_sources_state", active: filtered });
3355
+ }
3377
3356
  }
3378
3357
  return;
3379
3358
  }
@@ -3386,6 +3365,13 @@ function createProjectContext(opts) {
3386
3365
  return;
3387
3366
  }
3388
3367
 
3368
+ // --- Context Sources ---
3369
+ if (msg.type === "context_sources_save") {
3370
+ var activeIds = msg.active || [];
3371
+ saveContextSources(slug, activeIds);
3372
+ return;
3373
+ }
3374
+
3389
3375
  // --- Scheduled tasks permission gate ---
3390
3376
  if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
3391
3377
  msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
@@ -3844,6 +3830,91 @@ function createProjectContext(opts) {
3844
3830
  fullText = mentionPrefix + "\n\n" + fullText;
3845
3831
  }
3846
3832
 
3833
+ // Inject active terminal context sources (delta only: send new output since last message)
3834
+ var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
3835
+ var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
3836
+ var TERM_TAIL_SIZE = 6144; // keep last 6KB for recent state
3837
+ var ctxSources = loadContextSources(slug);
3838
+ if (ctxSources.length > 0) {
3839
+ if (!session._termContextCursors) session._termContextCursors = {};
3840
+ var termContextParts = [];
3841
+ for (var ci = 0; ci < ctxSources.length; ci++) {
3842
+ var srcId = ctxSources[ci];
3843
+ if (srcId.startsWith("term:")) {
3844
+ var termId = parseInt(srcId.split(":")[1], 10);
3845
+ var sb = tm.getScrollback(termId);
3846
+ if (sb) {
3847
+ var lastCursor;
3848
+ if (termId in session._termContextCursors) {
3849
+ lastCursor = session._termContextCursors[termId];
3850
+ // Terminal was recycled (closed and reopened with same ID) — reset cursor
3851
+ if (lastCursor > sb.totalBytesWritten) lastCursor = 0;
3852
+ } else {
3853
+ // First time seeing this terminal — include last 8KB (what user can see now)
3854
+ lastCursor = Math.max(0, sb.totalBytesWritten - TERM_CONTEXT_MAX);
3855
+ }
3856
+ var newBytes = sb.totalBytesWritten - lastCursor;
3857
+ session._termContextCursors[termId] = sb.totalBytesWritten;
3858
+ if (newBytes <= 0) continue;
3859
+ // Build timestamped delta from chunks
3860
+ var deltaChunks = [];
3861
+ var bytePos = sb.bufferStart;
3862
+ for (var chunkIdx = 0; chunkIdx < sb.chunks.length; chunkIdx++) {
3863
+ var chunk = sb.chunks[chunkIdx];
3864
+ var chunkEnd = bytePos + chunk.data.length;
3865
+ if (chunkEnd > lastCursor) {
3866
+ // This chunk has new content
3867
+ var chunkData = chunk.data;
3868
+ if (bytePos < lastCursor) {
3869
+ // Partial chunk: only the part after lastCursor
3870
+ chunkData = chunkData.slice(lastCursor - bytePos);
3871
+ }
3872
+ deltaChunks.push({ ts: chunk.ts, data: chunkData });
3873
+ }
3874
+ bytePos = chunkEnd;
3875
+ }
3876
+ if (deltaChunks.length === 0) continue;
3877
+ // Format with timestamps: group by second to avoid excessive timestamps
3878
+ var lines = [];
3879
+ var lastTimeSec = 0;
3880
+ for (var di = 0; di < deltaChunks.length; di++) {
3881
+ var dc = deltaChunks[di];
3882
+ var cleaned = dc.data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
3883
+ if (!cleaned) continue;
3884
+ var timeSec = Math.floor(dc.ts / 1000);
3885
+ if (timeSec !== lastTimeSec) {
3886
+ var d = new Date(dc.ts);
3887
+ var timeStr = d.toTimeString().slice(0, 8); // HH:MM:SS
3888
+ lines.push("[" + timeStr + "] " + cleaned);
3889
+ lastTimeSec = timeSec;
3890
+ } else {
3891
+ lines.push(cleaned);
3892
+ }
3893
+ }
3894
+ var delta = lines.join("").trim();
3895
+ if (!delta) continue;
3896
+ var termInfo = tm.list().find(function(t) { return t.id === termId; });
3897
+ var termTitle = termInfo ? termInfo.title : "Terminal " + termId;
3898
+ var header;
3899
+ if (delta.length > TERM_CONTEXT_MAX) {
3900
+ var head = delta.slice(0, TERM_HEAD_SIZE);
3901
+ var tail = delta.slice(-TERM_TAIL_SIZE);
3902
+ var omittedBytes = delta.length - TERM_HEAD_SIZE - TERM_TAIL_SIZE;
3903
+ var omittedLines = delta.slice(TERM_HEAD_SIZE, delta.length - TERM_TAIL_SIZE).split("\n").length;
3904
+ delta = head + "\n\n... (" + omittedLines + " lines / " + Math.round(omittedBytes / 1024) + "KB omitted) ...\n\n" + tail;
3905
+ header = "[New terminal output from " + termTitle + " (large output, head+tail shown)]";
3906
+ } else {
3907
+ header = "[New terminal output from " + termTitle + "]";
3908
+ }
3909
+ termContextParts.push(header + "\n```\n" + delta + "\n```");
3910
+ }
3911
+ }
3912
+ }
3913
+ if (termContextParts.length > 0) {
3914
+ fullText = termContextParts.join("\n\n") + "\n\n" + fullText;
3915
+ }
3916
+ }
3917
+
3847
3918
  if (!session.isProcessing) {
3848
3919
  session.isProcessing = true;
3849
3920
  onProcessingChanged();
@@ -3863,1307 +3934,57 @@ function createProjectContext(opts) {
3863
3934
  sm.broadcastSessionList();
3864
3935
  }
3865
3936
 
3866
- // --- @Mention handler ---
3867
- var MENTION_WINDOW = 20; // turns to check for session continuity
3868
-
3869
- function getRecentTurns(session, n) {
3870
- var turns = [];
3871
- var history = session.history;
3872
- // Walk backwards through history, collect user/assistant/mention text turns
3873
- var assistantBuffer = "";
3874
- for (var i = history.length - 1; i >= 0 && turns.length < n; i--) {
3875
- var entry = history[i];
3876
- if (entry.type === "user_message") {
3877
- if (assistantBuffer) {
3878
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
3879
- assistantBuffer = "";
3880
- }
3881
- turns.push({ role: "user", text: entry.text || "" });
3882
- } else if (entry.type === "delta" || entry.type === "text") {
3883
- assistantBuffer = (entry.text || "") + assistantBuffer;
3884
- } else if (entry.type === "mention_response") {
3885
- if (assistantBuffer) {
3886
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
3887
- assistantBuffer = "";
3888
- }
3889
- turns.push({ role: "@" + (entry.mateName || "Mate"), text: entry.text || "", mateId: entry.mateId });
3890
- } else if (entry.type === "mention_user") {
3891
- if (assistantBuffer) {
3892
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
3893
- assistantBuffer = "";
3894
- }
3895
- turns.push({ role: "user", text: "@" + (entry.mateName || "Mate") + " " + (entry.text || ""), mateId: entry.mateId });
3896
- }
3897
- }
3898
- if (assistantBuffer) {
3899
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
3900
- }
3901
- turns.reverse();
3902
- return turns;
3903
- }
3904
-
3905
- // Check if the given mate has a mention response in the recent window
3906
- function hasMateInWindow(recentTurns, mateId) {
3907
- for (var i = 0; i < recentTurns.length; i++) {
3908
- if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
3909
- return true;
3910
- }
3911
- }
3912
- return false;
3913
- }
3914
-
3915
- // Build the "middle context": conversation turns since the mate's last response
3916
- function buildMiddleContext(recentTurns, mateId) {
3917
- // Find the last mention response from this mate
3918
- var lastIdx = -1;
3919
- for (var i = recentTurns.length - 1; i >= 0; i--) {
3920
- if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
3921
- lastIdx = i;
3922
- break;
3923
- }
3924
- }
3925
- if (lastIdx === -1 || lastIdx >= recentTurns.length - 1) return "";
3926
-
3927
- // Collect turns after the last mention response
3928
- var lines = ["[Conversation since your last response:]", "---"];
3929
- for (var j = lastIdx + 1; j < recentTurns.length; j++) {
3930
- var turn = recentTurns[j];
3931
- lines.push(turn.role + ": " + turn.text);
3932
- }
3933
- lines.push("---");
3934
- return lines.join("\n");
3935
- }
3936
-
3937
- function buildMentionContext(userName, recentTurns) {
3938
- var lines = [
3939
- "You were @mentioned in a project session by " + userName + ".",
3940
- "You are responding inline in their conversation. Keep your response focused on what was asked.",
3941
- "You have read-only access to the project files but cannot make changes.",
3942
- "",
3943
- "Recent conversation context:",
3944
- "---",
3945
- ];
3946
- for (var i = 0; i < recentTurns.length; i++) {
3947
- var turn = recentTurns[i];
3948
- lines.push(turn.role + ": " + turn.text);
3949
- }
3950
- lines.push("---");
3951
- return lines.join("\n");
3952
- }
3953
-
3954
- // --- Shared digest worker: one reusable Haiku session for gate+digest ---
3955
- // Combines gate check and digest generation into a single prompt,
3956
- // processes jobs sequentially from a queue, reuses the session across calls.
3957
- // Session is recycled after DIGEST_WORKER_MAX_TURNS to prevent context bloat.
3958
- var _digestWorker = null;
3959
- var _digestQueue = [];
3960
- var _digestBusy = false;
3961
- var _digestWorkerTurns = 0;
3962
- var DIGEST_WORKER_MAX_TURNS = 20;
3963
-
3964
- function enqueueDigest(job) {
3965
- _digestQueue.push(job);
3966
- if (!_digestBusy) processDigestQueue();
3967
- }
3968
-
3969
- function processDigestQueue() {
3970
- if (_digestQueue.length === 0) { _digestBusy = false; return; }
3971
- _digestBusy = true;
3972
- var job = _digestQueue.shift();
3973
-
3974
- var mateDir = matesModule.getMateDir(job.mateCtx, job.mateId);
3975
- var knowledgeDir = path.join(mateDir, "knowledge");
3976
-
3977
- // Load mate role for gate context
3978
- var mateRole = "";
3979
- try {
3980
- var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
3981
- var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
3982
- if (roleMatch) mateRole = roleMatch[1].trim();
3983
- } catch (e) {}
3984
-
3985
- // Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
3986
- var prompt = [
3987
- "[SYSTEM: Memory Gate + Digest]",
3988
- "You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
3989
- "",
3990
- "Conversation (" + job.type + "):",
3991
- job.conversationContent,
3992
- "",
3993
- "STEP 1: Should this be saved to memory?",
3994
- 'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
3995
- "When in doubt, save it.",
3996
- "",
3997
- 'STEP 2: If yes, output a JSON digest. If no, output exactly: {"skip":true}',
3998
- "",
3999
- "JSON schema (output ONLY the JSON, no markdown, no fences):",
4000
- "{",
4001
- ' "date": "YYYY-MM-DD",',
4002
- ' "type": "' + job.type + '",',
4003
- ' "topic": "short topic description",',
4004
- ' "summary": "2-3 sentence summary",',
4005
- ' "key_quotes": ["user quotes, verbatim, max 5"],',
4006
- ' "user_context": "personal/project context or null",',
4007
- ' "my_position": "what I said/recommended",',
4008
- job.type === "dm" ? ' "user_intent": "what the user wanted",' : ' "other_perspectives": "key points from others",',
4009
- ' "decisions": "what was decided or null",',
4010
- ' "open_items": "what remains unresolved",',
4011
- ' "user_sentiment": "how user felt",',
4012
- ' "confidence": "high|medium|low",',
4013
- ' "revisit_later": true/false,',
4014
- ' "tags": ["topic", "tags"],',
4015
- ' "user_observations": [{"category":"pattern|decision|reaction|preference","observation":"...","evidence":"..."}]',
4016
- "}",
4017
- "",
4018
- "user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
4019
- "Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
4020
- "Omit the field entirely if nothing notable about the user.",
4021
- ].join("\n");
4022
-
4023
- function handleResult(text) {
4024
- var cleaned = text.trim();
4025
- if (cleaned.indexOf("```") === 0) {
4026
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
4027
- }
4028
-
4029
- var digestObj = null;
4030
- try { digestObj = JSON.parse(cleaned); } catch (e) {
4031
- console.error("[digest-worker] Parse failed for " + job.mateId + ":", e.message);
4032
- digestObj = { date: new Date().toISOString().slice(0, 10), topic: "parse_failed", raw: text.substring(0, 500) };
4033
- }
4034
-
4035
- if (digestObj && digestObj.skip) {
4036
- console.log("[digest-worker] Gate declined for " + job.mateId);
4037
- if (job.onDone) job.onDone();
4038
- processDigestQueue();
4039
- return;
4040
- }
4041
-
4042
- try {
4043
- fs.mkdirSync(knowledgeDir, { recursive: true });
4044
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4045
- fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
4046
- } catch (e) {
4047
- console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
4048
- }
4049
-
4050
- // Write user observations if present
4051
- if (digestObj.user_observations && digestObj.user_observations.length > 0) {
4052
- try {
4053
- var obsFile = path.join(knowledgeDir, "user-observations.jsonl");
4054
- var obsMate = matesModule.getMate(job.mateCtx, job.mateId);
4055
- var obsMateName = (obsMate && obsMate.name) || job.mateId;
4056
- var obsLines = [];
4057
- for (var oi = 0; oi < digestObj.user_observations.length; oi++) {
4058
- var obs = digestObj.user_observations[oi];
4059
- obsLines.push(JSON.stringify({
4060
- date: digestObj.date || new Date().toISOString().slice(0, 10),
4061
- category: obs.category || "pattern",
4062
- observation: obs.observation || "",
4063
- evidence: obs.evidence || "",
4064
- confidence: digestObj.confidence || "medium",
4065
- mateName: obsMateName,
4066
- mateId: job.mateId
4067
- }));
4068
- }
4069
- fs.appendFileSync(obsFile, obsLines.join("\n") + "\n");
4070
- } catch (e) {
4071
- console.error("[digest-worker] Observations write failed for " + job.mateId + ":", e.message);
4072
- }
4073
- }
4074
-
4075
- updateMemorySummary(job.mateCtx, job.mateId, digestObj);
4076
- maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
4077
- if (job.onDone) job.onDone();
4078
- processDigestQueue();
4079
- }
4080
-
4081
- // Recycle worker session if it has exceeded max turns
4082
- if (_digestWorker && _digestWorkerTurns >= DIGEST_WORKER_MAX_TURNS) {
4083
- try { _digestWorker.close(); } catch (e) {}
4084
- _digestWorker = null;
4085
- _digestWorkerTurns = 0;
4086
- }
4087
-
4088
- var responseText = "";
4089
- if (_digestWorker && _digestWorker.isAlive()) {
4090
- _digestWorkerTurns++;
4091
- _digestWorker.pushMessage(prompt, {
4092
- onActivity: function () {},
4093
- onDelta: function (d) { responseText += d; },
4094
- onDone: function () { handleResult(responseText); },
4095
- onError: function (err) {
4096
- console.error("[digest-worker] Error:", err);
4097
- _digestWorker = null;
4098
- _digestWorkerTurns = 0;
4099
- if (job.onDone) job.onDone();
4100
- processDigestQueue();
4101
- },
4102
- });
4103
- } else {
4104
- sdk.createMentionSession({
4105
- claudeMd: "",
4106
- model: "haiku",
4107
- initialContext: "[Digest Worker] You generate memory digests. Respond with ONLY JSON.",
4108
- initialMessage: prompt,
4109
- onActivity: function () {},
4110
- onDelta: function (d) { responseText += d; },
4111
- onDone: function () { handleResult(responseText); },
4112
- onError: function (err) {
4113
- console.error("[digest-worker] Create error:", err);
4114
- _digestWorker = null;
4115
- if (job.onDone) job.onDone();
4116
- processDigestQueue();
4117
- },
4118
- }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
4119
- if (job.onDone) job.onDone();
4120
- processDigestQueue();
4121
- });
4122
- }
4123
- }
4124
-
4125
- function digestMentionSession(session, mateId, mateCtx, mateResponse, userQuestion) {
4126
- if (!session._mentionSessions || !session._mentionSessions[mateId]) return;
4127
- var mentionSession = session._mentionSessions[mateId];
4128
- if (!mentionSession.isAlive()) return;
4129
-
4130
- mentionSession._digesting = true;
4131
-
4132
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4133
- var knowledgeDir = path.join(mateDir, "knowledge");
4134
-
4135
- // Migration: generate initial summary if missing
4136
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4137
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4138
- if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
4139
- initMemorySummary(mateCtx, mateId, function () {});
4140
- }
4141
-
4142
- var userQ = userQuestion || "(unknown)";
4143
- var mateR = mateResponse || "(unknown)";
4144
- var conversationContent = "User: " + (userQ.length > 2000 ? userQ.substring(0, 2000) + "..." : userQ) +
4145
- "\nMate: " + (mateR.length > 2000 ? mateR.substring(0, 2000) + "..." : mateR);
4146
-
4147
- enqueueDigest({
4148
- mateCtx: mateCtx,
4149
- mateId: mateId,
4150
- type: "mention",
4151
- conversationContent: conversationContent,
4152
- onDone: function () { mentionSession._digesting = false; },
4153
- });
4154
- }
4155
-
4156
- // Digest DM turn for mate projects - uses shared digest worker
4157
- var _dmDigestPending = false;
4158
- function digestDmTurn(session, responsePreview) {
4159
- if (!isMate || _dmDigestPending) return;
4160
- var mateId = path.basename(cwd);
4161
- var mateCtx = matesModule.buildMateCtx(projectOwnerId);
4162
- if (!matesModule.isMate(mateCtx, mateId)) return;
4163
-
4164
- // Collect full conversation from session history (all user + mate turns)
4165
- var conversationParts = [];
4166
- var totalLen = 0;
4167
- var CONV_CAP = 6000;
4168
- for (var hi = 0; hi < session.history.length; hi++) {
4169
- var entry = session.history[hi];
4170
- if (entry.type === "user_message" && entry.text) {
4171
- var uText = entry.text;
4172
- if (totalLen + uText.length > CONV_CAP) {
4173
- uText = uText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
4174
- }
4175
- conversationParts.push("User: " + uText);
4176
- totalLen += uText.length;
4177
- } else if (entry.type === "assistant_message" && entry.text) {
4178
- var aText = entry.text;
4179
- if (totalLen + aText.length > CONV_CAP) {
4180
- aText = aText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
4181
- }
4182
- conversationParts.push("Mate: " + aText);
4183
- totalLen += aText.length;
4184
- }
4185
- if (totalLen >= CONV_CAP) break;
4186
- }
4187
- var lastResponseText = responsePreview || "";
4188
- if (lastResponseText && conversationParts.length > 0) {
4189
- var lastPart = conversationParts[conversationParts.length - 1];
4190
- if (lastPart.indexOf("Mate:") !== 0 || lastPart.indexOf(lastResponseText.substring(0, 50)) === -1) {
4191
- var rText = lastResponseText;
4192
- if (totalLen + rText.length > CONV_CAP) {
4193
- rText = rText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
4194
- }
4195
- conversationParts.push("Mate: " + rText);
4196
- }
4197
- }
4198
- if (conversationParts.length === 0) return;
4199
-
4200
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4201
- var knowledgeDir = path.join(mateDir, "knowledge");
4202
-
4203
- // Migration: if memory-summary.md missing but digests exist, generate initial summary
4204
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4205
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4206
- if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
4207
- initMemorySummary(mateCtx, mateId, function () {
4208
- console.log("[memory-migrate] Initial summary generated for mate " + mateId);
4209
- });
4210
- }
4211
-
4212
- _dmDigestPending = true;
4213
-
4214
- enqueueDigest({
4215
- mateCtx: mateCtx,
4216
- mateId: mateId,
4217
- type: "dm",
4218
- conversationContent: conversationParts.join("\n"),
4219
- onDone: function () { _dmDigestPending = false; },
4220
- });
4221
- }
4222
-
4223
- function handleMention(ws, msg) {
4224
- if (!msg.mateId) return;
4225
- if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
4226
-
4227
- var session = getSessionForWs(ws);
4228
- if (!session) return;
4229
-
4230
- // Block mentions during an active debate
4231
- if (session._debate && session._debate.phase === "live") {
4232
- sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Cannot use @mentions during an active debate." });
4233
- return;
4234
- }
4235
-
4236
- // Check if a mention is already in progress for this session
4237
- if (session._mentionInProgress) {
4238
- sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "A mention is already in progress." });
4239
- return;
4240
- }
4241
-
4242
- var userId = ws._clayUser ? ws._clayUser.id : null;
4243
- var mateCtx = matesModule.buildMateCtx(userId);
4244
- var mate = matesModule.getMate(mateCtx, msg.mateId);
4245
- if (!mate) {
4246
- sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Mate not found" });
4247
- return;
4248
- }
4249
-
4250
- var mateName = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
4251
- var avatarColor = (mate.profile && mate.profile.avatarColor) || "#6c5ce7";
4252
- var avatarStyle = (mate.profile && mate.profile.avatarStyle) || "bottts";
4253
- var avatarSeed = (mate.profile && mate.profile.avatarSeed) || mate.id;
4254
-
4255
- // Build full mention text (include pasted content)
4256
- var mentionFullInput = msg.text || "";
4257
- if (msg.pastes && msg.pastes.length > 0) {
4258
- for (var pi = 0; pi < msg.pastes.length; pi++) {
4259
- if (mentionFullInput) mentionFullInput += "\n\n";
4260
- mentionFullInput += msg.pastes[pi];
4261
- }
4262
- }
4263
-
4264
- // Save images to disk (same pattern as regular messages)
4265
- var imageRefs = [];
4266
- if (msg.images && msg.images.length > 0) {
4267
- for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
4268
- var img = msg.images[imgIdx];
4269
- var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
4270
- if (savedName) {
4271
- imageRefs.push({ mediaType: img.mediaType, file: savedName });
4272
- }
4273
- }
4274
- }
4275
-
4276
- // Save mention user message to session history
4277
- var mentionUserEntry = { type: "mention_user", text: msg.text, mateId: msg.mateId, mateName: mateName };
4278
- if (msg.pastes && msg.pastes.length > 0) mentionUserEntry.pastes = msg.pastes;
4279
- if (imageRefs.length > 0) mentionUserEntry.imageRefs = imageRefs;
4280
- session.history.push(mentionUserEntry);
4281
- sm.appendToSessionFile(session, mentionUserEntry);
4282
- sendToSessionOthers(ws, session.localId, hydrateImageRefs(mentionUserEntry));
4283
-
4284
- // Extract recent turns for continuity check
4285
- var recentTurns = getRecentTurns(session, MENTION_WINDOW);
4286
-
4287
- // Determine user name for context
4288
- var userName = "User";
4289
- if (ws._clayUser) {
4290
- var p = ws._clayUser.profile || {};
4291
- userName = p.name || ws._clayUser.displayName || ws._clayUser.username || "User";
4292
- }
4293
-
4294
- session._mentionInProgress = true;
4295
-
4296
- // Send mention start indicator
4297
- sendToSession(session.localId, {
4298
- type: "mention_start",
4299
- mateId: msg.mateId,
4300
- mateName: mateName,
4301
- avatarColor: avatarColor,
4302
- avatarStyle: avatarStyle,
4303
- avatarSeed: avatarSeed,
4304
- });
4305
-
4306
- // Shared callbacks for both new and continued sessions
4307
- var mentionCallbacks = {
4308
- onActivity: function (activity) {
4309
- sendToSession(session.localId, {
4310
- type: "mention_activity",
4311
- mateId: msg.mateId,
4312
- activity: activity,
4313
- });
4314
- },
4315
- onDelta: function (delta) {
4316
- sendToSession(session.localId, {
4317
- type: "mention_stream",
4318
- mateId: msg.mateId,
4319
- mateName: mateName,
4320
- delta: delta,
4321
- });
4322
- },
4323
- onDone: function (fullText) {
4324
- session._mentionInProgress = false;
4325
-
4326
- // Save mention response to session history
4327
- var mentionResponseEntry = {
4328
- type: "mention_response",
4329
- mateId: msg.mateId,
4330
- mateName: mateName,
4331
- text: fullText,
4332
- avatarColor: avatarColor,
4333
- avatarStyle: avatarStyle,
4334
- avatarSeed: avatarSeed,
4335
- };
4336
- session.history.push(mentionResponseEntry);
4337
- sm.appendToSessionFile(session, mentionResponseEntry);
4338
-
4339
- // Queue mention context for injection into the current agent's next turn
4340
- if (!session.pendingMentionContexts) session.pendingMentionContexts = [];
4341
- session.pendingMentionContexts.push(
4342
- "[Context: @" + mateName + " was mentioned and responded]\n\n" +
4343
- "User asked @" + mateName + ": " + msg.text + "\n" +
4344
- mateName + " responded: " + fullText + "\n\n" +
4345
- "[End of @mention context. This is for your reference only. Do not re-execute or repeat this response.]"
4346
- );
4347
-
4348
- sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
4349
-
4350
- // Check if the mate wrote a debate brief during this turn
4351
- checkForDmDebateBrief(session, msg.mateId, mateCtx);
4352
-
4353
- // Generate session digest for mate's long-term memory
4354
- digestMentionSession(session, msg.mateId, mateCtx, fullText, msg.text);
4355
- },
4356
- onError: function (errMsg) {
4357
- session._mentionInProgress = false;
4358
- // Clean up dead session
4359
- if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
4360
- delete session._mentionSessions[msg.mateId];
4361
- }
4362
- console.error("[mention] Error for mate " + msg.mateId + ":", errMsg);
4363
- sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: errMsg });
4364
- },
4365
- };
4366
-
4367
- // Initialize mention sessions map if needed
4368
- if (!session._mentionSessions) session._mentionSessions = {};
4369
-
4370
- // Session continuity: check if this mate has a response in the recent window
4371
- var existingSession = session._mentionSessions[msg.mateId];
4372
- // Don't reuse a session that's still generating a digest (would mix digest output into mention stream)
4373
- var canContinue = existingSession && existingSession.isAlive() && !existingSession._digesting && hasMateInWindow(recentTurns, msg.mateId);
4374
-
4375
- if (canContinue) {
4376
- // Continue existing mention session with middle context
4377
- var middleContext = buildMiddleContext(recentTurns, msg.mateId);
4378
- var continuationText = middleContext ? middleContext + "\n\n" + mentionFullInput : mentionFullInput;
4379
- existingSession.pushMessage(continuationText, mentionCallbacks, msg.images);
4380
- } else {
4381
- // Clean up old session if it exists
4382
- if (existingSession) {
4383
- existingSession.close();
4384
- delete session._mentionSessions[msg.mateId];
4385
- }
4386
-
4387
- // Load Mate CLAUDE.md
4388
- var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
4389
- var claudeMd = "";
4390
- try {
4391
- claudeMd = fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
4392
- } catch (e) {
4393
- // CLAUDE.md may not exist for new mates
4394
- }
4395
-
4396
- // Load session digests (unified: uses memory-summary.md if available)
4397
- // Pass user's message as query for BM25 search of relevant past sessions
4398
- var recentDigests = loadMateDigests(mateCtx, msg.mateId, mentionFullInput);
4399
-
4400
- // Build initial mention context
4401
- var mentionContext = buildMentionContext(userName, recentTurns) + recentDigests;
4402
-
4403
- // Create new persistent mention session
4404
- sdk.createMentionSession({
4405
- claudeMd: claudeMd,
4406
- initialContext: mentionContext,
4407
- initialMessage: mentionFullInput,
4408
- initialImages: msg.images || null,
4409
- onActivity: mentionCallbacks.onActivity,
4410
- onDelta: mentionCallbacks.onDelta,
4411
- onDone: mentionCallbacks.onDone,
4412
- onError: mentionCallbacks.onError,
4413
- canUseTool: function (toolName, input, toolOpts) {
4414
- var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
4415
- if (autoAllow[toolName]) {
4416
- return Promise.resolve({ behavior: "allow", updatedInput: input });
4417
- }
4418
- // Route through the project session's permission system
4419
- return new Promise(function (resolve) {
4420
- var requestId = crypto.randomUUID();
4421
- session.pendingPermissions[requestId] = {
4422
- resolve: resolve,
4423
- requestId: requestId,
4424
- toolName: toolName,
4425
- toolInput: input,
4426
- toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
4427
- decisionReason: (toolOpts && toolOpts.decisionReason) || "",
4428
- mateId: msg.mateId,
4429
- };
4430
- sendToSession(session.localId, {
4431
- type: "permission_request",
4432
- requestId: requestId,
4433
- toolName: toolName,
4434
- toolInput: input,
4435
- toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
4436
- decisionReason: (toolOpts && toolOpts.decisionReason) || "",
4437
- mateId: msg.mateId,
4438
- });
4439
- onProcessingChanged();
4440
- if (toolOpts && toolOpts.signal) {
4441
- toolOpts.signal.addEventListener("abort", function () {
4442
- delete session.pendingPermissions[requestId];
4443
- sendToSession(session.localId, { type: "permission_cancel", requestId: requestId });
4444
- onProcessingChanged();
4445
- resolve({ behavior: "deny", message: "Request cancelled" });
4446
- });
4447
- }
4448
- });
4449
- },
4450
- }).then(function (mentionSession) {
4451
- if (mentionSession) {
4452
- session._mentionSessions[msg.mateId] = mentionSession;
4453
- }
4454
- }).catch(function (err) {
4455
- session._mentionInProgress = false;
4456
- console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
4457
- sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
4458
- });
4459
- }
4460
- }
4461
-
4462
- // --- Shared mate helpers (used by debate module and other code) ---
3937
+ // --- Shared helpers ---
4463
3938
 
4464
3939
  function escapeRegex(str) {
4465
3940
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4466
3941
  }
4467
3942
 
4468
- function getMateProfile(mateCtx, mateId) {
4469
- var mate = matesModule.getMate(mateCtx, mateId);
4470
- if (!mate) return { name: "Mate", avatarColor: "#6c5ce7", avatarStyle: "bottts", avatarSeed: mateId };
4471
- return {
4472
- name: (mate.profile && mate.profile.displayName) || mate.name || "Mate",
4473
- avatarColor: (mate.profile && mate.profile.avatarColor) || "#6c5ce7",
4474
- avatarStyle: (mate.profile && mate.profile.avatarStyle) || "bottts",
4475
- avatarSeed: (mate.profile && mate.profile.avatarSeed) || mateId,
4476
- };
4477
- }
4478
-
4479
- function loadMateClaudeMd(mateCtx, mateId) {
4480
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4481
- try {
4482
- return fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
4483
- } catch (e) {
4484
- return "";
4485
- }
4486
- }
4487
-
4488
- function formatRawDigests(rawLines, headerLabel) {
4489
- if (!rawLines || rawLines.length === 0) return "";
4490
- var lines = ["\n\n" + (headerLabel || "Your recent session memories:")];
4491
- for (var i = 0; i < rawLines.length; i++) {
4492
- try {
4493
- var d = JSON.parse(rawLines[i]);
4494
- if (d.type === "debate" && d.my_role) {
4495
- // Debate memories are role-played positions, not genuine opinions
4496
- lines.push("- [" + (d.date || "?") + "] DEBATE (role: " + d.my_role + ") " + (d.topic || "unknown") +
4497
- ": argued " + (d.my_position || "N/A") + " (assigned role, not my actual opinion)" +
4498
- (d.outcome ? " | Outcome: " + d.outcome : "") +
4499
- (d.open_items ? " | Open: " + d.open_items : ""));
4500
- } else {
4501
- lines.push("- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
4502
- (d.decisions ? " | Decisions: " + d.decisions : "") +
4503
- (d.open_items ? " | Open: " + d.open_items : ""));
4504
- }
4505
- } catch (e) {}
4506
- }
4507
- return lines.join("\n");
4508
- }
4509
-
4510
- function loadMateDigests(mateCtx, mateId, query) {
4511
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4512
- var knowledgeDir = path.join(mateDir, "knowledge");
4513
- var mate = matesModule.getMate(mateCtx, mateId);
4514
- var hasGlobalSearch = mate && mate.globalSearch;
4515
-
4516
- // Load shared user profile (available to ALL mates)
4517
- var userProfileResult = "";
4518
- try {
4519
- var matesRoot = matesModule.resolveMatesRoot(mateCtx);
4520
- var userProfilePath = path.join(matesRoot, "user-profile.md");
4521
- if (fs.existsSync(userProfilePath)) {
4522
- var profileContent = fs.readFileSync(userProfilePath, "utf8").trim();
4523
- if (profileContent && profileContent.length > 50) {
4524
- userProfileResult = "\n\n" + profileContent;
4525
- }
4526
- }
4527
- } catch (e) {}
4528
-
4529
- // Check for memory-summary.md first
4530
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4531
- var hasSummary = false;
4532
- var summaryContent = "";
4533
- try {
4534
- if (fs.existsSync(summaryFile)) {
4535
- summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4536
- if (summaryContent) hasSummary = true;
4537
- }
4538
- } catch (e) {}
4539
-
4540
- // Load raw digests
4541
- var allLines = [];
4542
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4543
- try {
4544
- if (fs.existsSync(digestFile)) {
4545
- allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4546
- }
4547
- } catch (e) {}
4548
-
4549
- var result = userProfileResult;
4550
-
4551
- if (hasSummary) {
4552
- // Load summary + latest 5 raw digests for richer context
4553
- var recent = allLines.slice(-5);
4554
- result = "\n\nYour memory summary:\n" + summaryContent;
4555
- if (recent.length > 0) {
4556
- result += formatRawDigests(recent, "Latest raw session memories:");
4557
- }
4558
- } else {
4559
- // Backward compatible: latest 8 raw digests
4560
- var recent = allLines.slice(-8);
4561
- result = formatRawDigests(recent, "Your recent session memories:");
4562
- }
4563
-
4564
- // Global search: always load team memory summaries for globalSearch mates
4565
- var otherDigests = [];
4566
- if (hasGlobalSearch) {
4567
- try {
4568
- var allMates = matesModule.getAllMates(mateCtx);
4569
- var teamSummaries = [];
4570
- for (var mi = 0; mi < allMates.length; mi++) {
4571
- if (allMates[mi].id === mateId) continue;
4572
- var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
4573
- var mateName = allMates[mi].name || allMates[mi].id;
4574
-
4575
- // Collect digest files for BM25 search
4576
- var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
4577
- if (fs.existsSync(otherDigest)) {
4578
- otherDigests.push({ path: otherDigest, mateName: mateName });
4579
- }
4580
-
4581
- // Collect memory summaries for direct context injection
4582
- var otherSummary = path.join(otherDir, "knowledge", "memory-summary.md");
4583
- try {
4584
- if (fs.existsSync(otherSummary)) {
4585
- var summaryText = fs.readFileSync(otherSummary, "utf8").trim();
4586
- if (summaryText && summaryText.length > 50) {
4587
- teamSummaries.push({ mateName: mateName, summary: summaryText });
4588
- }
4589
- }
4590
- } catch (e) {}
4591
- }
4592
-
4593
- // Inject team memory summaries into context
4594
- if (teamSummaries.length > 0) {
4595
- result += "\n\nTeam memory summaries (other mates' accumulated context):";
4596
- for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
4597
- var ts = teamSummaries[tsi];
4598
- // Cap each summary to avoid context overflow
4599
- var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
4600
- result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
4601
- }
4602
- }
4603
- } catch (e) {}
4604
-
4605
- // Inject recent user observations from all mates (newest first, max 15)
4606
- try {
4607
- var allObservations = [];
4608
- var allMatesForObs = matesModule.getAllMates(mateCtx);
4609
- for (var moi = 0; moi < allMatesForObs.length; moi++) {
4610
- var moDir = matesModule.getMateDir(mateCtx, allMatesForObs[moi].id);
4611
- var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
4612
- try {
4613
- if (fs.existsSync(moFile)) {
4614
- var moLines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4615
- for (var mli = 0; mli < moLines.length; mli++) {
4616
- try {
4617
- var moEntry = JSON.parse(moLines[mli]);
4618
- moEntry._mateName = moEntry.mateName || allMatesForObs[moi].name || allMatesForObs[moi].id;
4619
- allObservations.push(moEntry);
4620
- } catch (e) {}
4621
- }
4622
- }
4623
- } catch (e) {}
4624
- }
4625
- if (allObservations.length > 0) {
4626
- // Sort by date descending
4627
- allObservations.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
4628
- var recentObs = allObservations.slice(0, 15);
4629
- result += "\n\nRecent user observations from all mates:";
4630
- for (var roi = 0; roi < recentObs.length; roi++) {
4631
- var ro = recentObs[roi];
4632
- result += "\n- [" + (ro.date || "?") + "] [@" + ro._mateName + "] [" + (ro.category || "?") + "] " + (ro.observation || "") + (ro.evidence ? " (evidence: " + ro.evidence + ")" : "");
4633
- }
4634
- }
4635
- } catch (e) {}
4636
-
4637
- // Inject recent activity timeline across all projects (chronological)
4638
- try {
4639
- var timelineEntries = [];
4640
-
4641
- // Own sessions
4642
- sm.sessions.forEach(function (s) {
4643
- if (s.hidden || !s.history || s.history.length === 0) return;
4644
- timelineEntries.push({
4645
- title: s.title || "New Session",
4646
- project: null,
4647
- ts: s.lastActivity || s.createdAt || 0
4648
- });
4649
- });
4650
-
4651
- // Cross-project sessions
4652
- var crossForTimeline = getAllProjectSessions();
4653
- for (var cti = 0; cti < crossForTimeline.length; cti++) {
4654
- var cs = crossForTimeline[cti];
4655
- timelineEntries.push({
4656
- title: cs.title || "New Session",
4657
- project: cs._projectTitle || null,
4658
- ts: cs.lastActivity || cs.createdAt || 0
4659
- });
4660
- }
4661
-
4662
- // Sort by time descending, take latest 20
4663
- timelineEntries.sort(function (a, b) { return b.ts - a.ts; });
4664
- timelineEntries = timelineEntries.slice(0, 20);
4665
-
4666
- if (timelineEntries.length > 0) {
4667
- result += "\n\nRecent activity timeline (newest first):";
4668
- for (var ti = 0; ti < timelineEntries.length; ti++) {
4669
- var te = timelineEntries[ti];
4670
- var dateStr = te.ts ? new Date(te.ts).toISOString().replace("T", " ").substring(0, 16) : "?";
4671
- var line = "- [" + dateStr + "] " + te.title;
4672
- if (te.project) line += " (project: " + te.project + ")";
4673
- result += "\n" + line;
4674
- }
4675
- }
4676
- } catch (e) {}
4677
- }
4678
-
4679
- // BM25 unified search: digests + session history for current topic
4680
- // globalSearch mates always search (they see everything); others need enough digests
4681
- if (query && (hasGlobalSearch || allLines.length > 5)) {
4682
- try {
4683
- // Collect mate's own sessions
4684
- var mateSessions = [];
4685
- sm.sessions.forEach(function (s) {
4686
- if (!s.hidden && s.history && s.history.length > 0) {
4687
- mateSessions.push(s);
4688
- }
4689
- });
4690
-
4691
- // globalSearch: also collect sessions from all other projects + knowledge files
4692
- var knowledgeFiles = [];
4693
- if (hasGlobalSearch) {
4694
- var crossSessions = getAllProjectSessions();
4695
- for (var cs = 0; cs < crossSessions.length; cs++) {
4696
- mateSessions.push(crossSessions[cs]);
4697
- }
4698
-
4699
- // Collect knowledge files from all mates
4700
- try {
4701
- var allMatesForKnowledge = matesModule.getAllMates(mateCtx);
4702
- for (var mk = 0; mk < allMatesForKnowledge.length; mk++) {
4703
- var mkDir = matesModule.getMateDir(mateCtx, allMatesForKnowledge[mk].id);
4704
- var mkName = allMatesForKnowledge[mk].name || allMatesForKnowledge[mk].id;
4705
- var mkKnowledgeDir = path.join(mkDir, "knowledge");
4706
- try {
4707
- var kFiles = fs.readdirSync(mkKnowledgeDir);
4708
- for (var kfi = 0; kfi < kFiles.length; kfi++) {
4709
- var kfName = kFiles[kfi];
4710
- // Skip system files (digests, identity, base-template)
4711
- if (kfName === "session-digests.jsonl" || kfName === "memory-summary.md" ||
4712
- kfName === "identity-backup.md" || kfName === "identity-history.jsonl" ||
4713
- kfName === "base-template.md") continue;
4714
- knowledgeFiles.push({
4715
- filePath: path.join(mkKnowledgeDir, kfName),
4716
- name: kfName,
4717
- mateName: mkName
4718
- });
4719
- }
4720
- } catch (e) {}
4721
- }
4722
- } catch (e) {}
4723
- }
4724
-
4725
- var searchResults = sessionSearch.searchMate({
4726
- digestFilePath: digestFile,
4727
- otherDigests: otherDigests,
4728
- sessions: mateSessions,
4729
- knowledgeFiles: knowledgeFiles,
4730
- query: query,
4731
- maxResults: hasGlobalSearch ? 12 : 5,
4732
- minScore: 1.0
4733
- });
4734
- var contextStr = sessionSearch.formatForContext(searchResults);
4735
- if (contextStr) result += contextStr;
4736
- } catch (e) {
4737
- console.error("[session-search] Mate search failed:", e.message);
4738
- }
4739
- }
4740
-
4741
- return result;
4742
- }
4743
-
4744
- // Gate check: ask Haiku whether this conversation contains anything worth remembering
4745
- function gateMemory(mateCtx, mateId, conversationContent, callback, opts) {
4746
- opts = opts || {};
4747
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4748
- var knowledgeDir = path.join(mateDir, "knowledge");
4749
-
4750
- // Load mate role/activities from mate.yaml (lightweight, no full CLAUDE.md)
4751
- var mateRole = "";
4752
- var mateActivities = "";
4753
- try {
4754
- var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
4755
- var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
4756
- var actMatch = yamlRaw.match(/^activities:\s*(.+)$/m);
4757
- if (roleMatch) mateRole = roleMatch[1].trim();
4758
- if (actMatch) mateActivities = actMatch[1].trim();
4759
- } catch (e) {}
4760
-
4761
- // Load existing memory summary if available
4762
- var summaryContent = "";
4763
- try {
4764
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4765
- if (fs.existsSync(summaryFile)) {
4766
- summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4767
- }
4768
- } catch (e) {}
4769
-
4770
- // Cap conversation content for gate
4771
- var cappedContent = conversationContent;
4772
- if (cappedContent.length > 3000) {
4773
- cappedContent = cappedContent.substring(0, 3000) + "...";
4774
- }
4775
-
4776
- var gateContext = [
4777
- "[SYSTEM: Memory Gate]",
4778
- "You are a memory filter for an AI Mate.",
4779
- "",
4780
- "Mate role: " + (mateRole || "assistant"),
4781
- "Mate activities: " + (mateActivities || "general"),
4782
- "",
4783
- "Current memory summary:",
4784
- summaryContent || "No memory summary yet.",
4785
- "",
4786
- "Conversation just ended:",
4787
- cappedContent,
4788
- ].join("\n");
4789
-
4790
- var gatePrompt = opts.gatePrompt || [
4791
- 'Should this conversation be saved to long-term memory?',
4792
- 'Answer "yes" if ANY of these apply:',
4793
- "- A new decision, commitment, or direction",
4794
- "- A change in position or strategy",
4795
- "- New information relevant to this Mate's role",
4796
- "- A user preference, opinion, or pattern not already in the summary",
4797
- "- The user shared personal context, project details, or goals",
4798
- "- The user expressed what they like, dislike, or care about",
4799
- "- The user gave instructions on how they want things done",
4800
- "- Anything the user would reasonably expect to be remembered next time",
4801
- "",
4802
- 'Answer "no" ONLY if:',
4803
- "- It exactly duplicates what is already in the memory summary",
4804
- "- The entire conversation is a single trivial exchange (e.g. just 'hi' / 'hello')",
4805
- "",
4806
- "When in doubt, answer yes. It is better to remember too much than to forget something important.",
4807
- "",
4808
- 'Answer with ONLY "yes" or "no". Nothing else.',
4809
- ].join("\n");
4810
- var defaultOnError = opts.defaultYes !== undefined ? !!opts.defaultYes : true;
4811
-
4812
- var gateText = "";
4813
- var _gateSession = null;
4814
- sdk.createMentionSession({
4815
- claudeMd: "",
4816
- model: "haiku",
4817
- initialContext: gateContext,
4818
- initialMessage: gatePrompt,
4819
- onActivity: function () {},
4820
- onDelta: function (delta) {
4821
- gateText += delta;
4822
- },
4823
- onDone: function () {
4824
- var answer = gateText.trim().toLowerCase();
4825
- var shouldRemember = answer.indexOf("yes") !== -1;
4826
- if (_gateSession) try { _gateSession.close(); } catch (e) {}
4827
- callback(shouldRemember);
4828
- },
4829
- onError: function (err) {
4830
- console.error("[memory-gate] Gate check failed for mate " + mateId + ":", err);
4831
- if (_gateSession) try { _gateSession.close(); } catch (e) {}
4832
- callback(defaultOnError);
4833
- },
4834
- }).then(function (gs) {
4835
- _gateSession = gs;
4836
- if (!gs) callback(defaultOnError);
4837
- }).catch(function (err) {
4838
- console.error("[memory-gate] Failed to create gate session for mate " + mateId + ":", err);
4839
- callback(defaultOnError);
4840
- });
4841
- }
4842
-
4843
- // Update (or create) memory-summary.md based on a new digest
4844
- function updateMemorySummary(mateCtx, mateId, digestObj) {
4845
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4846
- var knowledgeDir = path.join(mateDir, "knowledge");
4847
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4848
-
4849
- // Check if summary exists; if not, try initial generation first
4850
- var summaryExists = false;
4851
- var summaryContent = "";
4852
- try {
4853
- if (fs.existsSync(summaryFile)) {
4854
- summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4855
- if (summaryContent) summaryExists = true;
4856
- }
4857
- } catch (e) {}
4858
-
4859
- if (!summaryExists) {
4860
- // Try initial summary generation from existing digests (migration)
4861
- initMemorySummary(mateCtx, mateId, function () {
4862
- // After init, do incremental update with the new digest
4863
- doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
4864
- });
4865
- } else {
4866
- doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
4867
- }
4868
- }
4869
-
4870
- // Incremental update of memory-summary.md with a single new digest
4871
- function doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj) {
4872
- var existingSummary = "";
4873
- try {
4874
- if (fs.existsSync(summaryFile)) {
4875
- existingSummary = fs.readFileSync(summaryFile, "utf8").trim();
4876
- }
4877
- } catch (e) {}
4878
-
4879
- var updateContext = [
4880
- "[SYSTEM: Memory Summary Update]",
4881
- "You are updating an AI Mate's long-term memory summary.",
4882
- "",
4883
- "Current summary:",
4884
- existingSummary || "(empty, this is the first entry)",
4885
- "",
4886
- "New session digest to incorporate:",
4887
- JSON.stringify(digestObj, null, 2),
4888
- ].join("\n");
4889
-
4890
- var updatePrompt = [
4891
- "Update the summary by:",
4892
- "1. Adding new information from this session",
4893
- "2. Updating existing entries if positions changed",
4894
- "3. Moving resolved open threads out of \"Open Threads\"",
4895
- "4. Adding to \"My Track Record\" if a past prediction/recommendation can now be evaluated",
4896
- "5. Removing outdated or redundant information",
4897
- "6. Preserving important user quotes and context from key_quotes and user_context fields",
4898
- "",
4899
- "Maintain this structure:",
4900
- "",
4901
- "# Memory Summary",
4902
- "Last updated: YYYY-MM-DD (session count: N+1)",
4903
- "",
4904
- "## User Context",
4905
- "(who they are, what they work on, project details, goals)",
4906
- "## User Patterns",
4907
- "(preferences, work style, communication style, likes/dislikes)",
4908
- "## Key Decisions",
4909
- "## Notable Quotes",
4910
- "(important things the user said, verbatim when possible)",
4911
- "## My Track Record",
4912
- "## Open Threads",
4913
- "## Recurring Topics",
4914
- "",
4915
- "Keep it concise. Each section should have at most 10 bullet points.",
4916
- "Drop the oldest/least relevant if needed.",
4917
- "The Notable Quotes section is valuable for preserving the user's voice and intent.",
4918
- "Output ONLY the updated markdown. Nothing else.",
4919
- ].join("\n");
4920
-
4921
- var updateText = "";
4922
- var _updateSession = null;
4923
- sdk.createMentionSession({
4924
- claudeMd: "",
4925
- model: "haiku",
4926
- initialContext: updateContext,
4927
- initialMessage: updatePrompt,
4928
- onActivity: function () {},
4929
- onDelta: function (delta) {
4930
- updateText += delta;
4931
- },
4932
- onDone: function () {
4933
- try {
4934
- var cleaned = updateText.trim();
4935
- if (cleaned.indexOf("```") === 0) {
4936
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
4937
- }
4938
- fs.mkdirSync(knowledgeDir, { recursive: true });
4939
- fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
4940
- console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
4941
- } catch (e) {
4942
- console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
4943
- }
4944
- if (_updateSession) try { _updateSession.close(); } catch (e) {}
4945
- },
4946
- onError: function (err) {
4947
- console.error("[memory-summary] Summary update failed for mate " + mateId + ":", err);
4948
- if (_updateSession) try { _updateSession.close(); } catch (e) {}
4949
- },
4950
- }).then(function (us) {
4951
- _updateSession = us;
4952
- }).catch(function (err) {
4953
- console.error("[memory-summary] Failed to create summary update session for mate " + mateId + ":", err);
4954
- });
4955
- }
4956
-
4957
- // User profile synthesis: collect observations from all mates, synthesize unified profile
4958
- var USER_PROFILE_SYNTHESIS_THRESHOLD = 8;
4959
-
4960
- function maybeSynthesizeUserProfile(mateCtx, mateId) {
4961
- var mate = matesModule.getMate(mateCtx, mateId);
4962
- if (!mate || !mate.globalSearch) return; // Only primary/globalSearch mates synthesize
4963
-
4964
- var matesRoot = matesModule.resolveMatesRoot(mateCtx);
4965
- var profilePath = path.join(matesRoot, "user-profile.md");
4966
-
4967
- // Collect all observations across all mates
4968
- var allObs = [];
4969
- try {
4970
- var allMates = matesModule.getAllMates(mateCtx);
4971
- for (var mi = 0; mi < allMates.length; mi++) {
4972
- var moDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
4973
- var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
4974
- try {
4975
- if (fs.existsSync(moFile)) {
4976
- var lines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4977
- for (var li = 0; li < lines.length; li++) {
4978
- try { allObs.push(JSON.parse(lines[li])); } catch (e) {}
4979
- }
4980
- }
4981
- } catch (e) {}
4982
- }
4983
- } catch (e) { return; }
4984
-
4985
- if (allObs.length === 0) return;
4986
-
4987
- // Check if synthesis is needed (threshold since last synthesis)
4988
- var existingProfile = "";
4989
- var lastObsCount = 0;
4990
- try {
4991
- if (fs.existsSync(profilePath)) {
4992
- existingProfile = fs.readFileSync(profilePath, "utf8").trim();
4993
- var countMatch = existingProfile.match(/from (\d+) observations/);
4994
- if (countMatch) lastObsCount = parseInt(countMatch[1], 10) || 0;
4995
- }
4996
- } catch (e) {}
4997
-
4998
- if (allObs.length - lastObsCount < USER_PROFILE_SYNTHESIS_THRESHOLD) return;
4999
-
5000
- // Sort newest first for synthesis
5001
- allObs.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
5002
-
5003
- var synthContext = [
5004
- "[SYSTEM: User Profile Synthesis]",
5005
- "You are synthesizing a user profile from observations collected by multiple AI teammates.",
5006
- "",
5007
- "Current profile:",
5008
- existingProfile || "(none yet, first synthesis)",
5009
- "",
5010
- "All observations (" + allObs.length + " total, newest first):",
5011
- allObs.map(function (o) {
5012
- return "[" + (o.date || "?") + "] [@" + (o.mateName || o.mateId || "?") + "] [" + (o.category || "?") + "] " + (o.observation || "") + (o.evidence ? " (evidence: " + o.evidence + ")" : "");
5013
- }).join("\n"),
5014
- ].join("\n");
5015
-
5016
- var synthPrompt = [
5017
- "Synthesize a unified user profile from these observations.",
5018
- "",
5019
- "Rules:",
5020
- "1. Organize by: Communication Style, Decision Patterns, Working Habits, Technical Preferences, Emotional Signals",
5021
- "2. Each point: observation + source mates and dates in parentheses",
5022
- "3. If observations contradict, note both with dates. Preferences evolve.",
5023
- "4. Mark patterns seen 3+ times as [strong], 2 times as [emerging]",
5024
- "5. Keep under 800 words. This is a reference card, not a biography.",
5025
- '6. End with: "Last synthesized: YYYY-MM-DD from N observations across M mates"',
5026
- "",
5027
- "Output ONLY the markdown profile. No fences, no extra text.",
5028
- ].join("\n");
5029
-
5030
- var synthText = "";
5031
- sdk.createMentionSession({
5032
- claudeMd: "",
5033
- model: "haiku",
5034
- initialContext: synthContext,
5035
- initialMessage: synthPrompt,
5036
- onActivity: function () {},
5037
- onDelta: function (delta) { synthText += delta; },
5038
- onDone: function () {
5039
- try {
5040
- var cleaned = synthText.trim();
5041
- if (cleaned.indexOf("```") === 0) {
5042
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5043
- }
5044
- fs.mkdirSync(path.dirname(profilePath), { recursive: true });
5045
- fs.writeFileSync(profilePath, cleaned + "\n", "utf8");
5046
- console.log("[user-profile] Synthesized user-profile.md from " + allObs.length + " observations");
5047
- } catch (e) {
5048
- console.error("[user-profile] Failed to write user-profile.md:", e.message);
5049
- }
5050
- },
5051
- onError: function (err) {
5052
- console.error("[user-profile] Synthesis failed:", err);
5053
- },
5054
- }).catch(function (err) {
5055
- console.error("[user-profile] Failed to create synthesis session:", err);
5056
- });
5057
- }
5058
-
5059
- // Initial summary generation (migration): read latest 20 digests and generate first summary
5060
- function initMemorySummary(mateCtx, mateId, callback) {
5061
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
5062
- var knowledgeDir = path.join(mateDir, "knowledge");
5063
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
5064
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
5065
-
5066
- // Check if digests exist
5067
- var allLines = [];
5068
- try {
5069
- if (fs.existsSync(digestFile)) {
5070
- allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
5071
- }
5072
- } catch (e) {}
5073
-
5074
- if (allLines.length === 0) {
5075
- // No digests to summarize, just callback
5076
- callback();
5077
- return;
5078
- }
5079
-
5080
- var recent = allLines.slice(-20);
5081
- var digestsText = [];
5082
- for (var i = 0; i < recent.length; i++) {
5083
- try {
5084
- var d = JSON.parse(recent[i]);
5085
- digestsText.push(JSON.stringify(d));
5086
- } catch (e) {}
5087
- }
5088
-
5089
- if (digestsText.length === 0) {
5090
- callback();
5091
- return;
5092
- }
5093
-
5094
- var initContext = [
5095
- "[SYSTEM: Initial Memory Summary]",
5096
- "You are creating the first long-term memory summary for an AI Mate.",
5097
- "",
5098
- "Here are the most recent session digests (up to 20):",
5099
- digestsText.join("\n"),
5100
- ].join("\n");
5101
-
5102
- var initPrompt = [
5103
- "Create a memory summary from these sessions.",
5104
- "",
5105
- "Structure:",
5106
- "",
5107
- "# Memory Summary",
5108
- "Last updated: YYYY-MM-DD (session count: N)",
5109
- "",
5110
- "## User Context",
5111
- "(who they are, what they work on, project details, goals)",
5112
- "## User Patterns",
5113
- "(preferences, work style, communication style, likes/dislikes)",
5114
- "## Key Decisions",
5115
- "## Notable Quotes",
5116
- "(important things the user said, verbatim when possible)",
5117
- "## My Track Record",
5118
- "## Open Threads",
5119
- "## Recurring Topics",
5120
- "",
5121
- "Keep it concise. Focus on patterns, decisions, and the user's own words.",
5122
- "Each section should have at most 10 bullet points.",
5123
- "Preserve key_quotes from digests in the Notable Quotes section.",
5124
- "Set session count to " + digestsText.length + ".",
5125
- "Output ONLY the markdown. Nothing else.",
5126
- ].join("\n");
5127
-
5128
- var initText = "";
5129
- var _initSession = null;
5130
- sdk.createMentionSession({
5131
- claudeMd: "",
5132
- model: "haiku",
5133
- initialContext: initContext,
5134
- initialMessage: initPrompt,
5135
- onActivity: function () {},
5136
- onDelta: function (delta) {
5137
- initText += delta;
5138
- },
5139
- onDone: function () {
5140
- try {
5141
- var cleaned = initText.trim();
5142
- if (cleaned.indexOf("```") === 0) {
5143
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5144
- }
5145
- fs.mkdirSync(knowledgeDir, { recursive: true });
5146
- fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
5147
- console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
5148
- } catch (e) {
5149
- console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
5150
- }
5151
- if (_initSession) try { _initSession.close(); } catch (e) {}
5152
- callback();
5153
- },
5154
- onError: function (err) {
5155
- console.error("[memory-summary] Initial summary generation failed for mate " + mateId + ":", err);
5156
- if (_initSession) try { _initSession.close(); } catch (e) {}
5157
- callback();
5158
- },
5159
- }).then(function (is) {
5160
- _initSession = is;
5161
- if (!is) callback();
5162
- }).catch(function (err) {
5163
- console.error("[memory-summary] Failed to create init summary session for mate " + mateId + ":", err);
5164
- callback();
5165
- });
5166
- }
3943
+ // --- Memory engine (delegated to project-memory.js) ---
3944
+ var _memory = attachMemory({
3945
+ cwd: cwd,
3946
+ sm: sm,
3947
+ sdk: sdk,
3948
+ sendTo: sendTo,
3949
+ matesModule: matesModule,
3950
+ sessionSearch: sessionSearch,
3951
+ getAllProjectSessions: getAllProjectSessions,
3952
+ projectOwnerId: projectOwnerId,
3953
+ handleMessage: handleMessage,
3954
+ });
3955
+ var loadMateDigests = _memory.loadMateDigests;
3956
+ var gateMemory = _memory.gateMemory;
3957
+ var updateMemorySummary = _memory.updateMemorySummary;
3958
+ var initMemorySummary = _memory.initMemorySummary;
3959
+
3960
+ // --- Mate interaction engine (delegated to project-mate-interaction.js) ---
3961
+ // Note: checkForDmDebateBrief comes from _debate (initialized below),
3962
+ // so we use a lazy getter that resolves at call time.
3963
+ var _mateInteraction = attachMateInteraction({
3964
+ cwd: cwd,
3965
+ sm: sm,
3966
+ sdk: sdk,
3967
+ sendTo: sendTo,
3968
+ sendToSession: sendToSession,
3969
+ sendToSessionOthers: sendToSessionOthers,
3970
+ matesModule: matesModule,
3971
+ isMate: isMate,
3972
+ projectOwnerId: projectOwnerId,
3973
+ getSessionForWs: getSessionForWs,
3974
+ getLinuxUserForSession: getLinuxUserForSession,
3975
+ saveImageFile: saveImageFile,
3976
+ hydrateImageRefs: hydrateImageRefs,
3977
+ onProcessingChanged: onProcessingChanged,
3978
+ loadMateDigests: loadMateDigests,
3979
+ updateMemorySummary: updateMemorySummary,
3980
+ initMemorySummary: initMemorySummary,
3981
+ get checkForDmDebateBrief() { return checkForDmDebateBrief; },
3982
+ });
3983
+ var handleMention = _mateInteraction.handleMention;
3984
+ var getMateProfile = _mateInteraction.getMateProfile;
3985
+ var loadMateClaudeMd = _mateInteraction.loadMateClaudeMd;
3986
+ var digestDmTurn = _mateInteraction.digestDmTurn;
3987
+ var enqueueDigest = _mateInteraction.enqueueDigest;
5167
3988
 
5168
3989
  // --- Debate engine (delegated to project-debate.js) ---
5169
3990
  var _debate = attachDebate({