clay-server 2.19.0 → 2.20.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/project.js CHANGED
@@ -129,7 +129,8 @@ function createProjectContext(opts) {
129
129
 
130
130
  // Convert imageRefs in history entries to images with URLs for the client
131
131
  function hydrateImageRefs(entry) {
132
- if (!entry || entry.type !== "user_message" || !entry.imageRefs) return entry;
132
+ if (!entry || !entry.imageRefs) return entry;
133
+ if (entry.type !== "user_message" && entry.type !== "mention_user") return entry;
133
134
  var images = [];
134
135
  for (var ri = 0; ri < entry.imageRefs.length; ri++) {
135
136
  var ref = entry.imageRefs[ri];
@@ -419,6 +420,19 @@ function createProjectContext(opts) {
419
420
  isMate: isMate,
420
421
  dangerouslySkipPermissions: dangerouslySkipPermissions,
421
422
  onProcessingChanged: onProcessingChanged,
423
+ onTurnDone: isMate ? function (session, preview) { digestDmTurn(session, preview); } : null,
424
+ getAutoContinueSetting: function (session) {
425
+ // Per-user setting in multi-user mode
426
+ if (usersModule.isMultiUser() && session && session.ownerId) {
427
+ return usersModule.getAutoContinue(session.ownerId);
428
+ }
429
+ // Single-user: fall back to daemon config
430
+ if (typeof opts.onGetDaemonConfig === "function") {
431
+ var dc = opts.onGetDaemonConfig();
432
+ return !!dc.autoContinueOnRateLimit;
433
+ }
434
+ return false;
435
+ },
422
436
  });
423
437
 
424
438
  // --- Ralph Loop state ---
@@ -1381,6 +1395,22 @@ function createProjectContext(opts) {
1381
1395
  return;
1382
1396
  }
1383
1397
 
1398
+ if (msg.type === "mention_stop") {
1399
+ var session = getSessionForWs(ws);
1400
+ if (session && session._mentionInProgress) {
1401
+ // Abort the active mention session for this mate
1402
+ var mateId = msg.mateId;
1403
+ if (mateId && session._mentionSessions && session._mentionSessions[mateId]) {
1404
+ session._mentionSessions[mateId].abort();
1405
+ session._mentionSessions[mateId].close();
1406
+ delete session._mentionSessions[mateId];
1407
+ }
1408
+ session._mentionInProgress = false;
1409
+ sendToSession(session.localId, { type: "mention_done", mateId: mateId, stopped: true });
1410
+ }
1411
+ return;
1412
+ }
1413
+
1384
1414
  // --- Debate ---
1385
1415
  if (msg.type === "debate_start") {
1386
1416
  handleDebateStart(ws, msg);
@@ -1406,6 +1436,9 @@ function createProjectContext(opts) {
1406
1436
  try {
1407
1437
  var entries = fs.readdirSync(knowledgeDir);
1408
1438
  for (var ki = 0; ki < entries.length; ki++) {
1439
+ if (entries[ki] === "session-digests.jsonl") continue;
1440
+ if (entries[ki] === "sticky-notes.md") continue;
1441
+ if (entries[ki] === "memory-summary.md") continue;
1409
1442
  if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
1410
1443
  var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1411
1444
  files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs, common: false });
@@ -1559,6 +1592,56 @@ function createProjectContext(opts) {
1559
1592
  return;
1560
1593
  }
1561
1594
 
1595
+ // --- Memory (session digests) management ---
1596
+ if (msg.type === "memory_list") {
1597
+ var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1598
+ var summaryFile = path.join(cwd, "knowledge", "memory-summary.md");
1599
+ var entries = [];
1600
+ var summary = "";
1601
+ try {
1602
+ var raw = fs.readFileSync(digestFile, "utf8").trim();
1603
+ if (raw) {
1604
+ var lines = raw.split("\n");
1605
+ for (var mi = 0; mi < lines.length; mi++) {
1606
+ try {
1607
+ var obj = JSON.parse(lines[mi]);
1608
+ obj.index = mi;
1609
+ entries.push(obj);
1610
+ } catch (e) {}
1611
+ }
1612
+ }
1613
+ } catch (e) { /* file may not exist */ }
1614
+ try {
1615
+ if (fs.existsSync(summaryFile)) {
1616
+ summary = fs.readFileSync(summaryFile, "utf8").trim();
1617
+ }
1618
+ } catch (e) {}
1619
+ // Return newest first
1620
+ entries.reverse();
1621
+ sendTo(ws, { type: "memory_list", entries: entries, summary: summary });
1622
+ return;
1623
+ }
1624
+
1625
+ if (msg.type === "memory_delete") {
1626
+ if (typeof msg.index !== "number") return;
1627
+ var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1628
+ try {
1629
+ var raw = fs.readFileSync(digestFile, "utf8").trim();
1630
+ var lines = raw ? raw.split("\n") : [];
1631
+ if (msg.index >= 0 && msg.index < lines.length) {
1632
+ lines.splice(msg.index, 1);
1633
+ if (lines.length === 0) {
1634
+ fs.unlinkSync(digestFile);
1635
+ } else {
1636
+ fs.writeFileSync(digestFile, lines.join("\n") + "\n");
1637
+ }
1638
+ }
1639
+ } catch (e) {}
1640
+ sendTo(ws, { type: "memory_deleted", index: msg.index });
1641
+ handleMessage(ws, { type: "memory_list" });
1642
+ return;
1643
+ }
1644
+
1562
1645
  if (msg.type === "push_subscribe") {
1563
1646
  if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
1564
1647
  return;
@@ -1802,6 +1885,8 @@ function createProjectContext(opts) {
1802
1885
  if (v && isNewer(v, currentVersion)) {
1803
1886
  latestVersion = v;
1804
1887
  sendTo(ws, { type: "update_available", version: v });
1888
+ } else {
1889
+ sendTo(ws, { type: "up_to_date", version: currentVersion });
1805
1890
  }
1806
1891
  }).catch(function () {});
1807
1892
  return;
@@ -1908,11 +1993,6 @@ function createProjectContext(opts) {
1908
1993
  }
1909
1994
 
1910
1995
  if (msg.type === "set_permission_mode" && msg.mode) {
1911
- // When dangerouslySkipPermissions is active, don't allow UI to change mode
1912
- if (dangerouslySkipPermissions) {
1913
- send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1914
- return;
1915
- }
1916
1996
  sm.currentPermissionMode = msg.mode;
1917
1997
  var session = getSessionForWs(ws);
1918
1998
  if (session) {
@@ -1926,14 +2006,12 @@ function createProjectContext(opts) {
1926
2006
  if (typeof opts.onSetServerDefaultMode === "function") {
1927
2007
  opts.onSetServerDefaultMode(msg.mode);
1928
2008
  }
1929
- if (!dangerouslySkipPermissions) {
1930
- sm.currentPermissionMode = msg.mode;
1931
- var session = getSessionForWs(ws);
1932
- if (session) {
1933
- sdk.setPermissionMode(session, msg.mode);
1934
- }
1935
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
2009
+ sm.currentPermissionMode = msg.mode;
2010
+ var session = getSessionForWs(ws);
2011
+ if (session) {
2012
+ sdk.setPermissionMode(session, msg.mode);
1936
2013
  }
2014
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1937
2015
  return;
1938
2016
  }
1939
2017
 
@@ -1941,14 +2019,12 @@ function createProjectContext(opts) {
1941
2019
  if (typeof opts.onSetProjectDefaultMode === "function") {
1942
2020
  opts.onSetProjectDefaultMode(slug, msg.mode);
1943
2021
  }
1944
- if (!dangerouslySkipPermissions) {
1945
- sm.currentPermissionMode = msg.mode;
1946
- var session = getSessionForWs(ws);
1947
- if (session) {
1948
- sdk.setPermissionMode(session, msg.mode);
1949
- }
1950
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
2022
+ sm.currentPermissionMode = msg.mode;
2023
+ var session = getSessionForWs(ws);
2024
+ if (session) {
2025
+ sdk.setPermissionMode(session, msg.mode);
1951
2026
  }
2027
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1952
2028
  return;
1953
2029
  }
1954
2030
 
@@ -2127,7 +2203,7 @@ function createProjectContext(opts) {
2127
2203
  var pending = session.pendingAskUser[toolId];
2128
2204
  if (!pending) return;
2129
2205
  delete session.pendingAskUser[toolId];
2130
- sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId });
2206
+ sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
2131
2207
  pending.resolve({
2132
2208
  behavior: "allow",
2133
2209
  updatedInput: Object.assign({}, pending.input, { answers: answers }),
@@ -2324,7 +2400,7 @@ function createProjectContext(opts) {
2324
2400
 
2325
2401
  // --- Browse directories (for add-project autocomplete) ---
2326
2402
  if (msg.type === "browse_dir") {
2327
- var rawPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
2403
+ var rawPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
2328
2404
  var absTarget = path.resolve(rawPath);
2329
2405
  var parentDir, prefix;
2330
2406
  try {
@@ -2363,7 +2439,7 @@ function createProjectContext(opts) {
2363
2439
 
2364
2440
  // --- Add project from web UI ---
2365
2441
  if (msg.type === "add_project") {
2366
- var addPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
2442
+ var addPath = (msg.path || "").replace(/^~/, require("./config").REAL_HOME);
2367
2443
  var addAbs = path.resolve(addPath);
2368
2444
  try {
2369
2445
  var addStat = fs.statSync(addAbs);
@@ -2545,7 +2621,7 @@ function createProjectContext(opts) {
2545
2621
 
2546
2622
  // --- Daemon config / server management (admin-only in multi-user mode) ---
2547
2623
  if (msg.type === "get_daemon_config" || msg.type === "set_pin" || msg.type === "set_keep_awake" ||
2548
- msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
2624
+ msg.type === "set_auto_continue" || msg.type === "set_image_retention" || msg.type === "shutdown_server" || msg.type === "restart_server") {
2549
2625
  if (usersModule.isMultiUser()) {
2550
2626
  var _wsUser = ws._clayUser;
2551
2627
  if (!_wsUser || _wsUser.role !== "admin") {
@@ -2580,6 +2656,15 @@ function createProjectContext(opts) {
2580
2656
  return;
2581
2657
  }
2582
2658
 
2659
+ if (msg.type === "set_auto_continue") {
2660
+ if (typeof opts.onSetAutoContinue === "function") {
2661
+ var acResult = opts.onSetAutoContinue(msg.value);
2662
+ sendTo(ws, { type: "set_auto_continue_result", ok: acResult.ok, autoContinueOnRateLimit: acResult.autoContinueOnRateLimit });
2663
+ send({ type: "auto_continue_changed", autoContinueOnRateLimit: acResult.autoContinueOnRateLimit });
2664
+ }
2665
+ return;
2666
+ }
2667
+
2583
2668
  if (msg.type === "set_image_retention") {
2584
2669
  if (typeof opts.onSetImageRetention === "function") {
2585
2670
  var irResult = opts.onSetImageRetention(msg.days);
@@ -2781,8 +2866,7 @@ function createProjectContext(opts) {
2781
2866
 
2782
2867
  // --- Global CLAUDE.md ---
2783
2868
  if (msg.type === "read_global_claude_md") {
2784
- var os = require("os");
2785
- var globalMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
2869
+ var globalMdPath = path.join(require("./config").REAL_HOME, ".claude", "CLAUDE.md");
2786
2870
  try {
2787
2871
  var globalMdContent = fs.readFileSync(globalMdPath, "utf8");
2788
2872
  sendTo(ws, { type: "global_claude_md_result", content: globalMdContent });
@@ -2793,8 +2877,7 @@ function createProjectContext(opts) {
2793
2877
  }
2794
2878
 
2795
2879
  if (msg.type === "write_global_claude_md") {
2796
- var os2 = require("os");
2797
- var globalMdDir = path.join(os2.homedir(), ".claude");
2880
+ var globalMdDir = path.join(require("./config").REAL_HOME, ".claude");
2798
2881
  var globalMdWritePath = path.join(globalMdDir, "CLAUDE.md");
2799
2882
  try {
2800
2883
  if (!fs.existsSync(globalMdDir)) {
@@ -3037,22 +3120,53 @@ function createProjectContext(opts) {
3037
3120
  }
3038
3121
 
3039
3122
  // --- Sticky notes ---
3123
+ function syncNotesKnowledge() {
3124
+ if (!isMate) return;
3125
+ try {
3126
+ var knDir = path.join(cwd, "knowledge");
3127
+ var knFile = path.join(knDir, "sticky-notes.md");
3128
+ var text = nm.getActiveNotesText();
3129
+ if (text) {
3130
+ fs.mkdirSync(knDir, { recursive: true });
3131
+ fs.writeFileSync(knFile, text);
3132
+ } else {
3133
+ try { fs.unlinkSync(knFile); } catch (e) {}
3134
+ }
3135
+ } catch (e) {
3136
+ console.error("[project] Failed to sync sticky-notes.md:", e.message);
3137
+ }
3138
+ }
3139
+
3040
3140
  if (msg.type === "note_create") {
3041
3141
  var note = nm.create(msg);
3042
- if (note) send({ type: "note_created", note: note });
3142
+ if (note) {
3143
+ send({ type: "note_created", note: note });
3144
+ syncNotesKnowledge();
3145
+ }
3043
3146
  return;
3044
3147
  }
3045
3148
 
3046
3149
  if (msg.type === "note_update") {
3047
3150
  if (!msg.id) return;
3048
3151
  var updated = nm.update(msg.id, msg);
3049
- if (updated) send({ type: "note_updated", note: updated });
3152
+ if (updated) {
3153
+ send({ type: "note_updated", note: updated });
3154
+ if (msg.text !== undefined || msg.hidden !== undefined) syncNotesKnowledge();
3155
+ }
3050
3156
  return;
3051
3157
  }
3052
3158
 
3053
3159
  if (msg.type === "note_delete") {
3054
3160
  if (!msg.id) return;
3055
- if (nm.remove(msg.id)) send({ type: "note_deleted", id: msg.id });
3161
+ if (nm.remove(msg.id)) {
3162
+ send({ type: "note_deleted", id: msg.id });
3163
+ syncNotesKnowledge();
3164
+ }
3165
+ return;
3166
+ }
3167
+
3168
+ if (msg.type === "note_list_request") {
3169
+ sendTo(ws, { type: "notes_list", notes: nm.list() });
3056
3170
  return;
3057
3171
  }
3058
3172
 
@@ -3556,6 +3670,7 @@ function createProjectContext(opts) {
3556
3670
  }
3557
3671
 
3558
3672
  var userMsg = { type: "user_message", text: msg.text || "" };
3673
+ var savedImagePaths = [];
3559
3674
  if (msg.images && msg.images.length > 0) {
3560
3675
  userMsg.imageCount = msg.images.length;
3561
3676
  // Save images as files, store URL references in history
@@ -3565,6 +3680,7 @@ function createProjectContext(opts) {
3565
3680
  var savedName = saveImageFile(img.mediaType, img.data);
3566
3681
  if (savedName) {
3567
3682
  imageRefs.push({ mediaType: img.mediaType, file: savedName });
3683
+ savedImagePaths.push(path.join(imagesDir, savedName));
3568
3684
  }
3569
3685
  }
3570
3686
  if (imageRefs.length > 0) {
@@ -3593,6 +3709,11 @@ function createProjectContext(opts) {
3593
3709
  }
3594
3710
 
3595
3711
  var fullText = msg.text || "";
3712
+ // Prepend saved image paths so Claude can copy/save them
3713
+ if (savedImagePaths.length > 0) {
3714
+ var imgPathLines = savedImagePaths.map(function (p) { return "[Uploaded image: " + p + "]"; }).join("\n");
3715
+ fullText = imgPathLines + (fullText ? "\n" + fullText : "");
3716
+ }
3596
3717
  if (msg.pastes && msg.pastes.length > 0) {
3597
3718
  for (var pi = 0; pi < msg.pastes.length; pi++) {
3598
3719
  if (fullText) fullText += "\n\n";
@@ -3719,63 +3840,226 @@ function createProjectContext(opts) {
3719
3840
  var mateDir = matesModule.getMateDir(mateCtx, mateId);
3720
3841
  var knowledgeDir = path.join(mateDir, "knowledge");
3721
3842
 
3722
- var digestPrompt = [
3723
- "[SYSTEM: Session Digest Request]",
3724
- "The conversation has ended. Summarize this session from YOUR perspective for your long-term memory.",
3725
- "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
3726
- "Schema:",
3727
- "{",
3728
- ' "date": "YYYY-MM-DD",',
3729
- ' "topic": "short topic description",',
3730
- ' "my_position": "what I said/recommended",',
3731
- ' "other_perspectives": "other mates or user perspectives if relevant",',
3732
- ' "decisions": "what was decided, or null if pending",',
3733
- ' "open_items": "what remains unresolved",',
3734
- ' "user_sentiment": "how the user seemed to feel about this topic"',
3735
- "}",
3736
- "",
3737
- "IMPORTANT: Output ONLY the JSON object. Nothing else.",
3738
- ].join("\n");
3843
+ // Migration: generate initial summary if missing
3844
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
3845
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
3846
+ if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
3847
+ initMemorySummary(mateCtx, mateId, function () {});
3848
+ }
3739
3849
 
3740
- var digestText = "";
3741
- mentionSession.pushMessage(digestPrompt, {
3742
- onActivity: function () {},
3743
- onDelta: function (delta) {
3744
- digestText += delta;
3745
- },
3746
- onDone: function () {
3747
- var digestObj = null;
3748
- try {
3749
- var cleaned = digestText.trim();
3750
- if (cleaned.indexOf("```") === 0) {
3751
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
3850
+ // Build conversation content for gate check (500 char cap each side)
3851
+ var userQ = userQuestion || "(unknown)";
3852
+ var mateR = mateResponse || "(unknown)";
3853
+ var conversationContent = "User: " + (userQ.length > 500 ? userQ.substring(0, 500) + "..." : userQ) +
3854
+ "\nMate: " + (mateR.length > 500 ? mateR.substring(0, 500) + "..." : mateR);
3855
+
3856
+ // Gate check: ask Haiku if this is worth remembering
3857
+ gateMemory(mateCtx, mateId, conversationContent, function (shouldRemember) {
3858
+ if (!shouldRemember) {
3859
+ console.log("[digest] Gate declined memory for mention, mate " + mateId);
3860
+ return;
3861
+ }
3862
+
3863
+ var digestPrompt = [
3864
+ "[SYSTEM: Session Digest]",
3865
+ "Summarize this conversation from YOUR perspective for your long-term memory.",
3866
+ "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
3867
+ "",
3868
+ "Schema:",
3869
+ "{",
3870
+ ' "date": "YYYY-MM-DD",',
3871
+ ' "type": "mention",',
3872
+ ' "topic": "short topic description",',
3873
+ ' "my_position": "what I said/recommended",',
3874
+ ' "decisions": "what was decided, or null if pending",',
3875
+ ' "open_items": "what remains unresolved",',
3876
+ ' "user_sentiment": "how the user seemed to feel",',
3877
+ ' "other_perspectives": "key points from others",',
3878
+ ' "confidence": "high | medium | low",',
3879
+ ' "revisit_later": true/false,',
3880
+ ' "tags": ["relevant", "topic", "tags"]',
3881
+ "}",
3882
+ "",
3883
+ "IMPORTANT: Output ONLY the JSON object. Nothing else.",
3884
+ ].join("\n");
3885
+
3886
+ var digestText = "";
3887
+ mentionSession.pushMessage(digestPrompt, {
3888
+ onActivity: function () {},
3889
+ onDelta: function (delta) {
3890
+ digestText += delta;
3891
+ },
3892
+ onDone: function () {
3893
+ var digestObj = null;
3894
+ try {
3895
+ var cleaned = digestText.trim();
3896
+ if (cleaned.indexOf("```") === 0) {
3897
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
3898
+ }
3899
+ digestObj = JSON.parse(cleaned);
3900
+ } catch (e) {
3901
+ console.error("[digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
3902
+ digestObj = {
3903
+ date: new Date().toISOString().slice(0, 10),
3904
+ topic: "parse_failed",
3905
+ raw: digestText.substring(0, 500),
3906
+ };
3752
3907
  }
3753
- digestObj = JSON.parse(cleaned);
3754
- } catch (e) {
3755
- console.error("[digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
3756
- digestObj = {
3757
- date: new Date().toISOString().slice(0, 10),
3758
- topic: "parse_failed",
3759
- raw: digestText.substring(0, 500),
3760
- };
3761
- }
3762
3908
 
3763
- try {
3764
- fs.mkdirSync(knowledgeDir, { recursive: true });
3765
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
3766
- fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
3767
- } catch (e) {
3768
- console.error("[digest] Failed to write digest for mate " + mateId + ":", e.message);
3769
- }
3770
- },
3771
- onError: function (err) {
3772
- console.error("[digest] Digest generation failed for mate " + mateId + ":", err);
3773
- },
3909
+ try {
3910
+ fs.mkdirSync(knowledgeDir, { recursive: true });
3911
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
3912
+ fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
3913
+ } catch (e) {
3914
+ console.error("[digest] Failed to write digest for mate " + mateId + ":", e.message);
3915
+ }
3916
+
3917
+ // Update memory summary
3918
+ updateMemorySummary(mateCtx, mateId, digestObj);
3919
+ },
3920
+ onError: function (err) {
3921
+ console.error("[digest] Digest generation failed for mate " + mateId + ":", err);
3922
+ },
3923
+ });
3924
+ });
3925
+ }
3926
+
3927
+ // Digest DM turn for mate projects - uses Haiku gate + conditional digest + summary update
3928
+ var _dmDigestPending = false;
3929
+ function digestDmTurn(session, responsePreview) {
3930
+ if (!isMate || _dmDigestPending) return;
3931
+ var mateId = path.basename(cwd);
3932
+ var mateCtx = matesModule.buildMateCtx(projectOwnerId);
3933
+ if (!matesModule.isMate(mateCtx, mateId)) return;
3934
+
3935
+ // Extract last user message from history
3936
+ var lastUserText = "";
3937
+ for (var hi = session.history.length - 1; hi >= 0; hi--) {
3938
+ var entry = session.history[hi];
3939
+ if (entry.type === "user_message" && entry.text) {
3940
+ lastUserText = entry.text;
3941
+ break;
3942
+ }
3943
+ }
3944
+
3945
+ // Use responsePreview (full accumulated response) instead of delta fragments
3946
+ var lastResponseText = responsePreview || "";
3947
+ if (!lastUserText && !lastResponseText) return;
3948
+
3949
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
3950
+ var knowledgeDir = path.join(mateDir, "knowledge");
3951
+
3952
+ // Migration: if memory-summary.md missing but digests exist, generate initial summary
3953
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
3954
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
3955
+ if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
3956
+ initMemorySummary(mateCtx, mateId, function () {
3957
+ console.log("[memory-migrate] Initial summary generated for mate " + mateId);
3958
+ });
3959
+ }
3960
+
3961
+ var conversationContent = "User: " + (lastUserText.length > 500 ? lastUserText.substring(0, 500) + "..." : lastUserText) +
3962
+ "\nMate: " + (lastResponseText.length > 500 ? lastResponseText.substring(0, 500) + "..." : lastResponseText);
3963
+
3964
+ _dmDigestPending = true;
3965
+
3966
+ // Gate check: ask Haiku if this is worth remembering
3967
+ gateMemory(mateCtx, mateId, conversationContent, function (shouldRemember) {
3968
+ if (!shouldRemember) {
3969
+ _dmDigestPending = false;
3970
+ console.log("[dm-digest] Gate declined memory for DM, mate " + mateId);
3971
+ return;
3972
+ }
3973
+
3974
+ var digestContext = [
3975
+ "[SYSTEM: Session Digest]",
3976
+ "Summarize this conversation from YOUR perspective for your long-term memory.",
3977
+ "",
3978
+ conversationContent,
3979
+ ].join("\n");
3980
+
3981
+ var digestPrompt = [
3982
+ "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
3983
+ "",
3984
+ "Schema:",
3985
+ "{",
3986
+ ' "date": "YYYY-MM-DD",',
3987
+ ' "type": "dm",',
3988
+ ' "topic": "short topic description",',
3989
+ ' "my_position": "what I said/recommended",',
3990
+ ' "decisions": "what was decided, or null if pending",',
3991
+ ' "open_items": "what remains unresolved",',
3992
+ ' "user_sentiment": "how the user seemed to feel",',
3993
+ ' "user_intent": "what the user wanted",',
3994
+ ' "confidence": "high | medium | low",',
3995
+ ' "revisit_later": true/false,',
3996
+ ' "tags": ["relevant", "topic", "tags"]',
3997
+ "}",
3998
+ "",
3999
+ "IMPORTANT: Output ONLY the JSON object. Nothing else.",
4000
+ ].join("\n");
4001
+
4002
+ var digestText = "";
4003
+ var _digestSession = null;
4004
+ sdk.createMentionSession({
4005
+ claudeMd: "",
4006
+ model: "haiku",
4007
+ initialContext: digestContext,
4008
+ initialMessage: digestPrompt,
4009
+ onActivity: function () {},
4010
+ onDelta: function (delta) {
4011
+ digestText += delta;
4012
+ },
4013
+ onDone: function () {
4014
+ _dmDigestPending = false;
4015
+ var digestObj = null;
4016
+ try {
4017
+ var cleaned = digestText.trim();
4018
+ if (cleaned.indexOf("```") === 0) {
4019
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
4020
+ }
4021
+ digestObj = JSON.parse(cleaned);
4022
+ } catch (e) {
4023
+ console.error("[dm-digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
4024
+ digestObj = {
4025
+ date: new Date().toISOString().slice(0, 10),
4026
+ type: "dm",
4027
+ topic: "parse_failed",
4028
+ raw: digestText.substring(0, 500),
4029
+ };
4030
+ }
4031
+
4032
+ try {
4033
+ fs.mkdirSync(knowledgeDir, { recursive: true });
4034
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4035
+ fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
4036
+ } catch (e) {
4037
+ console.error("[dm-digest] Failed to write digest for mate " + mateId + ":", e.message);
4038
+ }
4039
+
4040
+ // Update memory summary
4041
+ updateMemorySummary(mateCtx, mateId, digestObj);
4042
+
4043
+ if (_digestSession) try { _digestSession.close(); } catch (e) {}
4044
+ },
4045
+ onError: function (err) {
4046
+ _dmDigestPending = false;
4047
+ console.error("[dm-digest] Digest generation failed for mate " + mateId + ":", err);
4048
+ if (_digestSession) try { _digestSession.close(); } catch (e) {}
4049
+ },
4050
+ }).then(function (ds) {
4051
+ _digestSession = ds;
4052
+ if (!ds) _dmDigestPending = false;
4053
+ }).catch(function (err) {
4054
+ _dmDigestPending = false;
4055
+ console.error("[dm-digest] Failed to create digest session for mate " + mateId + ":", err);
4056
+ });
3774
4057
  });
3775
4058
  }
3776
4059
 
3777
4060
  function handleMention(ws, msg) {
3778
- if (!msg.mateId || !msg.text) return;
4061
+ if (!msg.mateId) return;
4062
+ if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
3779
4063
 
3780
4064
  var session = getSessionForWs(ws);
3781
4065
  if (!session) return;
@@ -3806,7 +4090,7 @@ function createProjectContext(opts) {
3806
4090
  var avatarSeed = (mate.profile && mate.profile.avatarSeed) || mate.id;
3807
4091
 
3808
4092
  // Build full mention text (include pasted content)
3809
- var mentionFullInput = msg.text;
4093
+ var mentionFullInput = msg.text || "";
3810
4094
  if (msg.pastes && msg.pastes.length > 0) {
3811
4095
  for (var pi = 0; pi < msg.pastes.length; pi++) {
3812
4096
  if (mentionFullInput) mentionFullInput += "\n\n";
@@ -3814,12 +4098,25 @@ function createProjectContext(opts) {
3814
4098
  }
3815
4099
  }
3816
4100
 
4101
+ // Save images to disk (same pattern as regular messages)
4102
+ var imageRefs = [];
4103
+ if (msg.images && msg.images.length > 0) {
4104
+ for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
4105
+ var img = msg.images[imgIdx];
4106
+ var savedName = saveImageFile(img.mediaType, img.data);
4107
+ if (savedName) {
4108
+ imageRefs.push({ mediaType: img.mediaType, file: savedName });
4109
+ }
4110
+ }
4111
+ }
4112
+
3817
4113
  // Save mention user message to session history
3818
4114
  var mentionUserEntry = { type: "mention_user", text: msg.text, mateId: msg.mateId, mateName: mateName };
3819
4115
  if (msg.pastes && msg.pastes.length > 0) mentionUserEntry.pastes = msg.pastes;
4116
+ if (imageRefs.length > 0) mentionUserEntry.imageRefs = imageRefs;
3820
4117
  session.history.push(mentionUserEntry);
3821
4118
  sm.appendToSessionFile(session, mentionUserEntry);
3822
- sendToSessionOthers(ws, session.localId, mentionUserEntry);
4119
+ sendToSessionOthers(ws, session.localId, hydrateImageRefs(mentionUserEntry));
3823
4120
 
3824
4121
  // Extract recent turns for continuity check
3825
4122
  var recentTurns = getRecentTurns(session, MENTION_WINDOW);
@@ -3912,7 +4209,7 @@ function createProjectContext(opts) {
3912
4209
  // Continue existing mention session with middle context
3913
4210
  var middleContext = buildMiddleContext(recentTurns, msg.mateId);
3914
4211
  var continuationText = middleContext ? middleContext + "\n\n" + mentionFullInput : mentionFullInput;
3915
- existingSession.pushMessage(continuationText, mentionCallbacks);
4212
+ existingSession.pushMessage(continuationText, mentionCallbacks, msg.images);
3916
4213
  } else {
3917
4214
  // Clean up old session if it exists
3918
4215
  if (existingSession) {
@@ -3929,26 +4226,8 @@ function createProjectContext(opts) {
3929
4226
  // CLAUDE.md may not exist for new mates
3930
4227
  }
3931
4228
 
3932
- // Load recent session digests for context continuity
3933
- var recentDigests = "";
3934
- try {
3935
- var digestFile = path.join(mateDir, "knowledge", "session-digests.jsonl");
3936
- if (fs.existsSync(digestFile)) {
3937
- var allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n");
3938
- var recent = allLines.slice(-5); // last 5 digests
3939
- if (recent.length > 0) {
3940
- recentDigests = "\n\nYour recent session memories (from past @mentions):\n";
3941
- for (var di = 0; di < recent.length; di++) {
3942
- try {
3943
- var d = JSON.parse(recent[di]);
3944
- recentDigests += "- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
3945
- (d.decisions ? " | Decisions: " + d.decisions : "") +
3946
- (d.open_items ? " | Open: " + d.open_items : "") + "\n";
3947
- } catch (e) {}
3948
- }
3949
- }
3950
- }
3951
- } catch (e) {}
4229
+ // Load session digests (unified: uses memory-summary.md if available)
4230
+ var recentDigests = loadMateDigests(mateCtx, msg.mateId);
3952
4231
 
3953
4232
  // Build initial mention context
3954
4233
  var mentionContext = buildMentionContext(userName, recentTurns) + recentDigests;
@@ -3958,6 +4237,7 @@ function createProjectContext(opts) {
3958
4237
  claudeMd: claudeMd,
3959
4238
  initialContext: mentionContext,
3960
4239
  initialMessage: mentionFullInput,
4240
+ initialImages: msg.images || null,
3961
4241
  onActivity: mentionCallbacks.onActivity,
3962
4242
  onDelta: mentionCallbacks.onDelta,
3963
4243
  onDone: mentionCallbacks.onDone,
@@ -4035,11 +4315,17 @@ function createProjectContext(opts) {
4035
4315
  // Sort by length descending to match longest name first
4036
4316
  names.sort(function (a, b) { return b.length - a.length; });
4037
4317
  var mentioned = [];
4318
+ // Strip markdown inline formatting so **@Name**, ~~@Name~~, `@Name`, [@Name](url) etc. still match
4319
+ var cleaned = text
4320
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) -> text
4321
+ .replace(/`([^`]*)`/g, "$1") // `code` -> code
4322
+ .replace(/(\*{1,3}|_{1,3}|~{2})/g, ""); // bold, italic, strikethrough markers
4038
4323
  console.log("[debate-mention] nameMap keys:", JSON.stringify(names));
4039
- console.log("[debate-mention] text snippet:", text.slice(0, 200));
4324
+ console.log("[debate-mention] text snippet:", cleaned.slice(0, 200));
4040
4325
  for (var i = 0; i < names.length; i++) {
4041
- var pattern = new RegExp("@" + escapeRegex(names[i]) + "(?=[\\s,.:;!?()\\]}>\"']|$)", "i");
4042
- var matched = pattern.test(text);
4326
+ // Match @Name followed by any non-name character (not alphanumeric, not Korean, not dash/underscore)
4327
+ var pattern = new RegExp("@" + escapeRegex(names[i]) + "(?![\\p{L}\\p{N}_-])", "iu");
4328
+ var matched = pattern.test(cleaned);
4043
4329
  console.log("[debate-mention] testing @" + names[i] + " pattern=" + pattern.toString() + " matched=" + matched);
4044
4330
  if (matched) {
4045
4331
  var mateId = nameMap[names[i]];
@@ -4071,27 +4357,369 @@ function createProjectContext(opts) {
4071
4357
  }
4072
4358
  }
4073
4359
 
4360
+ function formatRawDigests(rawLines, headerLabel) {
4361
+ if (!rawLines || rawLines.length === 0) return "";
4362
+ var lines = ["\n\n" + (headerLabel || "Your recent session memories:")];
4363
+ for (var i = 0; i < rawLines.length; i++) {
4364
+ try {
4365
+ var d = JSON.parse(rawLines[i]);
4366
+ if (d.type === "debate" && d.my_role) {
4367
+ // Debate memories are role-played positions, not genuine opinions
4368
+ lines.push("- [" + (d.date || "?") + "] DEBATE (role: " + d.my_role + ") " + (d.topic || "unknown") +
4369
+ ": argued " + (d.my_position || "N/A") + " (assigned role, not my actual opinion)" +
4370
+ (d.outcome ? " | Outcome: " + d.outcome : "") +
4371
+ (d.open_items ? " | Open: " + d.open_items : ""));
4372
+ } else {
4373
+ lines.push("- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
4374
+ (d.decisions ? " | Decisions: " + d.decisions : "") +
4375
+ (d.open_items ? " | Open: " + d.open_items : ""));
4376
+ }
4377
+ } catch (e) {}
4378
+ }
4379
+ return lines.join("\n");
4380
+ }
4381
+
4074
4382
  function loadMateDigests(mateCtx, mateId) {
4075
4383
  var mateDir = matesModule.getMateDir(mateCtx, mateId);
4384
+ var knowledgeDir = path.join(mateDir, "knowledge");
4385
+
4386
+ // Check for memory-summary.md first
4387
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4388
+ var hasSummary = false;
4389
+ var summaryContent = "";
4076
4390
  try {
4077
- var digestFile = path.join(mateDir, "knowledge", "session-digests.jsonl");
4078
- if (!fs.existsSync(digestFile)) return "";
4079
- var allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n");
4391
+ if (fs.existsSync(summaryFile)) {
4392
+ summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4393
+ if (summaryContent) hasSummary = true;
4394
+ }
4395
+ } catch (e) {}
4396
+
4397
+ // Load raw digests
4398
+ var allLines = [];
4399
+ try {
4400
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4401
+ if (fs.existsSync(digestFile)) {
4402
+ allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4403
+ }
4404
+ } catch (e) {}
4405
+
4406
+ if (hasSummary) {
4407
+ // Load summary + latest 3 raw digests
4408
+ var recent = allLines.slice(-3);
4409
+ var result = "\n\nYour memory summary:\n" + summaryContent;
4410
+ if (recent.length > 0) {
4411
+ result += formatRawDigests(recent, "Latest raw session memories:");
4412
+ }
4413
+ return result;
4414
+ } else {
4415
+ // Backward compatible: latest 5 raw digests
4080
4416
  var recent = allLines.slice(-5);
4081
- if (recent.length === 0) return "";
4082
- var lines = ["\n\nYour recent session memories:"];
4083
- for (var i = 0; i < recent.length; i++) {
4417
+ return formatRawDigests(recent, "Your recent session memories:");
4418
+ }
4419
+ }
4420
+
4421
+ // Gate check: ask Haiku whether this conversation contains anything worth remembering
4422
+ function gateMemory(mateCtx, mateId, conversationContent, callback, opts) {
4423
+ opts = opts || {};
4424
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
4425
+ var knowledgeDir = path.join(mateDir, "knowledge");
4426
+
4427
+ // Load mate role/activities from mate.yaml (lightweight, no full CLAUDE.md)
4428
+ var mateRole = "";
4429
+ var mateActivities = "";
4430
+ try {
4431
+ var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
4432
+ var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
4433
+ var actMatch = yamlRaw.match(/^activities:\s*(.+)$/m);
4434
+ if (roleMatch) mateRole = roleMatch[1].trim();
4435
+ if (actMatch) mateActivities = actMatch[1].trim();
4436
+ } catch (e) {}
4437
+
4438
+ // Load existing memory summary if available
4439
+ var summaryContent = "";
4440
+ try {
4441
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4442
+ if (fs.existsSync(summaryFile)) {
4443
+ summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4444
+ }
4445
+ } catch (e) {}
4446
+
4447
+ // Cap conversation content for gate (500 chars each side)
4448
+ var cappedContent = conversationContent;
4449
+ if (cappedContent.length > 1200) {
4450
+ cappedContent = cappedContent.substring(0, 1200) + "...";
4451
+ }
4452
+
4453
+ var gateContext = [
4454
+ "[SYSTEM: Memory Gate]",
4455
+ "You are a memory filter for an AI Mate.",
4456
+ "",
4457
+ "Mate role: " + (mateRole || "assistant"),
4458
+ "Mate activities: " + (mateActivities || "general"),
4459
+ "",
4460
+ "Current memory summary:",
4461
+ summaryContent || "No memory summary yet.",
4462
+ "",
4463
+ "Conversation just ended:",
4464
+ cappedContent,
4465
+ ].join("\n");
4466
+
4467
+ var gatePrompt = opts.gatePrompt || [
4468
+ 'Should this conversation be saved to long-term memory?',
4469
+ 'Answer "yes" ONLY if there is:',
4470
+ "- A new decision or commitment",
4471
+ "- A change in position or strategy",
4472
+ "- New information relevant to this Mate's role",
4473
+ "- A user preference or pattern not already in the summary",
4474
+ "",
4475
+ 'Answer "no" if:',
4476
+ "- It duplicates what is already in the memory summary",
4477
+ "- It is casual/trivial conversation",
4478
+ "- It is not relevant to this Mate's role",
4479
+ "",
4480
+ 'Answer with ONLY "yes" or "no". Nothing else.',
4481
+ ].join("\n");
4482
+ var defaultOnError = !!opts.defaultYes;
4483
+
4484
+ var gateText = "";
4485
+ var _gateSession = null;
4486
+ sdk.createMentionSession({
4487
+ claudeMd: "",
4488
+ model: "haiku",
4489
+ initialContext: gateContext,
4490
+ initialMessage: gatePrompt,
4491
+ onActivity: function () {},
4492
+ onDelta: function (delta) {
4493
+ gateText += delta;
4494
+ },
4495
+ onDone: function () {
4496
+ var answer = gateText.trim().toLowerCase();
4497
+ var shouldRemember = answer.indexOf("yes") !== -1;
4498
+ if (_gateSession) try { _gateSession.close(); } catch (e) {}
4499
+ callback(shouldRemember);
4500
+ },
4501
+ onError: function (err) {
4502
+ console.error("[memory-gate] Gate check failed for mate " + mateId + ":", err);
4503
+ if (_gateSession) try { _gateSession.close(); } catch (e) {}
4504
+ callback(defaultOnError);
4505
+ },
4506
+ }).then(function (gs) {
4507
+ _gateSession = gs;
4508
+ if (!gs) callback(defaultOnError);
4509
+ }).catch(function (err) {
4510
+ console.error("[memory-gate] Failed to create gate session for mate " + mateId + ":", err);
4511
+ callback(defaultOnError);
4512
+ });
4513
+ }
4514
+
4515
+ // Update (or create) memory-summary.md based on a new digest
4516
+ function updateMemorySummary(mateCtx, mateId, digestObj) {
4517
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
4518
+ var knowledgeDir = path.join(mateDir, "knowledge");
4519
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4520
+
4521
+ // Check if summary exists; if not, try initial generation first
4522
+ var summaryExists = false;
4523
+ var summaryContent = "";
4524
+ try {
4525
+ if (fs.existsSync(summaryFile)) {
4526
+ summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4527
+ if (summaryContent) summaryExists = true;
4528
+ }
4529
+ } catch (e) {}
4530
+
4531
+ if (!summaryExists) {
4532
+ // Try initial summary generation from existing digests (migration)
4533
+ initMemorySummary(mateCtx, mateId, function () {
4534
+ // After init, do incremental update with the new digest
4535
+ doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
4536
+ });
4537
+ } else {
4538
+ doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
4539
+ }
4540
+ }
4541
+
4542
+ // Incremental update of memory-summary.md with a single new digest
4543
+ function doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj) {
4544
+ var existingSummary = "";
4545
+ try {
4546
+ if (fs.existsSync(summaryFile)) {
4547
+ existingSummary = fs.readFileSync(summaryFile, "utf8").trim();
4548
+ }
4549
+ } catch (e) {}
4550
+
4551
+ var updateContext = [
4552
+ "[SYSTEM: Memory Summary Update]",
4553
+ "You are updating an AI Mate's long-term memory summary.",
4554
+ "",
4555
+ "Current summary:",
4556
+ existingSummary || "(empty, this is the first entry)",
4557
+ "",
4558
+ "New session digest to incorporate:",
4559
+ JSON.stringify(digestObj, null, 2),
4560
+ ].join("\n");
4561
+
4562
+ var updatePrompt = [
4563
+ "Update the summary by:",
4564
+ "1. Adding new information from this session",
4565
+ "2. Updating existing entries if positions changed",
4566
+ "3. Moving resolved open threads out of \"Open Threads\"",
4567
+ "4. Adding to \"My Track Record\" if a past prediction/recommendation can now be evaluated",
4568
+ "5. Removing outdated or redundant information",
4569
+ "",
4570
+ "Maintain this structure:",
4571
+ "",
4572
+ "# Memory Summary",
4573
+ "Last updated: YYYY-MM-DD (session count: N+1)",
4574
+ "",
4575
+ "## User Patterns",
4576
+ "## Key Decisions",
4577
+ "## My Track Record",
4578
+ "## Open Threads",
4579
+ "## Recurring Topics",
4580
+ "",
4581
+ "Keep it concise. Each section should have at most 10 bullet points.",
4582
+ "Drop the oldest/least relevant if needed.",
4583
+ "Output ONLY the updated markdown. Nothing else.",
4584
+ ].join("\n");
4585
+
4586
+ var updateText = "";
4587
+ var _updateSession = null;
4588
+ sdk.createMentionSession({
4589
+ claudeMd: "",
4590
+ model: "haiku",
4591
+ initialContext: updateContext,
4592
+ initialMessage: updatePrompt,
4593
+ onActivity: function () {},
4594
+ onDelta: function (delta) {
4595
+ updateText += delta;
4596
+ },
4597
+ onDone: function () {
4084
4598
  try {
4085
- var d = JSON.parse(recent[i]);
4086
- lines.push("- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
4087
- (d.decisions ? " | Decisions: " + d.decisions : "") +
4088
- (d.open_items ? " | Open: " + d.open_items : "") );
4089
- } catch (e) {}
4599
+ var cleaned = updateText.trim();
4600
+ if (cleaned.indexOf("```") === 0) {
4601
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
4602
+ }
4603
+ fs.mkdirSync(knowledgeDir, { recursive: true });
4604
+ fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
4605
+ console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
4606
+ } catch (e) {
4607
+ console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
4608
+ }
4609
+ if (_updateSession) try { _updateSession.close(); } catch (e) {}
4610
+ },
4611
+ onError: function (err) {
4612
+ console.error("[memory-summary] Summary update failed for mate " + mateId + ":", err);
4613
+ if (_updateSession) try { _updateSession.close(); } catch (e) {}
4614
+ },
4615
+ }).then(function (us) {
4616
+ _updateSession = us;
4617
+ }).catch(function (err) {
4618
+ console.error("[memory-summary] Failed to create summary update session for mate " + mateId + ":", err);
4619
+ });
4620
+ }
4621
+
4622
+ // Initial summary generation (migration): read latest 20 digests and generate first summary
4623
+ function initMemorySummary(mateCtx, mateId, callback) {
4624
+ var mateDir = matesModule.getMateDir(mateCtx, mateId);
4625
+ var knowledgeDir = path.join(mateDir, "knowledge");
4626
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4627
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4628
+
4629
+ // Check if digests exist
4630
+ var allLines = [];
4631
+ try {
4632
+ if (fs.existsSync(digestFile)) {
4633
+ allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4090
4634
  }
4091
- return lines.join("\n");
4092
- } catch (e) {
4093
- return "";
4635
+ } catch (e) {}
4636
+
4637
+ if (allLines.length === 0) {
4638
+ // No digests to summarize, just callback
4639
+ callback();
4640
+ return;
4641
+ }
4642
+
4643
+ var recent = allLines.slice(-20);
4644
+ var digestsText = [];
4645
+ for (var i = 0; i < recent.length; i++) {
4646
+ try {
4647
+ var d = JSON.parse(recent[i]);
4648
+ digestsText.push(JSON.stringify(d));
4649
+ } catch (e) {}
4650
+ }
4651
+
4652
+ if (digestsText.length === 0) {
4653
+ callback();
4654
+ return;
4094
4655
  }
4656
+
4657
+ var initContext = [
4658
+ "[SYSTEM: Initial Memory Summary]",
4659
+ "You are creating the first long-term memory summary for an AI Mate.",
4660
+ "",
4661
+ "Here are the most recent session digests (up to 20):",
4662
+ digestsText.join("\n"),
4663
+ ].join("\n");
4664
+
4665
+ var initPrompt = [
4666
+ "Create a memory summary from these sessions.",
4667
+ "",
4668
+ "Structure:",
4669
+ "",
4670
+ "# Memory Summary",
4671
+ "Last updated: YYYY-MM-DD (session count: N)",
4672
+ "",
4673
+ "## User Patterns",
4674
+ "## Key Decisions",
4675
+ "## My Track Record",
4676
+ "## Open Threads",
4677
+ "## Recurring Topics",
4678
+ "",
4679
+ "Keep it concise. Focus on patterns and decisions, not individual session details.",
4680
+ "Each section should have at most 10 bullet points.",
4681
+ "Set session count to " + digestsText.length + ".",
4682
+ "Output ONLY the markdown. Nothing else.",
4683
+ ].join("\n");
4684
+
4685
+ var initText = "";
4686
+ var _initSession = null;
4687
+ sdk.createMentionSession({
4688
+ claudeMd: "",
4689
+ model: "haiku",
4690
+ initialContext: initContext,
4691
+ initialMessage: initPrompt,
4692
+ onActivity: function () {},
4693
+ onDelta: function (delta) {
4694
+ initText += delta;
4695
+ },
4696
+ onDone: function () {
4697
+ try {
4698
+ var cleaned = initText.trim();
4699
+ if (cleaned.indexOf("```") === 0) {
4700
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
4701
+ }
4702
+ fs.mkdirSync(knowledgeDir, { recursive: true });
4703
+ fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
4704
+ console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
4705
+ } catch (e) {
4706
+ console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
4707
+ }
4708
+ if (_initSession) try { _initSession.close(); } catch (e) {}
4709
+ callback();
4710
+ },
4711
+ onError: function (err) {
4712
+ console.error("[memory-summary] Initial summary generation failed for mate " + mateId + ":", err);
4713
+ if (_initSession) try { _initSession.close(); } catch (e) {}
4714
+ callback();
4715
+ },
4716
+ }).then(function (is) {
4717
+ _initSession = is;
4718
+ if (!is) callback();
4719
+ }).catch(function (err) {
4720
+ console.error("[memory-summary] Failed to create init summary session for mate " + mateId + ":", err);
4721
+ callback();
4722
+ });
4095
4723
  }
4096
4724
 
4097
4725
  function buildModeratorContext(debate) {
@@ -4372,7 +5000,11 @@ function createProjectContext(opts) {
4372
5000
  if (h.type === "debate_conclude_confirm") hasConclude = true;
4373
5001
  if (h.type === "debate_turn_done" && h.role === "moderator") lastModText = h.text || "";
4374
5002
  }
4375
- if (!hasEnded && !hasConclude && lastModText !== null) {
5003
+ // conclude_confirm in history without a subsequent ended = still awaiting user decision
5004
+ if (hasConclude && !hasEnded) {
5005
+ debate.awaitingConcludeConfirm = true;
5006
+ } else if (!hasEnded && !hasConclude && lastModText !== null) {
5007
+ // No explicit entry yet; infer from last moderator text having no @mentions
4376
5008
  var mentions = detectMentions(lastModText, debate.nameMap);
4377
5009
  if (mentions.length === 0) {
4378
5010
  debate.awaitingConcludeConfirm = true;
@@ -4646,6 +5278,7 @@ function createProjectContext(opts) {
4646
5278
  console.log("[debate] No mentions detected, requesting user confirmation to end.");
4647
5279
  debate.turnInProgress = false;
4648
5280
  debate.awaitingConcludeConfirm = true;
5281
+ persistDebateState(session);
4649
5282
  var concludeEntry = { type: "debate_conclude_confirm", topic: debate.topic, round: debate.round };
4650
5283
  session.history.push(concludeEntry);
4651
5284
  sm.appendToSessionFile(session, concludeEntry);
@@ -5254,68 +5887,86 @@ function createProjectContext(opts) {
5254
5887
  var mateDir = matesModule.getMateDir(debate.mateCtx, mateId);
5255
5888
  var knowledgeDir = path.join(mateDir, "knowledge");
5256
5889
 
5257
- var digestPrompt = [
5258
- "[SYSTEM: Debate Session Digest Request]",
5259
- "The debate has ended. Summarize this debate from YOUR perspective for your long-term memory.",
5260
- "Topic: " + debate.topic,
5261
- "Your role: " + role,
5262
- "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
5263
- "Schema:",
5264
- "{",
5265
- ' "date": "YYYY-MM-DD",',
5266
- ' "type": "debate",',
5267
- ' "topic": "the debate topic",',
5268
- ' "my_role": "your role in the debate",',
5269
- ' "my_position": "what you argued/said",',
5270
- ' "other_perspectives": "key points from other participants",',
5271
- ' "outcome": "how the debate concluded",',
5272
- ' "open_items": "unresolved points"',
5273
- "}",
5274
- "",
5275
- "IMPORTANT: Output ONLY the JSON object. Nothing else.",
5276
- ].join("\n");
5890
+ // Migration: generate initial summary if missing
5891
+ var summaryFile = path.join(knowledgeDir, "memory-summary.md");
5892
+ var digestFileCheck = path.join(knowledgeDir, "session-digests.jsonl");
5893
+ if (!fs.existsSync(summaryFile) && fs.existsSync(digestFileCheck)) {
5894
+ initMemorySummary(debate.mateCtx, mateId, function () {});
5895
+ }
5896
+
5897
+ // Debates are user-initiated structured events. The moderator already
5898
+ // synthesizes a summary, so skip the memory gate and always create a digest.
5899
+ (function () {
5900
+ var digestPrompt = [
5901
+ "[SYSTEM: Session Digest]",
5902
+ "Summarize this conversation from YOUR perspective for your long-term memory.",
5903
+ "Output ONLY a single valid JSON object (no markdown, no code fences, no extra text).",
5904
+ "",
5905
+ "Schema:",
5906
+ "{",
5907
+ ' "date": "YYYY-MM-DD",',
5908
+ ' "type": "debate",',
5909
+ ' "topic": "short topic description",',
5910
+ ' "my_position": "what I said/recommended",',
5911
+ ' "decisions": "what was decided, or null if pending",',
5912
+ ' "open_items": "what remains unresolved",',
5913
+ ' "user_sentiment": "how the user seemed to feel",',
5914
+ ' "my_role": "' + role + '",',
5915
+ ' "other_perspectives": "key points from others",',
5916
+ ' "outcome": "how the debate concluded",',
5917
+ ' "confidence": "high | medium | low",',
5918
+ ' "revisit_later": true/false,',
5919
+ ' "tags": ["relevant", "topic", "tags"]',
5920
+ "}",
5921
+ "",
5922
+ "IMPORTANT: Output ONLY the JSON object. Nothing else.",
5923
+ ].join("\n");
5924
+
5925
+ var digestText = "";
5926
+ mentionSession.pushMessage(digestPrompt, {
5927
+ onActivity: function () {},
5928
+ onDelta: function (delta) {
5929
+ digestText += delta;
5930
+ },
5931
+ onDone: function () {
5932
+ var digestObj = null;
5933
+ try {
5934
+ var cleaned = digestText.trim();
5935
+ if (cleaned.indexOf("```") === 0) {
5936
+ cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5937
+ }
5938
+ digestObj = JSON.parse(cleaned);
5939
+ } catch (e) {
5940
+ console.error("[debate-digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
5941
+ digestObj = {
5942
+ date: new Date().toISOString().slice(0, 10),
5943
+ type: "debate",
5944
+ topic: debate.topic,
5945
+ my_role: role,
5946
+ raw: digestText.substring(0, 500),
5947
+ };
5948
+ }
5277
5949
 
5278
- var digestText = "";
5279
- mentionSession.pushMessage(digestPrompt, {
5280
- onActivity: function () {},
5281
- onDelta: function (delta) {
5282
- digestText += delta;
5283
- },
5284
- onDone: function () {
5285
- var digestObj = null;
5286
- try {
5287
- var cleaned = digestText.trim();
5288
- if (cleaned.indexOf("```") === 0) {
5289
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5950
+ try {
5951
+ fs.mkdirSync(knowledgeDir, { recursive: true });
5952
+ var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
5953
+ fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
5954
+ } catch (e) {
5955
+ console.error("[debate-digest] Failed to write digest for mate " + mateId + ":", e.message);
5290
5956
  }
5291
- digestObj = JSON.parse(cleaned);
5292
- } catch (e) {
5293
- console.error("[debate-digest] Failed to parse digest JSON for mate " + mateId + ":", e.message);
5294
- digestObj = {
5295
- date: new Date().toISOString().slice(0, 10),
5296
- type: "debate",
5297
- topic: debate.topic,
5298
- my_role: role,
5299
- raw: digestText.substring(0, 500),
5300
- };
5301
- }
5302
5957
 
5303
- try {
5304
- fs.mkdirSync(knowledgeDir, { recursive: true });
5305
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
5306
- fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
5307
- } catch (e) {
5308
- console.error("[debate-digest] Failed to write digest for mate " + mateId + ":", e.message);
5309
- }
5958
+ // Update memory summary
5959
+ updateMemorySummary(debate.mateCtx, mateId, digestObj);
5310
5960
 
5311
- // Close the session after digest
5312
- mentionSession.close();
5313
- },
5314
- onError: function (err) {
5315
- console.error("[debate-digest] Digest generation failed for mate " + mateId + ":", err);
5316
- mentionSession.close();
5317
- },
5318
- });
5961
+ // Close the session after digest
5962
+ mentionSession.close();
5963
+ },
5964
+ onError: function (err) {
5965
+ console.error("[debate-digest] Digest generation failed for mate " + mateId + ":", err);
5966
+ mentionSession.close();
5967
+ },
5968
+ });
5969
+ })();
5319
5970
  }
5320
5971
 
5321
5972
  // --- Session presence (who is viewing which session) ---
@@ -5568,7 +6219,7 @@ function createProjectContext(opts) {
5568
6219
  return;
5569
6220
  }
5570
6221
  var skillUserInfo = getOsUserInfoForReq(req);
5571
- var spawnCwd = scope === "global" ? (skillUserInfo ? skillUserInfo.home : os.homedir()) : cwd;
6222
+ var spawnCwd = scope === "global" ? (skillUserInfo ? skillUserInfo.home : require("./config").REAL_HOME) : cwd;
5572
6223
  var scopeFlag = scope === "global" ? "--global" : "--project";
5573
6224
  var skillSpawnOpts = {
5574
6225
  cwd: spawnCwd,
@@ -5658,7 +6309,7 @@ function createProjectContext(opts) {
5658
6309
  return;
5659
6310
  }
5660
6311
  var uninstallUserInfo = getOsUserInfoForReq(req);
5661
- var baseDir = scope === "global" ? (uninstallUserInfo ? uninstallUserInfo.home : os.homedir()) : cwd;
6312
+ var baseDir = scope === "global" ? (uninstallUserInfo ? uninstallUserInfo.home : require("./config").REAL_HOME) : cwd;
5662
6313
  var skillDir = path.join(baseDir, ".claude", "skills", skill);
5663
6314
  // Safety: ensure skillDir is inside the expected .claude/skills directory
5664
6315
  var expectedParent = path.join(baseDir, ".claude", "skills");
@@ -5709,7 +6360,7 @@ function createProjectContext(opts) {
5709
6360
  // Installed skills (global + project)
5710
6361
  if (req.method === "GET" && urlPath === "/api/installed-skills") {
5711
6362
  var installed = {};
5712
- var globalDir = path.join(os.homedir(), ".claude", "skills");
6363
+ var globalDir = path.join(require("./config").REAL_HOME, ".claude", "skills");
5713
6364
  var projectDir = path.join(cwd, ".claude", "skills");
5714
6365
  var scanDirs = [
5715
6366
  { dir: globalDir, scope: "global" },
@@ -5763,7 +6414,7 @@ function createProjectContext(opts) {
5763
6414
  return;
5764
6415
  }
5765
6416
  // Read installed versions
5766
- var globalSkillsDir = path.join(os.homedir(), ".claude", "skills");
6417
+ var globalSkillsDir = path.join(require("./config").REAL_HOME, ".claude", "skills");
5767
6418
  var projectSkillsDir = path.join(cwd, ".claude", "skills");
5768
6419
  var results = [];
5769
6420
  var pending = skills.length;
@@ -5992,6 +6643,7 @@ function createProjectContext(opts) {
5992
6643
  };
5993
6644
  if (isMate) {
5994
6645
  status.isMate = true;
6646
+ status.mateId = path.basename(cwd);
5995
6647
  }
5996
6648
  if (worktreeMeta) {
5997
6649
  status.isWorktree = true;
@@ -6037,7 +6689,20 @@ function createProjectContext(opts) {
6037
6689
  // Enforce immediately on startup
6038
6690
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
6039
6691
  try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
6692
+ try { matesModule.enforceStickyNotes(claudeMdPath); } catch (e) {}
6040
6693
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
6694
+ // Sync sticky notes knowledge file on startup
6695
+ try {
6696
+ var knDir = path.join(cwd, "knowledge");
6697
+ var knFile = path.join(knDir, "sticky-notes.md");
6698
+ var notesText = nm.getActiveNotesText();
6699
+ if (notesText) {
6700
+ fs.mkdirSync(knDir, { recursive: true });
6701
+ fs.writeFileSync(knFile, notesText);
6702
+ } else {
6703
+ try { fs.unlinkSync(knFile); } catch (e) {}
6704
+ }
6705
+ } catch (e) {}
6041
6706
  // Watch for changes
6042
6707
  try {
6043
6708
  crisisWatcher = fs.watch(claudeMdPath, function () {
@@ -6046,6 +6711,7 @@ function createProjectContext(opts) {
6046
6711
  crisisDebounce = null;
6047
6712
  try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
6048
6713
  try { matesModule.enforceSessionMemory(claudeMdPath); } catch (e) {}
6714
+ try { matesModule.enforceStickyNotes(claudeMdPath); } catch (e) {}
6049
6715
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
6050
6716
  }, 500);
6051
6717
  });