clay-server 2.26.0-beta.4 → 2.26.0-beta.6

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,6 +16,8 @@ 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
 
21
23
  // --- Context Sources persistence ---
@@ -159,8 +161,13 @@ function createProjectContext(opts) {
159
161
  var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
160
162
  var isMate = opts.isMate || false;
161
163
  var onCreateWorktree = opts.onCreateWorktree || null;
164
+ var serverPort = opts.port || 2633;
165
+ var serverTls = opts.tls || false;
162
166
  var latestVersion = null;
163
167
 
168
+ // Browser MCP server runs in-process via createSdkMcpServer (no child process spawn).
169
+ // Do NOT write to .claude-local/settings.json -- the SDK reads that too, causing duplicate spawns.
170
+
164
171
  // --- Chat image storage ---
165
172
  var _imgConfig = require("./config");
166
173
  var _imgUtils = require("./utils");
@@ -170,7 +177,18 @@ function createProjectContext(opts) {
170
177
 
171
178
  // Convert imageRefs in history entries to images with URLs for the client
172
179
  function hydrateImageRefs(entry) {
173
- if (!entry || !entry.imageRefs) return entry;
180
+ if (!entry) return entry;
181
+ // Hydrate context_preview: convert screenshotFile to screenshotUrl
182
+ if (entry.type === "context_preview" && entry.tab && entry.tab.screenshotFile) {
183
+ var hydrated = {};
184
+ for (var k in entry) hydrated[k] = entry[k];
185
+ hydrated.tab = {};
186
+ for (var tk in entry.tab) hydrated.tab[tk] = entry.tab[tk];
187
+ hydrated.tab.screenshotUrl = "/p/" + slug + "/images/" + entry.tab.screenshotFile;
188
+ delete hydrated.tab.screenshotFile;
189
+ return hydrated;
190
+ }
191
+ if (!entry.imageRefs) return entry;
174
192
  if (entry.type !== "user_message" && entry.type !== "mention_user") return entry;
175
193
  var images = [];
176
194
  for (var ri = 0; ri < entry.imageRefs.length; ri++) {
@@ -178,8 +196,8 @@ function createProjectContext(opts) {
178
196
  images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
179
197
  }
180
198
  var hydrated = {};
181
- for (var k in entry) {
182
- if (k !== "imageRefs") hydrated[k] = entry[k];
199
+ for (var k2 in entry) {
200
+ if (k2 !== "imageRefs") hydrated[k2] = entry[k2];
183
201
  }
184
202
  hydrated.images = images;
185
203
  return hydrated;
@@ -273,6 +291,60 @@ function createProjectContext(opts) {
273
291
  // --- Per-project clients ---
274
292
  var clients = new Set();
275
293
 
294
+ // --- Browser extension state ---
295
+ var _browserTabList = {}; // tabId -> { id, url, title, favIconUrl }
296
+ var _extensionWs = null; // WebSocket of the client with the Chrome extension
297
+ var _extToken = crypto.randomUUID(); // Auth token for MCP server bridge
298
+ var pendingExtensionRequests = {}; // requestId -> { resolve, timer }
299
+
300
+ function sendExtensionCommand(ws, command, args, timeout) {
301
+ return new Promise(function(resolve) {
302
+ var requestId = crypto.randomUUID();
303
+ var ms = timeout || 3000;
304
+ var timer = setTimeout(function() {
305
+ delete pendingExtensionRequests[requestId];
306
+ resolve(null);
307
+ }, ms);
308
+ pendingExtensionRequests[requestId] = { resolve: resolve, timer: timer };
309
+ sendTo(ws, {
310
+ type: "extension_command",
311
+ command: command,
312
+ args: args,
313
+ requestId: requestId
314
+ });
315
+ });
316
+ }
317
+
318
+ // Send extension command via the tracked extension client (for MCP bridge)
319
+ function sendExtensionCommandAny(command, args, timeout) {
320
+ if (!_extensionWs || _extensionWs.readyState !== 1) {
321
+ return Promise.reject(new Error("Browser extension not connected"));
322
+ }
323
+ return sendExtensionCommand(_extensionWs, command, args, timeout);
324
+ }
325
+
326
+ function requestTabContext(ws, tabId) {
327
+ // Try inject first (best-effort), then request all data in parallel.
328
+ // Even if inject fails (CSP etc.), page text and screenshot still work.
329
+ return sendExtensionCommand(ws, "tab_inject", { tabId: tabId }).then(function() {}, function() {}).then(function() {
330
+ return Promise.all([
331
+ sendExtensionCommand(ws, "tab_console", { tabId: tabId }),
332
+ sendExtensionCommand(ws, "tab_network", { tabId: tabId }),
333
+ sendExtensionCommand(ws, "tab_page_text", { tabId: tabId }),
334
+ sendExtensionCommand(ws, "tab_screenshot", { tabId: tabId })
335
+ ]);
336
+ }).then(function(results) {
337
+ return {
338
+ console: results[0],
339
+ network: results[1],
340
+ pageText: results[2],
341
+ screenshot: results[3]
342
+ };
343
+ }).catch(function() {
344
+ return null;
345
+ });
346
+ }
347
+
276
348
  function send(obj) {
277
349
  var data = JSON.stringify(obj);
278
350
  for (var ws of clients) {
@@ -485,6 +557,45 @@ function createProjectContext(opts) {
485
557
  mateDisplayName: opts.mateDisplayName || "",
486
558
  isMate: isMate,
487
559
  dangerouslySkipPermissions: dangerouslySkipPermissions,
560
+ mcpServers: isMate ? undefined : (function () {
561
+ try {
562
+ var browserMcp = require("./browser-mcp-server");
563
+ var mcpConfig = browserMcp.create(sendExtensionCommandAny, function () {
564
+ return Object.values(_browserTabList || {});
565
+ }, {
566
+ watchTab: function (tabId) {
567
+ var key = "tab:" + tabId;
568
+ var active = loadContextSources(slug);
569
+ if (active.indexOf(key) === -1) {
570
+ active.push(key);
571
+ saveContextSources(slug, active);
572
+ var msg = JSON.stringify({ type: "context_sources_state", active: active });
573
+ for (var c of clients) { if (c.readyState === 1) c.send(msg); }
574
+ }
575
+ return active;
576
+ },
577
+ unwatchTab: function (tabId) {
578
+ var key = "tab:" + tabId;
579
+ var active = loadContextSources(slug);
580
+ var idx = active.indexOf(key);
581
+ if (idx !== -1) {
582
+ active.splice(idx, 1);
583
+ saveContextSources(slug, active);
584
+ var msg = JSON.stringify({ type: "context_sources_state", active: active });
585
+ for (var c of clients) { if (c.readyState === 1) c.send(msg); }
586
+ }
587
+ return active;
588
+ },
589
+ });
590
+ if (!mcpConfig) return undefined;
591
+ var servers = {};
592
+ servers[mcpConfig.name || "clay-browser"] = mcpConfig;
593
+ return servers;
594
+ } catch (e) {
595
+ console.error("[project] Failed to create browser MCP server:", e.message);
596
+ return undefined;
597
+ }
598
+ })(),
488
599
  onProcessingChanged: onProcessingChanged,
489
600
  onTurnDone: isMate ? function (session, preview) { digestDmTurn(session, preview); } : null,
490
601
  scheduleMessage: function (session, text, resetsAt) {
@@ -1261,7 +1372,9 @@ function createProjectContext(opts) {
1261
1372
  }
1262
1373
  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 });
1263
1374
  sendTo(ws, { type: "term_list", terminals: tm.list() });
1264
- sendTo(ws, { type: "context_sources_state", active: loadContextSources(slug) });
1375
+ // Restore context sources (keep tab: sources — validated against _browserTabList at query time)
1376
+ var restoredSources = loadContextSources(slug);
1377
+ sendTo(ws, { type: "context_sources_state", active: restoredSources });
1265
1378
  sendTo(ws, { type: "notes_list", notes: nm.list() });
1266
1379
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
1267
1380
 
@@ -1410,7 +1523,19 @@ function createProjectContext(opts) {
1410
1523
  }
1411
1524
  sendTo(ws, hydrateImageRefs(_hitem));
1412
1525
  }
1413
- sendTo(ws, { type: "history_done" });
1526
+ // Include last result data + cached context usage for accurate restore
1527
+ var _lastUsage = null, _lastModelUsage = null, _lastCost = null, _lastStreamInputTokens = null;
1528
+ for (var _ri = total - 1; _ri >= 0; _ri--) {
1529
+ if (active.history[_ri].type === "result") {
1530
+ var _r = active.history[_ri];
1531
+ _lastUsage = _r.usage || null;
1532
+ _lastModelUsage = _r.modelUsage || null;
1533
+ _lastCost = _r.cost != null ? _r.cost : null;
1534
+ _lastStreamInputTokens = _r.lastStreamInputTokens || null;
1535
+ break;
1536
+ }
1537
+ }
1538
+ sendTo(ws, { type: "history_done", lastUsage: _lastUsage, lastModelUsage: _lastModelUsage, lastCost: _lastCost, lastStreamInputTokens: _lastStreamInputTokens, contextUsage: active.lastContextUsage || null });
1414
1539
 
1415
1540
  if (active.isProcessing) {
1416
1541
  sendTo(ws, { type: "status", status: "processing" });
@@ -1721,80 +1846,10 @@ function createProjectContext(opts) {
1721
1846
  return;
1722
1847
  }
1723
1848
 
1724
- // --- Memory (session digests) management ---
1725
- if (msg.type === "memory_list") {
1726
- var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1727
- var summaryFile = path.join(cwd, "knowledge", "memory-summary.md");
1728
- var entries = [];
1729
- var summary = "";
1730
- try {
1731
- var raw = fs.readFileSync(digestFile, "utf8").trim();
1732
- if (raw) {
1733
- var lines = raw.split("\n");
1734
- for (var mi = 0; mi < lines.length; mi++) {
1735
- try {
1736
- var obj = JSON.parse(lines[mi]);
1737
- obj.index = mi;
1738
- entries.push(obj);
1739
- } catch (e) {}
1740
- }
1741
- }
1742
- } catch (e) { /* file may not exist */ }
1743
- try {
1744
- if (fs.existsSync(summaryFile)) {
1745
- summary = fs.readFileSync(summaryFile, "utf8").trim();
1746
- }
1747
- } catch (e) {}
1748
- // Return newest first
1749
- entries.reverse();
1750
- sendTo(ws, { type: "memory_list", entries: entries, summary: summary });
1751
- return;
1752
- }
1753
-
1754
- if (msg.type === "memory_search") {
1755
- if (!msg.query || typeof msg.query !== "string") {
1756
- sendTo(ws, { type: "memory_search_results", results: [], query: "" });
1757
- return;
1758
- }
1759
- var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1760
- try {
1761
- var results = sessionSearch.searchDigests(digestFile, msg.query, {
1762
- maxResults: msg.maxResults || 10,
1763
- minScore: msg.minScore || 0.5,
1764
- dateFrom: msg.dateFrom || null,
1765
- dateTo: msg.dateTo || null
1766
- });
1767
- sendTo(ws, {
1768
- type: "memory_search_results",
1769
- results: sessionSearch.formatForMemoryUI(results),
1770
- query: msg.query
1771
- });
1772
- } catch (e) {
1773
- console.error("[session-search] Search failed:", e.message);
1774
- sendTo(ws, { type: "memory_search_results", results: [], query: msg.query });
1775
- }
1776
- return;
1777
- }
1778
-
1779
- if (msg.type === "memory_delete") {
1780
- if (typeof msg.index !== "number") return;
1781
- var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
1782
- try {
1783
- var raw = fs.readFileSync(digestFile, "utf8").trim();
1784
- var lines = raw ? raw.split("\n") : [];
1785
- if (msg.index >= 0 && msg.index < lines.length) {
1786
- lines.splice(msg.index, 1);
1787
- if (lines.length === 0) {
1788
- fs.unlinkSync(digestFile);
1789
- } else {
1790
- fs.writeFileSync(digestFile, lines.join("\n") + "\n");
1791
- }
1792
- }
1793
- } catch (e) {}
1794
- sendTo(ws, { type: "memory_deleted", index: msg.index });
1795
- handleMessage(ws, { type: "memory_list" });
1796
- return;
1797
- }
1849
+ // --- Memory (session digests) management (delegated to project-memory.js) ---
1850
+ if (msg.type === "memory_list") { _memory.handleMemoryList(ws); return; }
1851
+ if (msg.type === "memory_search") { _memory.handleMemorySearch(ws, msg); return; }
1852
+ if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
1798
1853
 
1799
1854
  if (msg.type === "push_subscribe") {
1800
1855
  var _pushUserId = ws._clayUser ? ws._clayUser.id : null;
@@ -3428,6 +3483,27 @@ function createProjectContext(opts) {
3428
3483
  return;
3429
3484
  }
3430
3485
 
3486
+ // --- Browser Extension ---
3487
+ if (msg.type === "browser_tab_list") {
3488
+ _extensionWs = ws; // Track which client has the extension
3489
+ var tabs = msg.tabs || [];
3490
+ _browserTabList = {};
3491
+ for (var bti = 0; bti < tabs.length; bti++) {
3492
+ _browserTabList[tabs[bti].id] = tabs[bti];
3493
+ }
3494
+ return;
3495
+ }
3496
+
3497
+ if (msg.type === "extension_result") {
3498
+ var pending = pendingExtensionRequests[msg.requestId];
3499
+ if (pending) {
3500
+ clearTimeout(pending.timer);
3501
+ pending.resolve(msg.result);
3502
+ delete pendingExtensionRequests[msg.requestId];
3503
+ }
3504
+ return;
3505
+ }
3506
+
3431
3507
  // --- Scheduled tasks permission gate ---
3432
3508
  if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
3433
3509
  msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
@@ -3971,1326 +4047,229 @@ function createProjectContext(opts) {
3971
4047
  }
3972
4048
  }
3973
4049
 
3974
- if (!session.isProcessing) {
3975
- session.isProcessing = true;
3976
- onProcessingChanged();
3977
- session.sentToolResults = {};
3978
- sendToSession(session.localId, { type: "status", status: "processing" });
3979
- if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
3980
- // No active query (or worker idle between queries): start a new query
3981
- session._queryStartTs = Date.now();
3982
- console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
3983
- sdk.startQuery(session, fullText, msg.images, getLinuxUserForSession(session));
3984
- } else {
3985
- sdk.pushMessage(session, fullText, msg.images);
3986
- }
3987
- } else {
3988
- sdk.pushMessage(session, fullText, msg.images);
3989
- }
3990
- sm.broadcastSessionList();
3991
- }
3992
-
3993
- // --- @Mention handler ---
3994
- var MENTION_WINDOW = 20; // turns to check for session continuity
3995
-
3996
- function getRecentTurns(session, n) {
3997
- var turns = [];
3998
- var history = session.history;
3999
- // Walk backwards through history, collect user/assistant/mention text turns
4000
- var assistantBuffer = "";
4001
- for (var i = history.length - 1; i >= 0 && turns.length < n; i--) {
4002
- var entry = history[i];
4003
- if (entry.type === "user_message") {
4004
- if (assistantBuffer) {
4005
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
4006
- assistantBuffer = "";
4007
- }
4008
- turns.push({ role: "user", text: entry.text || "" });
4009
- } else if (entry.type === "delta" || entry.type === "text") {
4010
- assistantBuffer = (entry.text || "") + assistantBuffer;
4011
- } else if (entry.type === "mention_response") {
4012
- if (assistantBuffer) {
4013
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
4014
- assistantBuffer = "";
4015
- }
4016
- turns.push({ role: "@" + (entry.mateName || "Mate"), text: entry.text || "", mateId: entry.mateId });
4017
- } else if (entry.type === "mention_user") {
4018
- if (assistantBuffer) {
4019
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
4020
- assistantBuffer = "";
4021
- }
4022
- turns.push({ role: "user", text: "@" + (entry.mateName || "Mate") + " " + (entry.text || ""), mateId: entry.mateId });
4023
- }
4024
- }
4025
- if (assistantBuffer) {
4026
- turns.push({ role: "assistant", text: assistantBuffer.trim() });
4027
- }
4028
- turns.reverse();
4029
- return turns;
4030
- }
4031
-
4032
- // Check if the given mate has a mention response in the recent window
4033
- function hasMateInWindow(recentTurns, mateId) {
4034
- for (var i = 0; i < recentTurns.length; i++) {
4035
- if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
4036
- return true;
4037
- }
4038
- }
4039
- return false;
4040
- }
4041
-
4042
- // Build the "middle context": conversation turns since the mate's last response
4043
- function buildMiddleContext(recentTurns, mateId) {
4044
- // Find the last mention response from this mate
4045
- var lastIdx = -1;
4046
- for (var i = recentTurns.length - 1; i >= 0; i--) {
4047
- if (recentTurns[i].mateId === mateId && recentTurns[i].role.charAt(0) === "@") {
4048
- lastIdx = i;
4049
- break;
4050
- }
4051
- }
4052
- if (lastIdx === -1 || lastIdx >= recentTurns.length - 1) return "";
4053
-
4054
- // Collect turns after the last mention response
4055
- var lines = ["[Conversation since your last response:]", "---"];
4056
- for (var j = lastIdx + 1; j < recentTurns.length; j++) {
4057
- var turn = recentTurns[j];
4058
- lines.push(turn.role + ": " + turn.text);
4059
- }
4060
- lines.push("---");
4061
- return lines.join("\n");
4062
- }
4063
-
4064
- function buildMentionContext(userName, recentTurns) {
4065
- var lines = [
4066
- "You were @mentioned in a project session by " + userName + ".",
4067
- "You are responding inline in their conversation. Keep your response focused on what was asked.",
4068
- "You have read-only access to the project files but cannot make changes.",
4069
- "",
4070
- "Recent conversation context:",
4071
- "---",
4072
- ];
4073
- for (var i = 0; i < recentTurns.length; i++) {
4074
- var turn = recentTurns[i];
4075
- lines.push(turn.role + ": " + turn.text);
4076
- }
4077
- lines.push("---");
4078
- return lines.join("\n");
4079
- }
4080
-
4081
- // --- Shared digest worker: one reusable Haiku session for gate+digest ---
4082
- // Combines gate check and digest generation into a single prompt,
4083
- // processes jobs sequentially from a queue, reuses the session across calls.
4084
- // Session is recycled after DIGEST_WORKER_MAX_TURNS to prevent context bloat.
4085
- var _digestWorker = null;
4086
- var _digestQueue = [];
4087
- var _digestBusy = false;
4088
- var _digestWorkerTurns = 0;
4089
- var DIGEST_WORKER_MAX_TURNS = 20;
4090
-
4091
- function enqueueDigest(job) {
4092
- _digestQueue.push(job);
4093
- if (!_digestBusy) processDigestQueue();
4094
- }
4095
-
4096
- function processDigestQueue() {
4097
- if (_digestQueue.length === 0) { _digestBusy = false; return; }
4098
- _digestBusy = true;
4099
- var job = _digestQueue.shift();
4100
-
4101
- var mateDir = matesModule.getMateDir(job.mateCtx, job.mateId);
4102
- var knowledgeDir = path.join(mateDir, "knowledge");
4103
-
4104
- // Load mate role for gate context
4105
- var mateRole = "";
4106
- try {
4107
- var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
4108
- var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
4109
- if (roleMatch) mateRole = roleMatch[1].trim();
4110
- } catch (e) {}
4111
-
4112
- // Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
4113
- var prompt = [
4114
- "[SYSTEM: Memory Gate + Digest]",
4115
- "You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
4116
- "",
4117
- "Conversation (" + job.type + "):",
4118
- job.conversationContent,
4119
- "",
4120
- "STEP 1: Should this be saved to memory?",
4121
- 'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
4122
- "When in doubt, save it.",
4123
- "",
4124
- 'STEP 2: If yes, output a JSON digest. If no, output exactly: {"skip":true}',
4125
- "",
4126
- "JSON schema (output ONLY the JSON, no markdown, no fences):",
4127
- "{",
4128
- ' "date": "YYYY-MM-DD",',
4129
- ' "type": "' + job.type + '",',
4130
- ' "topic": "short topic description",',
4131
- ' "summary": "2-3 sentence summary",',
4132
- ' "key_quotes": ["user quotes, verbatim, max 5"],',
4133
- ' "user_context": "personal/project context or null",',
4134
- ' "my_position": "what I said/recommended",',
4135
- job.type === "dm" ? ' "user_intent": "what the user wanted",' : ' "other_perspectives": "key points from others",',
4136
- ' "decisions": "what was decided or null",',
4137
- ' "open_items": "what remains unresolved",',
4138
- ' "user_sentiment": "how user felt",',
4139
- ' "confidence": "high|medium|low",',
4140
- ' "revisit_later": true/false,',
4141
- ' "tags": ["topic", "tags"],',
4142
- ' "user_observations": [{"category":"pattern|decision|reaction|preference","observation":"...","evidence":"..."}]',
4143
- "}",
4144
- "",
4145
- "user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
4146
- "Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
4147
- "Omit the field entirely if nothing notable about the user.",
4148
- ].join("\n");
4149
-
4150
- function handleResult(text) {
4151
- var cleaned = text.trim();
4152
- if (cleaned.indexOf("```") === 0) {
4153
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
4154
- }
4155
-
4156
- var digestObj = null;
4157
- try { digestObj = JSON.parse(cleaned); } catch (e) {
4158
- console.error("[digest-worker] Parse failed for " + job.mateId + ":", e.message);
4159
- digestObj = { date: new Date().toISOString().slice(0, 10), topic: "parse_failed", raw: text.substring(0, 500) };
4160
- }
4161
-
4162
- if (digestObj && digestObj.skip) {
4163
- console.log("[digest-worker] Gate declined for " + job.mateId);
4164
- if (job.onDone) job.onDone();
4165
- processDigestQueue();
4166
- return;
4167
- }
4168
-
4169
- try {
4170
- fs.mkdirSync(knowledgeDir, { recursive: true });
4171
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4172
- fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
4173
- } catch (e) {
4174
- console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
4175
- }
4176
-
4177
- // Write user observations if present
4178
- if (digestObj.user_observations && digestObj.user_observations.length > 0) {
4179
- try {
4180
- var obsFile = path.join(knowledgeDir, "user-observations.jsonl");
4181
- var obsMate = matesModule.getMate(job.mateCtx, job.mateId);
4182
- var obsMateName = (obsMate && obsMate.name) || job.mateId;
4183
- var obsLines = [];
4184
- for (var oi = 0; oi < digestObj.user_observations.length; oi++) {
4185
- var obs = digestObj.user_observations[oi];
4186
- obsLines.push(JSON.stringify({
4187
- date: digestObj.date || new Date().toISOString().slice(0, 10),
4188
- category: obs.category || "pattern",
4189
- observation: obs.observation || "",
4190
- evidence: obs.evidence || "",
4191
- confidence: digestObj.confidence || "medium",
4192
- mateName: obsMateName,
4193
- mateId: job.mateId
4194
- }));
4195
- }
4196
- fs.appendFileSync(obsFile, obsLines.join("\n") + "\n");
4197
- } catch (e) {
4198
- console.error("[digest-worker] Observations write failed for " + job.mateId + ":", e.message);
4199
- }
4200
- }
4201
-
4202
- updateMemorySummary(job.mateCtx, job.mateId, digestObj);
4203
- maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
4204
- if (job.onDone) job.onDone();
4205
- processDigestQueue();
4206
- }
4207
-
4208
- // Recycle worker session if it has exceeded max turns
4209
- if (_digestWorker && _digestWorkerTurns >= DIGEST_WORKER_MAX_TURNS) {
4210
- try { _digestWorker.close(); } catch (e) {}
4211
- _digestWorker = null;
4212
- _digestWorkerTurns = 0;
4213
- }
4214
-
4215
- var responseText = "";
4216
- if (_digestWorker && _digestWorker.isAlive()) {
4217
- _digestWorkerTurns++;
4218
- _digestWorker.pushMessage(prompt, {
4219
- onActivity: function () {},
4220
- onDelta: function (d) { responseText += d; },
4221
- onDone: function () { handleResult(responseText); },
4222
- onError: function (err) {
4223
- console.error("[digest-worker] Error:", err);
4224
- _digestWorker = null;
4225
- _digestWorkerTurns = 0;
4226
- if (job.onDone) job.onDone();
4227
- processDigestQueue();
4228
- },
4229
- });
4230
- } else {
4231
- sdk.createMentionSession({
4232
- claudeMd: "",
4233
- model: "haiku",
4234
- initialContext: "[Digest Worker] You generate memory digests. Respond with ONLY JSON.",
4235
- initialMessage: prompt,
4236
- onActivity: function () {},
4237
- onDelta: function (d) { responseText += d; },
4238
- onDone: function () { handleResult(responseText); },
4239
- onError: function (err) {
4240
- console.error("[digest-worker] Create error:", err);
4241
- _digestWorker = null;
4242
- if (job.onDone) job.onDone();
4243
- processDigestQueue();
4244
- },
4245
- }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
4246
- if (job.onDone) job.onDone();
4247
- processDigestQueue();
4248
- });
4249
- }
4250
- }
4251
-
4252
- function digestMentionSession(session, mateId, mateCtx, mateResponse, userQuestion) {
4253
- if (!session._mentionSessions || !session._mentionSessions[mateId]) return;
4254
- var mentionSession = session._mentionSessions[mateId];
4255
- if (!mentionSession.isAlive()) return;
4256
-
4257
- mentionSession._digesting = true;
4258
-
4259
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4260
- var knowledgeDir = path.join(mateDir, "knowledge");
4261
-
4262
- // Migration: generate initial summary if missing
4263
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4264
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4265
- if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
4266
- initMemorySummary(mateCtx, mateId, function () {});
4267
- }
4268
-
4269
- var userQ = userQuestion || "(unknown)";
4270
- var mateR = mateResponse || "(unknown)";
4271
- var conversationContent = "User: " + (userQ.length > 2000 ? userQ.substring(0, 2000) + "..." : userQ) +
4272
- "\nMate: " + (mateR.length > 2000 ? mateR.substring(0, 2000) + "..." : mateR);
4273
-
4274
- enqueueDigest({
4275
- mateCtx: mateCtx,
4276
- mateId: mateId,
4277
- type: "mention",
4278
- conversationContent: conversationContent,
4279
- onDone: function () { mentionSession._digesting = false; },
4050
+ // Collect browser tab context (async: requires round-trip to client extension)
4051
+ var tabSources = ctxSources.filter(function(id) {
4052
+ if (!id.startsWith("tab:")) return false;
4053
+ // Only include tabs that currently exist in the browser
4054
+ var tid = parseInt(id.split(":")[1], 10);
4055
+ return !!_browserTabList[tid];
4280
4056
  });
4281
- }
4282
4057
 
4283
- // Digest DM turn for mate projects - uses shared digest worker
4284
- var _dmDigestPending = false;
4285
- function digestDmTurn(session, responsePreview) {
4286
- if (!isMate || _dmDigestPending) return;
4287
- var mateId = path.basename(cwd);
4288
- var mateCtx = matesModule.buildMateCtx(projectOwnerId);
4289
- if (!matesModule.isMate(mateCtx, mateId)) return;
4290
-
4291
- // Collect full conversation from session history (all user + mate turns)
4292
- var conversationParts = [];
4293
- var totalLen = 0;
4294
- var CONV_CAP = 6000;
4295
- for (var hi = 0; hi < session.history.length; hi++) {
4296
- var entry = session.history[hi];
4297
- if (entry.type === "user_message" && entry.text) {
4298
- var uText = entry.text;
4299
- if (totalLen + uText.length > CONV_CAP) {
4300
- uText = uText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
4301
- }
4302
- conversationParts.push("User: " + uText);
4303
- totalLen += uText.length;
4304
- } else if (entry.type === "assistant_message" && entry.text) {
4305
- var aText = entry.text;
4306
- if (totalLen + aText.length > CONV_CAP) {
4307
- aText = aText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
4308
- }
4309
- conversationParts.push("Mate: " + aText);
4310
- totalLen += aText.length;
4311
- }
4312
- if (totalLen >= CONV_CAP) break;
4313
- }
4314
- var lastResponseText = responsePreview || "";
4315
- if (lastResponseText && conversationParts.length > 0) {
4316
- var lastPart = conversationParts[conversationParts.length - 1];
4317
- if (lastPart.indexOf("Mate:") !== 0 || lastPart.indexOf(lastResponseText.substring(0, 50)) === -1) {
4318
- var rText = lastResponseText;
4319
- if (totalLen + rText.length > CONV_CAP) {
4320
- rText = rText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
4058
+ function dispatchToSdk(finalText) {
4059
+ if (!session.isProcessing) {
4060
+ session.isProcessing = true;
4061
+ onProcessingChanged();
4062
+ session.sentToolResults = {};
4063
+ sendToSession(session.localId, { type: "status", status: "processing" });
4064
+ if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
4065
+ // No active query (or worker idle between queries): start a new query
4066
+ session._queryStartTs = Date.now();
4067
+ console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
4068
+ sdk.startQuery(session, finalText, msg.images, getLinuxUserForSession(session));
4069
+ } else {
4070
+ sdk.pushMessage(session, finalText, msg.images);
4321
4071
  }
4322
- conversationParts.push("Mate: " + rText);
4072
+ } else {
4073
+ sdk.pushMessage(session, finalText, msg.images);
4323
4074
  }
4075
+ sm.broadcastSessionList();
4324
4076
  }
4325
- if (conversationParts.length === 0) return;
4326
4077
 
4327
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4328
- var knowledgeDir = path.join(mateDir, "knowledge");
4329
-
4330
- // Migration: if memory-summary.md missing but digests exist, generate initial summary
4331
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4332
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4333
- if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
4334
- initMemorySummary(mateCtx, mateId, function () {
4335
- console.log("[memory-migrate] Initial summary generated for mate " + mateId);
4078
+ if (tabSources.length > 0) {
4079
+ // Request tab context from all active browser tab sources
4080
+ var tabPromises = tabSources.map(function(srcId) {
4081
+ var tabId = parseInt(srcId.split(":")[1], 10);
4082
+ return requestTabContext(ws, tabId);
4336
4083
  });
4337
- }
4338
-
4339
- _dmDigestPending = true;
4340
-
4341
- enqueueDigest({
4342
- mateCtx: mateCtx,
4343
- mateId: mateId,
4344
- type: "dm",
4345
- conversationContent: conversationParts.join("\n"),
4346
- onDone: function () { _dmDigestPending = false; },
4347
- });
4348
- }
4349
-
4350
- function handleMention(ws, msg) {
4351
- if (!msg.mateId) return;
4352
- if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
4353
-
4354
- var session = getSessionForWs(ws);
4355
- if (!session) return;
4356
-
4357
- // Block mentions during an active debate
4358
- if (session._debate && session._debate.phase === "live") {
4359
- sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Cannot use @mentions during an active debate." });
4360
- return;
4361
- }
4362
-
4363
- // Check if a mention is already in progress for this session
4364
- if (session._mentionInProgress) {
4365
- sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "A mention is already in progress." });
4366
- return;
4367
- }
4368
-
4369
- var userId = ws._clayUser ? ws._clayUser.id : null;
4370
- var mateCtx = matesModule.buildMateCtx(userId);
4371
- var mate = matesModule.getMate(mateCtx, msg.mateId);
4372
- if (!mate) {
4373
- sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Mate not found" });
4374
- return;
4375
- }
4376
-
4377
- var mateName = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
4378
- var avatarColor = (mate.profile && mate.profile.avatarColor) || "#6c5ce7";
4379
- var avatarStyle = (mate.profile && mate.profile.avatarStyle) || "bottts";
4380
- var avatarSeed = (mate.profile && mate.profile.avatarSeed) || mate.id;
4381
-
4382
- // Build full mention text (include pasted content)
4383
- var mentionFullInput = msg.text || "";
4384
- if (msg.pastes && msg.pastes.length > 0) {
4385
- for (var pi = 0; pi < msg.pastes.length; pi++) {
4386
- if (mentionFullInput) mentionFullInput += "\n\n";
4387
- mentionFullInput += msg.pastes[pi];
4388
- }
4389
- }
4390
-
4391
- // Save images to disk (same pattern as regular messages)
4392
- var imageRefs = [];
4393
- if (msg.images && msg.images.length > 0) {
4394
- for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
4395
- var img = msg.images[imgIdx];
4396
- var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
4397
- if (savedName) {
4398
- imageRefs.push({ mediaType: img.mediaType, file: savedName });
4399
- }
4400
- }
4401
- }
4402
-
4403
- // Save mention user message to session history
4404
- var mentionUserEntry = { type: "mention_user", text: msg.text, mateId: msg.mateId, mateName: mateName };
4405
- if (msg.pastes && msg.pastes.length > 0) mentionUserEntry.pastes = msg.pastes;
4406
- if (imageRefs.length > 0) mentionUserEntry.imageRefs = imageRefs;
4407
- session.history.push(mentionUserEntry);
4408
- sm.appendToSessionFile(session, mentionUserEntry);
4409
- sendToSessionOthers(ws, session.localId, hydrateImageRefs(mentionUserEntry));
4410
-
4411
- // Extract recent turns for continuity check
4412
- var recentTurns = getRecentTurns(session, MENTION_WINDOW);
4413
-
4414
- // Determine user name for context
4415
- var userName = "User";
4416
- if (ws._clayUser) {
4417
- var p = ws._clayUser.profile || {};
4418
- userName = p.name || ws._clayUser.displayName || ws._clayUser.username || "User";
4419
- }
4420
-
4421
- session._mentionInProgress = true;
4422
-
4423
- // Send mention start indicator
4424
- sendToSession(session.localId, {
4425
- type: "mention_start",
4426
- mateId: msg.mateId,
4427
- mateName: mateName,
4428
- avatarColor: avatarColor,
4429
- avatarStyle: avatarStyle,
4430
- avatarSeed: avatarSeed,
4431
- });
4432
-
4433
- // Shared callbacks for both new and continued sessions
4434
- var mentionCallbacks = {
4435
- onActivity: function (activity) {
4436
- sendToSession(session.localId, {
4437
- type: "mention_activity",
4438
- mateId: msg.mateId,
4439
- activity: activity,
4440
- });
4441
- },
4442
- onDelta: function (delta) {
4443
- sendToSession(session.localId, {
4444
- type: "mention_stream",
4445
- mateId: msg.mateId,
4446
- mateName: mateName,
4447
- delta: delta,
4448
- });
4449
- },
4450
- onDone: function (fullText) {
4451
- session._mentionInProgress = false;
4452
-
4453
- // Save mention response to session history
4454
- var mentionResponseEntry = {
4455
- type: "mention_response",
4456
- mateId: msg.mateId,
4457
- mateName: mateName,
4458
- text: fullText,
4459
- avatarColor: avatarColor,
4460
- avatarStyle: avatarStyle,
4461
- avatarSeed: avatarSeed,
4462
- };
4463
- session.history.push(mentionResponseEntry);
4464
- sm.appendToSessionFile(session, mentionResponseEntry);
4465
-
4466
- // Queue mention context for injection into the current agent's next turn
4467
- if (!session.pendingMentionContexts) session.pendingMentionContexts = [];
4468
- session.pendingMentionContexts.push(
4469
- "[Context: @" + mateName + " was mentioned and responded]\n\n" +
4470
- "User asked @" + mateName + ": " + msg.text + "\n" +
4471
- mateName + " responded: " + fullText + "\n\n" +
4472
- "[End of @mention context. This is for your reference only. Do not re-execute or repeat this response.]"
4473
- );
4474
-
4475
- sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
4476
-
4477
- // Check if the mate wrote a debate brief during this turn
4478
- checkForDmDebateBrief(session, msg.mateId, mateCtx);
4479
-
4480
- // Generate session digest for mate's long-term memory
4481
- digestMentionSession(session, msg.mateId, mateCtx, fullText, msg.text);
4482
- },
4483
- onError: function (errMsg) {
4484
- session._mentionInProgress = false;
4485
- // Clean up dead session
4486
- if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
4487
- delete session._mentionSessions[msg.mateId];
4488
- }
4489
- console.error("[mention] Error for mate " + msg.mateId + ":", errMsg);
4490
- sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: errMsg });
4491
- },
4492
- };
4493
-
4494
- // Initialize mention sessions map if needed
4495
- if (!session._mentionSessions) session._mentionSessions = {};
4496
-
4497
- // Session continuity: check if this mate has a response in the recent window
4498
- var existingSession = session._mentionSessions[msg.mateId];
4499
- // Don't reuse a session that's still generating a digest (would mix digest output into mention stream)
4500
- var canContinue = existingSession && existingSession.isAlive() && !existingSession._digesting && hasMateInWindow(recentTurns, msg.mateId);
4501
-
4502
- if (canContinue) {
4503
- // Continue existing mention session with middle context
4504
- var middleContext = buildMiddleContext(recentTurns, msg.mateId);
4505
- var continuationText = middleContext ? middleContext + "\n\n" + mentionFullInput : mentionFullInput;
4506
- existingSession.pushMessage(continuationText, mentionCallbacks, msg.images);
4507
- } else {
4508
- // Clean up old session if it exists
4509
- if (existingSession) {
4510
- existingSession.close();
4511
- delete session._mentionSessions[msg.mateId];
4512
- }
4513
-
4514
- // Load Mate CLAUDE.md
4515
- var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
4516
- var claudeMd = "";
4517
- try {
4518
- claudeMd = fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
4519
- } catch (e) {
4520
- // CLAUDE.md may not exist for new mates
4521
- }
4522
-
4523
- // Load session digests (unified: uses memory-summary.md if available)
4524
- // Pass user's message as query for BM25 search of relevant past sessions
4525
- var recentDigests = loadMateDigests(mateCtx, msg.mateId, mentionFullInput);
4526
-
4527
- // Build initial mention context
4528
- var mentionContext = buildMentionContext(userName, recentTurns) + recentDigests;
4529
-
4530
- // Create new persistent mention session
4531
- sdk.createMentionSession({
4532
- claudeMd: claudeMd,
4533
- initialContext: mentionContext,
4534
- initialMessage: mentionFullInput,
4535
- initialImages: msg.images || null,
4536
- onActivity: mentionCallbacks.onActivity,
4537
- onDelta: mentionCallbacks.onDelta,
4538
- onDone: mentionCallbacks.onDone,
4539
- onError: mentionCallbacks.onError,
4540
- canUseTool: function (toolName, input, toolOpts) {
4541
- var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
4542
- if (autoAllow[toolName]) {
4543
- return Promise.resolve({ behavior: "allow", updatedInput: input });
4544
- }
4545
- // Route through the project session's permission system
4546
- return new Promise(function (resolve) {
4547
- var requestId = crypto.randomUUID();
4548
- session.pendingPermissions[requestId] = {
4549
- resolve: resolve,
4550
- requestId: requestId,
4551
- toolName: toolName,
4552
- toolInput: input,
4553
- toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
4554
- decisionReason: (toolOpts && toolOpts.decisionReason) || "",
4555
- mateId: msg.mateId,
4556
- };
4557
- sendToSession(session.localId, {
4558
- type: "permission_request",
4559
- requestId: requestId,
4560
- toolName: toolName,
4561
- toolInput: input,
4562
- toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
4563
- decisionReason: (toolOpts && toolOpts.decisionReason) || "",
4564
- mateId: msg.mateId,
4565
- });
4566
- onProcessingChanged();
4567
- if (toolOpts && toolOpts.signal) {
4568
- toolOpts.signal.addEventListener("abort", function () {
4569
- delete session.pendingPermissions[requestId];
4570
- sendToSession(session.localId, { type: "permission_cancel", requestId: requestId });
4571
- onProcessingChanged();
4572
- resolve({ behavior: "deny", message: "Request cancelled" });
4573
- });
4084
+ Promise.all(tabPromises).then(function(results) {
4085
+ var tabContextParts = [];
4086
+ var screenshotImages = [];
4087
+
4088
+ for (var ti = 0; ti < results.length; ti++) {
4089
+ if (!results[ti]) continue;
4090
+ var tabId2 = parseInt(tabSources[ti].split(":")[1], 10);
4091
+ var tabInfo = _browserTabList[tabId2];
4092
+ var tabLabel = tabInfo ? (tabInfo.title || tabInfo.url || "Tab " + tabId2) : "Tab " + tabId2;
4093
+ var r = results[ti];
4094
+ var parts = [];
4095
+
4096
+ // Console logs
4097
+ if (r.console && r.console.logs) {
4098
+ try {
4099
+ var logs = typeof r.console.logs === "string" ? JSON.parse(r.console.logs) : r.console.logs;
4100
+ if (logs && logs.length > 0) {
4101
+ var logLines = [];
4102
+ var logSlice = logs.slice(-50);
4103
+ for (var li = 0; li < logSlice.length; li++) {
4104
+ var entry = logSlice[li];
4105
+ var ts = entry.ts ? new Date(entry.ts).toTimeString().slice(0, 8) : "";
4106
+ var lvl = (entry.level || "log").toUpperCase();
4107
+ logLines.push("[" + ts + " " + lvl + "] " + (entry.text || ""));
4108
+ }
4109
+ parts.push("Console:\n" + logLines.join("\n"));
4110
+ }
4111
+ } catch (e) {
4112
+ // ignore parse errors
4574
4113
  }
4575
- });
4576
- },
4577
- }).then(function (mentionSession) {
4578
- if (mentionSession) {
4579
- session._mentionSessions[msg.mateId] = mentionSession;
4580
- }
4581
- }).catch(function (err) {
4582
- session._mentionInProgress = false;
4583
- console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
4584
- sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
4585
- });
4586
- }
4587
- }
4588
-
4589
- // --- Shared mate helpers (used by debate module and other code) ---
4590
-
4591
- function escapeRegex(str) {
4592
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4593
- }
4594
-
4595
- function getMateProfile(mateCtx, mateId) {
4596
- var mate = matesModule.getMate(mateCtx, mateId);
4597
- if (!mate) return { name: "Mate", avatarColor: "#6c5ce7", avatarStyle: "bottts", avatarSeed: mateId };
4598
- return {
4599
- name: (mate.profile && mate.profile.displayName) || mate.name || "Mate",
4600
- avatarColor: (mate.profile && mate.profile.avatarColor) || "#6c5ce7",
4601
- avatarStyle: (mate.profile && mate.profile.avatarStyle) || "bottts",
4602
- avatarSeed: (mate.profile && mate.profile.avatarSeed) || mateId,
4603
- };
4604
- }
4605
-
4606
- function loadMateClaudeMd(mateCtx, mateId) {
4607
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4608
- try {
4609
- return fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
4610
- } catch (e) {
4611
- return "";
4612
- }
4613
- }
4614
-
4615
- function formatRawDigests(rawLines, headerLabel) {
4616
- if (!rawLines || rawLines.length === 0) return "";
4617
- var lines = ["\n\n" + (headerLabel || "Your recent session memories:")];
4618
- for (var i = 0; i < rawLines.length; i++) {
4619
- try {
4620
- var d = JSON.parse(rawLines[i]);
4621
- if (d.type === "debate" && d.my_role) {
4622
- // Debate memories are role-played positions, not genuine opinions
4623
- lines.push("- [" + (d.date || "?") + "] DEBATE (role: " + d.my_role + ") " + (d.topic || "unknown") +
4624
- ": argued " + (d.my_position || "N/A") + " (assigned role, not my actual opinion)" +
4625
- (d.outcome ? " | Outcome: " + d.outcome : "") +
4626
- (d.open_items ? " | Open: " + d.open_items : ""));
4627
- } else {
4628
- lines.push("- [" + (d.date || "?") + "] " + (d.topic || "unknown") + ": " + (d.my_position || "") +
4629
- (d.decisions ? " | Decisions: " + d.decisions : "") +
4630
- (d.open_items ? " | Open: " + d.open_items : ""));
4631
- }
4632
- } catch (e) {}
4633
- }
4634
- return lines.join("\n");
4635
- }
4636
-
4637
- function loadMateDigests(mateCtx, mateId, query) {
4638
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4639
- var knowledgeDir = path.join(mateDir, "knowledge");
4640
- var mate = matesModule.getMate(mateCtx, mateId);
4641
- var hasGlobalSearch = mate && mate.globalSearch;
4642
-
4643
- // Load shared user profile (available to ALL mates)
4644
- var userProfileResult = "";
4645
- try {
4646
- var matesRoot = matesModule.resolveMatesRoot(mateCtx);
4647
- var userProfilePath = path.join(matesRoot, "user-profile.md");
4648
- if (fs.existsSync(userProfilePath)) {
4649
- var profileContent = fs.readFileSync(userProfilePath, "utf8").trim();
4650
- if (profileContent && profileContent.length > 50) {
4651
- userProfileResult = "\n\n" + profileContent;
4652
- }
4653
- }
4654
- } catch (e) {}
4655
-
4656
- // Check for memory-summary.md first
4657
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4658
- var hasSummary = false;
4659
- var summaryContent = "";
4660
- try {
4661
- if (fs.existsSync(summaryFile)) {
4662
- summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4663
- if (summaryContent) hasSummary = true;
4664
- }
4665
- } catch (e) {}
4666
-
4667
- // Load raw digests
4668
- var allLines = [];
4669
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
4670
- try {
4671
- if (fs.existsSync(digestFile)) {
4672
- allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4673
- }
4674
- } catch (e) {}
4675
-
4676
- var result = userProfileResult;
4677
-
4678
- if (hasSummary) {
4679
- // Load summary + latest 5 raw digests for richer context
4680
- var recent = allLines.slice(-5);
4681
- result = "\n\nYour memory summary:\n" + summaryContent;
4682
- if (recent.length > 0) {
4683
- result += formatRawDigests(recent, "Latest raw session memories:");
4684
- }
4685
- } else {
4686
- // Backward compatible: latest 8 raw digests
4687
- var recent = allLines.slice(-8);
4688
- result = formatRawDigests(recent, "Your recent session memories:");
4689
- }
4690
-
4691
- // Global search: always load team memory summaries for globalSearch mates
4692
- var otherDigests = [];
4693
- if (hasGlobalSearch) {
4694
- try {
4695
- var allMates = matesModule.getAllMates(mateCtx);
4696
- var teamSummaries = [];
4697
- for (var mi = 0; mi < allMates.length; mi++) {
4698
- if (allMates[mi].id === mateId) continue;
4699
- var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
4700
- var mateName = allMates[mi].name || allMates[mi].id;
4701
-
4702
- // Collect digest files for BM25 search
4703
- var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
4704
- if (fs.existsSync(otherDigest)) {
4705
- otherDigests.push({ path: otherDigest, mateName: mateName });
4706
4114
  }
4707
4115
 
4708
- // Collect memory summaries for direct context injection
4709
- var otherSummary = path.join(otherDir, "knowledge", "memory-summary.md");
4710
- try {
4711
- if (fs.existsSync(otherSummary)) {
4712
- var summaryText = fs.readFileSync(otherSummary, "utf8").trim();
4713
- if (summaryText && summaryText.length > 50) {
4714
- teamSummaries.push({ mateName: mateName, summary: summaryText });
4116
+ // Network requests
4117
+ if (r.network && r.network.network) {
4118
+ try {
4119
+ var netLog = typeof r.network.network === "string" ? JSON.parse(r.network.network) : r.network.network;
4120
+ if (netLog && netLog.length > 0) {
4121
+ var netLines = [];
4122
+ var netSlice = netLog.slice(-30);
4123
+ for (var ni = 0; ni < netSlice.length; ni++) {
4124
+ var req = netSlice[ni];
4125
+ var line = (req.method || "GET") + " " + (req.url || "") + " " + (req.status || 0) + " " + (req.duration || 0) + "ms";
4126
+ if (req.error) line += " [" + req.error + "]";
4127
+ netLines.push(line);
4128
+ }
4129
+ parts.push("Network (last " + netSlice.length + " requests):\n" + netLines.join("\n"));
4715
4130
  }
4131
+ } catch (e) {
4132
+ // ignore parse errors
4716
4133
  }
4717
- } catch (e) {}
4718
- }
4719
-
4720
- // Inject team memory summaries into context
4721
- if (teamSummaries.length > 0) {
4722
- result += "\n\nTeam memory summaries (other mates' accumulated context):";
4723
- for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
4724
- var ts = teamSummaries[tsi];
4725
- // Cap each summary to avoid context overflow
4726
- var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
4727
- result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
4728
4134
  }
4729
- }
4730
- } catch (e) {}
4731
4135
 
4732
- // Inject recent user observations from all mates (newest first, max 15)
4733
- try {
4734
- var allObservations = [];
4735
- var allMatesForObs = matesModule.getAllMates(mateCtx);
4736
- for (var moi = 0; moi < allMatesForObs.length; moi++) {
4737
- var moDir = matesModule.getMateDir(mateCtx, allMatesForObs[moi].id);
4738
- var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
4739
- try {
4740
- if (fs.existsSync(moFile)) {
4741
- var moLines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
4742
- for (var mli = 0; mli < moLines.length; mli++) {
4743
- try {
4744
- var moEntry = JSON.parse(moLines[mli]);
4745
- moEntry._mateName = moEntry.mateName || allMatesForObs[moi].name || allMatesForObs[moi].id;
4746
- allObservations.push(moEntry);
4747
- } catch (e) {}
4136
+ // Page text (from tab_page_text command)
4137
+ if (r.pageText && (r.pageText.text || r.pageText.value)) {
4138
+ var pageContent = r.pageText.text || r.pageText.value;
4139
+ if (pageContent.length > 0) {
4140
+ if (pageContent.length > 32768) {
4141
+ pageContent = pageContent.substring(0, 32768) + "\n... (truncated)";
4748
4142
  }
4143
+ parts.push("Page text:\n" + pageContent);
4749
4144
  }
4750
- } catch (e) {}
4751
- }
4752
- if (allObservations.length > 0) {
4753
- // Sort by date descending
4754
- allObservations.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
4755
- var recentObs = allObservations.slice(0, 15);
4756
- result += "\n\nRecent user observations from all mates:";
4757
- for (var roi = 0; roi < recentObs.length; roi++) {
4758
- var ro = recentObs[roi];
4759
- result += "\n- [" + (ro.date || "?") + "] [@" + ro._mateName + "] [" + (ro.category || "?") + "] " + (ro.observation || "") + (ro.evidence ? " (evidence: " + ro.evidence + ")" : "");
4760
4145
  }
4761
- }
4762
- } catch (e) {}
4763
-
4764
- // Inject recent activity timeline across all projects (chronological)
4765
- try {
4766
- var timelineEntries = [];
4767
4146
 
4768
- // Own sessions
4769
- sm.sessions.forEach(function (s) {
4770
- if (s.hidden || !s.history || s.history.length === 0) return;
4771
- timelineEntries.push({
4772
- title: s.title || "New Session",
4773
- project: null,
4774
- ts: s.lastActivity || s.createdAt || 0
4775
- });
4776
- });
4777
-
4778
- // Cross-project sessions
4779
- var crossForTimeline = getAllProjectSessions();
4780
- for (var cti = 0; cti < crossForTimeline.length; cti++) {
4781
- var cs = crossForTimeline[cti];
4782
- timelineEntries.push({
4783
- title: cs.title || "New Session",
4784
- project: cs._projectTitle || null,
4785
- ts: cs.lastActivity || cs.createdAt || 0
4786
- });
4787
- }
4788
-
4789
- // Sort by time descending, take latest 20
4790
- timelineEntries.sort(function (a, b) { return b.ts - a.ts; });
4791
- timelineEntries = timelineEntries.slice(0, 20);
4792
-
4793
- if (timelineEntries.length > 0) {
4794
- result += "\n\nRecent activity timeline (newest first):";
4795
- for (var ti = 0; ti < timelineEntries.length; ti++) {
4796
- var te = timelineEntries[ti];
4797
- var dateStr = te.ts ? new Date(te.ts).toISOString().replace("T", " ").substring(0, 16) : "?";
4798
- var line = "- [" + dateStr + "] " + te.title;
4799
- if (te.project) line += " (project: " + te.project + ")";
4800
- result += "\n" + line;
4147
+ // Screenshot — save to disk and add to images for SDK
4148
+ if (r.screenshot && r.screenshot.image) {
4149
+ try {
4150
+ var screenshotData = r.screenshot.image;
4151
+ var screenshotName = saveImageFile("image/png", screenshotData, getLinuxUserForSession(session));
4152
+ if (screenshotName) {
4153
+ var screenshotPath = path.join(imagesDir, screenshotName);
4154
+ // Add to images array for SDK multimodal
4155
+ screenshotImages.push({
4156
+ mediaType: "image/png",
4157
+ data: screenshotData,
4158
+ file: screenshotName,
4159
+ tabTitle: tabLabel,
4160
+ tabUrl: tabInfo ? tabInfo.url : ""
4161
+ });
4162
+ parts.push("[Screenshot saved: " + screenshotPath + "]");
4163
+ }
4164
+ } catch (e) {
4165
+ // ignore screenshot save errors
4166
+ }
4801
4167
  }
4802
- }
4803
- } catch (e) {}
4804
- }
4805
4168
 
4806
- // BM25 unified search: digests + session history for current topic
4807
- // globalSearch mates always search (they see everything); others need enough digests
4808
- if (query && (hasGlobalSearch || allLines.length > 5)) {
4809
- try {
4810
- // Collect mate's own sessions
4811
- var mateSessions = [];
4812
- sm.sessions.forEach(function (s) {
4813
- if (!s.hidden && s.history && s.history.length > 0) {
4814
- mateSessions.push(s);
4169
+ if (r.console && r.console.error) {
4170
+ parts.push("(Console error: " + r.console.error + ")");
4815
4171
  }
4816
- });
4817
-
4818
- // globalSearch: also collect sessions from all other projects + knowledge files
4819
- var knowledgeFiles = [];
4820
- if (hasGlobalSearch) {
4821
- var crossSessions = getAllProjectSessions();
4822
- for (var cs = 0; cs < crossSessions.length; cs++) {
4823
- mateSessions.push(crossSessions[cs]);
4172
+ if (r.network && r.network.error) {
4173
+ parts.push("(Network error: " + r.network.error + ")");
4824
4174
  }
4825
4175
 
4826
- // Collect knowledge files from all mates
4827
- try {
4828
- var allMatesForKnowledge = matesModule.getAllMates(mateCtx);
4829
- for (var mk = 0; mk < allMatesForKnowledge.length; mk++) {
4830
- var mkDir = matesModule.getMateDir(mateCtx, allMatesForKnowledge[mk].id);
4831
- var mkName = allMatesForKnowledge[mk].name || allMatesForKnowledge[mk].id;
4832
- var mkKnowledgeDir = path.join(mkDir, "knowledge");
4833
- try {
4834
- var kFiles = fs.readdirSync(mkKnowledgeDir);
4835
- for (var kfi = 0; kfi < kFiles.length; kfi++) {
4836
- var kfName = kFiles[kfi];
4837
- // Skip system files (digests, identity, base-template)
4838
- if (kfName === "session-digests.jsonl" || kfName === "memory-summary.md" ||
4839
- kfName === "identity-backup.md" || kfName === "identity-history.jsonl" ||
4840
- kfName === "base-template.md") continue;
4841
- knowledgeFiles.push({
4842
- filePath: path.join(mkKnowledgeDir, kfName),
4843
- name: kfName,
4844
- mateName: mkName
4845
- });
4846
- }
4847
- } catch (e) {}
4848
- }
4849
- } catch (e) {}
4176
+ if (parts.length > 0) {
4177
+ tabContextParts.push("[Browser tab: " + tabLabel + "]\n" + parts.join("\n\n"));
4178
+ }
4850
4179
  }
4851
4180
 
4852
- var searchResults = sessionSearch.searchMate({
4853
- digestFilePath: digestFile,
4854
- otherDigests: otherDigests,
4855
- sessions: mateSessions,
4856
- knowledgeFiles: knowledgeFiles,
4857
- query: query,
4858
- maxResults: hasGlobalSearch ? 12 : 5,
4859
- minScore: 1.0
4860
- });
4861
- var contextStr = sessionSearch.formatForContext(searchResults);
4862
- if (contextStr) result += contextStr;
4863
- } catch (e) {
4864
- console.error("[session-search] Mate search failed:", e.message);
4865
- }
4866
- }
4867
-
4868
- return result;
4869
- }
4870
-
4871
- // Gate check: ask Haiku whether this conversation contains anything worth remembering
4872
- function gateMemory(mateCtx, mateId, conversationContent, callback, opts) {
4873
- opts = opts || {};
4874
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4875
- var knowledgeDir = path.join(mateDir, "knowledge");
4876
-
4877
- // Load mate role/activities from mate.yaml (lightweight, no full CLAUDE.md)
4878
- var mateRole = "";
4879
- var mateActivities = "";
4880
- try {
4881
- var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
4882
- var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
4883
- var actMatch = yamlRaw.match(/^activities:\s*(.+)$/m);
4884
- if (roleMatch) mateRole = roleMatch[1].trim();
4885
- if (actMatch) mateActivities = actMatch[1].trim();
4886
- } catch (e) {}
4887
-
4888
- // Load existing memory summary if available
4889
- var summaryContent = "";
4890
- try {
4891
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4892
- if (fs.existsSync(summaryFile)) {
4893
- summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4894
- }
4895
- } catch (e) {}
4896
-
4897
- // Cap conversation content for gate
4898
- var cappedContent = conversationContent;
4899
- if (cappedContent.length > 3000) {
4900
- cappedContent = cappedContent.substring(0, 3000) + "...";
4901
- }
4902
-
4903
- var gateContext = [
4904
- "[SYSTEM: Memory Gate]",
4905
- "You are a memory filter for an AI Mate.",
4906
- "",
4907
- "Mate role: " + (mateRole || "assistant"),
4908
- "Mate activities: " + (mateActivities || "general"),
4909
- "",
4910
- "Current memory summary:",
4911
- summaryContent || "No memory summary yet.",
4912
- "",
4913
- "Conversation just ended:",
4914
- cappedContent,
4915
- ].join("\n");
4916
-
4917
- var gatePrompt = opts.gatePrompt || [
4918
- 'Should this conversation be saved to long-term memory?',
4919
- 'Answer "yes" if ANY of these apply:',
4920
- "- A new decision, commitment, or direction",
4921
- "- A change in position or strategy",
4922
- "- New information relevant to this Mate's role",
4923
- "- A user preference, opinion, or pattern not already in the summary",
4924
- "- The user shared personal context, project details, or goals",
4925
- "- The user expressed what they like, dislike, or care about",
4926
- "- The user gave instructions on how they want things done",
4927
- "- Anything the user would reasonably expect to be remembered next time",
4928
- "",
4929
- 'Answer "no" ONLY if:',
4930
- "- It exactly duplicates what is already in the memory summary",
4931
- "- The entire conversation is a single trivial exchange (e.g. just 'hi' / 'hello')",
4932
- "",
4933
- "When in doubt, answer yes. It is better to remember too much than to forget something important.",
4934
- "",
4935
- 'Answer with ONLY "yes" or "no". Nothing else.',
4936
- ].join("\n");
4937
- var defaultOnError = opts.defaultYes !== undefined ? !!opts.defaultYes : true;
4938
-
4939
- var gateText = "";
4940
- var _gateSession = null;
4941
- sdk.createMentionSession({
4942
- claudeMd: "",
4943
- model: "haiku",
4944
- initialContext: gateContext,
4945
- initialMessage: gatePrompt,
4946
- onActivity: function () {},
4947
- onDelta: function (delta) {
4948
- gateText += delta;
4949
- },
4950
- onDone: function () {
4951
- var answer = gateText.trim().toLowerCase();
4952
- var shouldRemember = answer.indexOf("yes") !== -1;
4953
- if (_gateSession) try { _gateSession.close(); } catch (e) {}
4954
- callback(shouldRemember);
4955
- },
4956
- onError: function (err) {
4957
- console.error("[memory-gate] Gate check failed for mate " + mateId + ":", err);
4958
- if (_gateSession) try { _gateSession.close(); } catch (e) {}
4959
- callback(defaultOnError);
4960
- },
4961
- }).then(function (gs) {
4962
- _gateSession = gs;
4963
- if (!gs) callback(defaultOnError);
4964
- }).catch(function (err) {
4965
- console.error("[memory-gate] Failed to create gate session for mate " + mateId + ":", err);
4966
- callback(defaultOnError);
4967
- });
4968
- }
4969
-
4970
- // Update (or create) memory-summary.md based on a new digest
4971
- function updateMemorySummary(mateCtx, mateId, digestObj) {
4972
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
4973
- var knowledgeDir = path.join(mateDir, "knowledge");
4974
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
4181
+ if (tabContextParts.length > 0) {
4182
+ fullText = tabContextParts.join("\n\n---\n\n") + "\n\n" + fullText;
4183
+ }
4975
4184
 
4976
- // Check if summary exists; if not, try initial generation first
4977
- var summaryExists = false;
4978
- var summaryContent = "";
4979
- try {
4980
- if (fs.existsSync(summaryFile)) {
4981
- summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
4982
- if (summaryContent) summaryExists = true;
4983
- }
4984
- } catch (e) {}
4185
+ // If screenshots were captured, send context preview cards and add to SDK images
4186
+ if (screenshotImages.length > 0) {
4187
+ if (!msg.images) msg.images = [];
4188
+ for (var si = 0; si < screenshotImages.length; si++) {
4189
+ var ss = screenshotImages[si];
4190
+ // Save context_preview to history so it restores on session load
4191
+ var previewEntry = {
4192
+ type: "context_preview",
4193
+ tab: {
4194
+ title: ss.tabTitle || "",
4195
+ url: ss.tabUrl || "",
4196
+ screenshotFile: ss.file
4197
+ }
4198
+ };
4199
+ session.history.push(previewEntry);
4200
+ // Send context card to all clients
4201
+ sendToSession(session.localId, {
4202
+ type: "context_preview",
4203
+ tab: {
4204
+ title: ss.tabTitle || "",
4205
+ url: ss.tabUrl || "",
4206
+ screenshotUrl: "/p/" + slug + "/images/" + ss.file
4207
+ }
4208
+ });
4209
+ // Add to SDK images for multimodal
4210
+ msg.images.push({ mediaType: ss.mediaType, data: ss.data });
4211
+ }
4212
+ sm.saveSessionFile(session);
4213
+ }
4985
4214
 
4986
- if (!summaryExists) {
4987
- // Try initial summary generation from existing digests (migration)
4988
- initMemorySummary(mateCtx, mateId, function () {
4989
- // After init, do incremental update with the new digest
4990
- doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
4215
+ dispatchToSdk(fullText);
4991
4216
  });
4992
4217
  } else {
4993
- doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
4218
+ dispatchToSdk(fullText);
4994
4219
  }
4995
4220
  }
4996
4221
 
4997
- // Incremental update of memory-summary.md with a single new digest
4998
- function doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj) {
4999
- var existingSummary = "";
5000
- try {
5001
- if (fs.existsSync(summaryFile)) {
5002
- existingSummary = fs.readFileSync(summaryFile, "utf8").trim();
5003
- }
5004
- } catch (e) {}
5005
-
5006
- var updateContext = [
5007
- "[SYSTEM: Memory Summary Update]",
5008
- "You are updating an AI Mate's long-term memory summary.",
5009
- "",
5010
- "Current summary:",
5011
- existingSummary || "(empty, this is the first entry)",
5012
- "",
5013
- "New session digest to incorporate:",
5014
- JSON.stringify(digestObj, null, 2),
5015
- ].join("\n");
5016
-
5017
- var updatePrompt = [
5018
- "Update the summary by:",
5019
- "1. Adding new information from this session",
5020
- "2. Updating existing entries if positions changed",
5021
- "3. Moving resolved open threads out of \"Open Threads\"",
5022
- "4. Adding to \"My Track Record\" if a past prediction/recommendation can now be evaluated",
5023
- "5. Removing outdated or redundant information",
5024
- "6. Preserving important user quotes and context from key_quotes and user_context fields",
5025
- "",
5026
- "Maintain this structure:",
5027
- "",
5028
- "# Memory Summary",
5029
- "Last updated: YYYY-MM-DD (session count: N+1)",
5030
- "",
5031
- "## User Context",
5032
- "(who they are, what they work on, project details, goals)",
5033
- "## User Patterns",
5034
- "(preferences, work style, communication style, likes/dislikes)",
5035
- "## Key Decisions",
5036
- "## Notable Quotes",
5037
- "(important things the user said, verbatim when possible)",
5038
- "## My Track Record",
5039
- "## Open Threads",
5040
- "## Recurring Topics",
5041
- "",
5042
- "Keep it concise. Each section should have at most 10 bullet points.",
5043
- "Drop the oldest/least relevant if needed.",
5044
- "The Notable Quotes section is valuable for preserving the user's voice and intent.",
5045
- "Output ONLY the updated markdown. Nothing else.",
5046
- ].join("\n");
5047
-
5048
- var updateText = "";
5049
- var _updateSession = null;
5050
- sdk.createMentionSession({
5051
- claudeMd: "",
5052
- model: "haiku",
5053
- initialContext: updateContext,
5054
- initialMessage: updatePrompt,
5055
- onActivity: function () {},
5056
- onDelta: function (delta) {
5057
- updateText += delta;
5058
- },
5059
- onDone: function () {
5060
- try {
5061
- var cleaned = updateText.trim();
5062
- if (cleaned.indexOf("```") === 0) {
5063
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5064
- }
5065
- fs.mkdirSync(knowledgeDir, { recursive: true });
5066
- fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
5067
- console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
5068
- } catch (e) {
5069
- console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
5070
- }
5071
- if (_updateSession) try { _updateSession.close(); } catch (e) {}
5072
- },
5073
- onError: function (err) {
5074
- console.error("[memory-summary] Summary update failed for mate " + mateId + ":", err);
5075
- if (_updateSession) try { _updateSession.close(); } catch (e) {}
5076
- },
5077
- }).then(function (us) {
5078
- _updateSession = us;
5079
- }).catch(function (err) {
5080
- console.error("[memory-summary] Failed to create summary update session for mate " + mateId + ":", err);
5081
- });
5082
- }
5083
-
5084
- // User profile synthesis: collect observations from all mates, synthesize unified profile
5085
- var USER_PROFILE_SYNTHESIS_THRESHOLD = 8;
5086
-
5087
- function maybeSynthesizeUserProfile(mateCtx, mateId) {
5088
- var mate = matesModule.getMate(mateCtx, mateId);
5089
- if (!mate || !mate.globalSearch) return; // Only primary/globalSearch mates synthesize
5090
-
5091
- var matesRoot = matesModule.resolveMatesRoot(mateCtx);
5092
- var profilePath = path.join(matesRoot, "user-profile.md");
5093
-
5094
- // Collect all observations across all mates
5095
- var allObs = [];
5096
- try {
5097
- var allMates = matesModule.getAllMates(mateCtx);
5098
- for (var mi = 0; mi < allMates.length; mi++) {
5099
- var moDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
5100
- var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
5101
- try {
5102
- if (fs.existsSync(moFile)) {
5103
- var lines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
5104
- for (var li = 0; li < lines.length; li++) {
5105
- try { allObs.push(JSON.parse(lines[li])); } catch (e) {}
5106
- }
5107
- }
5108
- } catch (e) {}
5109
- }
5110
- } catch (e) { return; }
5111
-
5112
- if (allObs.length === 0) return;
5113
-
5114
- // Check if synthesis is needed (threshold since last synthesis)
5115
- var existingProfile = "";
5116
- var lastObsCount = 0;
5117
- try {
5118
- if (fs.existsSync(profilePath)) {
5119
- existingProfile = fs.readFileSync(profilePath, "utf8").trim();
5120
- var countMatch = existingProfile.match(/from (\d+) observations/);
5121
- if (countMatch) lastObsCount = parseInt(countMatch[1], 10) || 0;
5122
- }
5123
- } catch (e) {}
4222
+ // --- Shared helpers ---
5124
4223
 
5125
- if (allObs.length - lastObsCount < USER_PROFILE_SYNTHESIS_THRESHOLD) return;
5126
-
5127
- // Sort newest first for synthesis
5128
- allObs.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
5129
-
5130
- var synthContext = [
5131
- "[SYSTEM: User Profile Synthesis]",
5132
- "You are synthesizing a user profile from observations collected by multiple AI teammates.",
5133
- "",
5134
- "Current profile:",
5135
- existingProfile || "(none yet, first synthesis)",
5136
- "",
5137
- "All observations (" + allObs.length + " total, newest first):",
5138
- allObs.map(function (o) {
5139
- return "[" + (o.date || "?") + "] [@" + (o.mateName || o.mateId || "?") + "] [" + (o.category || "?") + "] " + (o.observation || "") + (o.evidence ? " (evidence: " + o.evidence + ")" : "");
5140
- }).join("\n"),
5141
- ].join("\n");
5142
-
5143
- var synthPrompt = [
5144
- "Synthesize a unified user profile from these observations.",
5145
- "",
5146
- "Rules:",
5147
- "1. Organize by: Communication Style, Decision Patterns, Working Habits, Technical Preferences, Emotional Signals",
5148
- "2. Each point: observation + source mates and dates in parentheses",
5149
- "3. If observations contradict, note both with dates. Preferences evolve.",
5150
- "4. Mark patterns seen 3+ times as [strong], 2 times as [emerging]",
5151
- "5. Keep under 800 words. This is a reference card, not a biography.",
5152
- '6. End with: "Last synthesized: YYYY-MM-DD from N observations across M mates"',
5153
- "",
5154
- "Output ONLY the markdown profile. No fences, no extra text.",
5155
- ].join("\n");
5156
-
5157
- var synthText = "";
5158
- sdk.createMentionSession({
5159
- claudeMd: "",
5160
- model: "haiku",
5161
- initialContext: synthContext,
5162
- initialMessage: synthPrompt,
5163
- onActivity: function () {},
5164
- onDelta: function (delta) { synthText += delta; },
5165
- onDone: function () {
5166
- try {
5167
- var cleaned = synthText.trim();
5168
- if (cleaned.indexOf("```") === 0) {
5169
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5170
- }
5171
- fs.mkdirSync(path.dirname(profilePath), { recursive: true });
5172
- fs.writeFileSync(profilePath, cleaned + "\n", "utf8");
5173
- console.log("[user-profile] Synthesized user-profile.md from " + allObs.length + " observations");
5174
- } catch (e) {
5175
- console.error("[user-profile] Failed to write user-profile.md:", e.message);
5176
- }
5177
- },
5178
- onError: function (err) {
5179
- console.error("[user-profile] Synthesis failed:", err);
5180
- },
5181
- }).catch(function (err) {
5182
- console.error("[user-profile] Failed to create synthesis session:", err);
5183
- });
4224
+ function escapeRegex(str) {
4225
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5184
4226
  }
5185
4227
 
5186
- // Initial summary generation (migration): read latest 20 digests and generate first summary
5187
- function initMemorySummary(mateCtx, mateId, callback) {
5188
- var mateDir = matesModule.getMateDir(mateCtx, mateId);
5189
- var knowledgeDir = path.join(mateDir, "knowledge");
5190
- var summaryFile = path.join(knowledgeDir, "memory-summary.md");
5191
- var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
5192
-
5193
- // Check if digests exist
5194
- var allLines = [];
5195
- try {
5196
- if (fs.existsSync(digestFile)) {
5197
- allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
5198
- }
5199
- } catch (e) {}
5200
-
5201
- if (allLines.length === 0) {
5202
- // No digests to summarize, just callback
5203
- callback();
5204
- return;
5205
- }
5206
-
5207
- var recent = allLines.slice(-20);
5208
- var digestsText = [];
5209
- for (var i = 0; i < recent.length; i++) {
5210
- try {
5211
- var d = JSON.parse(recent[i]);
5212
- digestsText.push(JSON.stringify(d));
5213
- } catch (e) {}
5214
- }
5215
-
5216
- if (digestsText.length === 0) {
5217
- callback();
5218
- return;
5219
- }
5220
-
5221
- var initContext = [
5222
- "[SYSTEM: Initial Memory Summary]",
5223
- "You are creating the first long-term memory summary for an AI Mate.",
5224
- "",
5225
- "Here are the most recent session digests (up to 20):",
5226
- digestsText.join("\n"),
5227
- ].join("\n");
5228
-
5229
- var initPrompt = [
5230
- "Create a memory summary from these sessions.",
5231
- "",
5232
- "Structure:",
5233
- "",
5234
- "# Memory Summary",
5235
- "Last updated: YYYY-MM-DD (session count: N)",
5236
- "",
5237
- "## User Context",
5238
- "(who they are, what they work on, project details, goals)",
5239
- "## User Patterns",
5240
- "(preferences, work style, communication style, likes/dislikes)",
5241
- "## Key Decisions",
5242
- "## Notable Quotes",
5243
- "(important things the user said, verbatim when possible)",
5244
- "## My Track Record",
5245
- "## Open Threads",
5246
- "## Recurring Topics",
5247
- "",
5248
- "Keep it concise. Focus on patterns, decisions, and the user's own words.",
5249
- "Each section should have at most 10 bullet points.",
5250
- "Preserve key_quotes from digests in the Notable Quotes section.",
5251
- "Set session count to " + digestsText.length + ".",
5252
- "Output ONLY the markdown. Nothing else.",
5253
- ].join("\n");
5254
-
5255
- var initText = "";
5256
- var _initSession = null;
5257
- sdk.createMentionSession({
5258
- claudeMd: "",
5259
- model: "haiku",
5260
- initialContext: initContext,
5261
- initialMessage: initPrompt,
5262
- onActivity: function () {},
5263
- onDelta: function (delta) {
5264
- initText += delta;
5265
- },
5266
- onDone: function () {
5267
- try {
5268
- var cleaned = initText.trim();
5269
- if (cleaned.indexOf("```") === 0) {
5270
- cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
5271
- }
5272
- fs.mkdirSync(knowledgeDir, { recursive: true });
5273
- fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
5274
- console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
5275
- } catch (e) {
5276
- console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
5277
- }
5278
- if (_initSession) try { _initSession.close(); } catch (e) {}
5279
- callback();
5280
- },
5281
- onError: function (err) {
5282
- console.error("[memory-summary] Initial summary generation failed for mate " + mateId + ":", err);
5283
- if (_initSession) try { _initSession.close(); } catch (e) {}
5284
- callback();
5285
- },
5286
- }).then(function (is) {
5287
- _initSession = is;
5288
- if (!is) callback();
5289
- }).catch(function (err) {
5290
- console.error("[memory-summary] Failed to create init summary session for mate " + mateId + ":", err);
5291
- callback();
5292
- });
5293
- }
4228
+ // --- Memory engine (delegated to project-memory.js) ---
4229
+ var _memory = attachMemory({
4230
+ cwd: cwd,
4231
+ sm: sm,
4232
+ sdk: sdk,
4233
+ sendTo: sendTo,
4234
+ matesModule: matesModule,
4235
+ sessionSearch: sessionSearch,
4236
+ getAllProjectSessions: getAllProjectSessions,
4237
+ projectOwnerId: projectOwnerId,
4238
+ handleMessage: handleMessage,
4239
+ });
4240
+ var loadMateDigests = _memory.loadMateDigests;
4241
+ var gateMemory = _memory.gateMemory;
4242
+ var updateMemorySummary = _memory.updateMemorySummary;
4243
+ var initMemorySummary = _memory.initMemorySummary;
4244
+
4245
+ // --- Mate interaction engine (delegated to project-mate-interaction.js) ---
4246
+ // Note: checkForDmDebateBrief comes from _debate (initialized below),
4247
+ // so we use a lazy getter that resolves at call time.
4248
+ var _mateInteraction = attachMateInteraction({
4249
+ cwd: cwd,
4250
+ sm: sm,
4251
+ sdk: sdk,
4252
+ sendTo: sendTo,
4253
+ sendToSession: sendToSession,
4254
+ sendToSessionOthers: sendToSessionOthers,
4255
+ matesModule: matesModule,
4256
+ isMate: isMate,
4257
+ projectOwnerId: projectOwnerId,
4258
+ getSessionForWs: getSessionForWs,
4259
+ getLinuxUserForSession: getLinuxUserForSession,
4260
+ saveImageFile: saveImageFile,
4261
+ hydrateImageRefs: hydrateImageRefs,
4262
+ onProcessingChanged: onProcessingChanged,
4263
+ loadMateDigests: loadMateDigests,
4264
+ updateMemorySummary: updateMemorySummary,
4265
+ initMemorySummary: initMemorySummary,
4266
+ get checkForDmDebateBrief() { return checkForDmDebateBrief; },
4267
+ });
4268
+ var handleMention = _mateInteraction.handleMention;
4269
+ var getMateProfile = _mateInteraction.getMateProfile;
4270
+ var loadMateClaudeMd = _mateInteraction.loadMateClaudeMd;
4271
+ var digestDmTurn = _mateInteraction.digestDmTurn;
4272
+ var enqueueDigest = _mateInteraction.enqueueDigest;
5294
4273
 
5295
4274
  // --- Debate engine (delegated to project-debate.js) ---
5296
4275
  var _debate = attachDebate({
@@ -5367,6 +4346,44 @@ function createProjectContext(opts) {
5367
4346
 
5368
4347
  // --- Handle project-scoped HTTP requests ---
5369
4348
  function handleHTTP(req, res, urlPath) {
4349
+ // Browser MCP extension bridge: forward commands to Chrome extension
4350
+ if (req.method === "POST" && urlPath === "/ext-command") {
4351
+ parseJsonBody(req).then(function (body) {
4352
+ // Validate auth token
4353
+ if (!body.token || body.token !== _extToken) {
4354
+ res.writeHead(403, { "Content-Type": "application/json" });
4355
+ res.end('{"error":"Invalid token"}');
4356
+ return;
4357
+ }
4358
+ var command = body.command;
4359
+ var args = body.args || {};
4360
+ var timeout = Math.min(body.timeout || 5000, 30000); // max 30s
4361
+
4362
+ // Special command: list_tabs (no extension round-trip needed)
4363
+ if (command === "list_tabs") {
4364
+ var tabArr = [];
4365
+ for (var tid in _browserTabList) {
4366
+ tabArr.push(_browserTabList[tid]);
4367
+ }
4368
+ res.writeHead(200, { "Content-Type": "application/json" });
4369
+ res.end(JSON.stringify({ result: { tabs: tabArr } }));
4370
+ return;
4371
+ }
4372
+
4373
+ sendExtensionCommandAny(command, args, timeout).then(function (result) {
4374
+ res.writeHead(200, { "Content-Type": "application/json" });
4375
+ res.end(JSON.stringify({ result: result || {} }));
4376
+ }).catch(function (err) {
4377
+ res.writeHead(200, { "Content-Type": "application/json" });
4378
+ res.end(JSON.stringify({ error: err.message || "Extension command failed" }));
4379
+ });
4380
+ }).catch(function () {
4381
+ res.writeHead(400, { "Content-Type": "application/json" });
4382
+ res.end('{"error":"Invalid JSON body"}');
4383
+ });
4384
+ return true;
4385
+ }
4386
+
5370
4387
  // Serve chat images
5371
4388
  if (req.method === "GET" && urlPath.indexOf("/images/") === 0) {
5372
4389
  var imgName = path.basename(urlPath);