clay-server 2.26.0-beta.1 → 2.26.0-beta.11
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/bin/cli.js +5 -9
- package/lib/browser-mcp-server.js +496 -0
- package/lib/daemon.js +1 -1
- package/lib/os-users.js +23 -0
- package/lib/project-debate.js +243 -95
- package/lib/project-mate-interaction.js +766 -0
- package/lib/project-memory.js +677 -0
- package/lib/project.js +546 -1361
- package/lib/public/app.js +817 -175
- package/lib/public/css/debate.css +224 -2
- package/lib/public/css/icon-strip.css +10 -10
- package/lib/public/css/input.css +296 -83
- package/lib/public/css/mates.css +56 -57
- package/lib/public/css/mention.css +7 -4
- package/lib/public/css/menus.css +7 -0
- package/lib/public/css/messages.css +17 -0
- package/lib/public/css/mobile-nav.css +3 -1
- package/lib/public/css/overlays.css +181 -0
- package/lib/public/css/rewind.css +79 -0
- package/lib/public/css/server-settings.css +1 -0
- package/lib/public/css/sidebar.css +10 -0
- package/lib/public/css/title-bar.css +189 -3
- package/lib/public/index.html +53 -16
- package/lib/public/modules/context-sources.js +328 -0
- package/lib/public/modules/debate.js +184 -97
- package/lib/public/modules/input.js +18 -1
- package/lib/public/modules/mate-knowledge.js +11 -11
- package/lib/public/modules/mate-memory.js +5 -5
- package/lib/public/modules/mate-sidebar.js +13 -9
- package/lib/public/modules/mention.js +40 -2
- package/lib/public/modules/notifications.js +109 -1
- package/lib/public/modules/rewind.js +36 -0
- package/lib/public/modules/sidebar.js +107 -28
- package/lib/public/modules/terminal.js +8 -0
- package/lib/public/modules/theme.js +2 -1
- package/lib/public/modules/tools.js +69 -24
- package/lib/sdk-bridge.js +81 -7
- package/lib/sdk-worker.js +13 -1
- package/lib/server.js +42 -0
- package/lib/sessions.js +39 -7
- package/lib/terminal-manager.js +36 -6
- package/package.json +2 -2
package/lib/project.js
CHANGED
|
@@ -16,8 +16,36 @@ var matesModule = require("./mates");
|
|
|
16
16
|
var sessionSearch = require("./session-search");
|
|
17
17
|
var userPresence = require("./user-presence");
|
|
18
18
|
var { attachDebate } = require("./project-debate");
|
|
19
|
+
var { attachMemory } = require("./project-memory");
|
|
20
|
+
var { attachMateInteraction } = require("./project-mate-interaction");
|
|
19
21
|
var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
20
22
|
|
|
23
|
+
// --- Context Sources persistence ---
|
|
24
|
+
var _ctxSrcConfig = require("./config");
|
|
25
|
+
var _ctxSrcDir = path.join(_ctxSrcConfig.CONFIG_DIR, "context-sources");
|
|
26
|
+
|
|
27
|
+
function loadContextSources(slug) {
|
|
28
|
+
try {
|
|
29
|
+
var filePath = path.join(_ctxSrcDir, slug + ".json");
|
|
30
|
+
var data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
31
|
+
return data.active || [];
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveContextSources(slug, activeIds) {
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(_ctxSrcDir)) {
|
|
40
|
+
fs.mkdirSync(_ctxSrcDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
var filePath = path.join(_ctxSrcDir, slug + ".json");
|
|
43
|
+
fs.writeFileSync(filePath, JSON.stringify({ active: activeIds }), "utf8");
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error("[context-sources] Failed to save:", e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
21
49
|
// Validate environment variable string (KEY=VALUE per line)
|
|
22
50
|
// Returns null if valid, or an error string if invalid
|
|
23
51
|
function validateEnvString(str) {
|
|
@@ -133,8 +161,13 @@ function createProjectContext(opts) {
|
|
|
133
161
|
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
|
|
134
162
|
var isMate = opts.isMate || false;
|
|
135
163
|
var onCreateWorktree = opts.onCreateWorktree || null;
|
|
164
|
+
var serverPort = opts.port || 2633;
|
|
165
|
+
var serverTls = opts.tls || false;
|
|
136
166
|
var latestVersion = null;
|
|
137
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
|
+
|
|
138
171
|
// --- Chat image storage ---
|
|
139
172
|
var _imgConfig = require("./config");
|
|
140
173
|
var _imgUtils = require("./utils");
|
|
@@ -144,7 +177,18 @@ function createProjectContext(opts) {
|
|
|
144
177
|
|
|
145
178
|
// Convert imageRefs in history entries to images with URLs for the client
|
|
146
179
|
function hydrateImageRefs(entry) {
|
|
147
|
-
if (!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;
|
|
148
192
|
if (entry.type !== "user_message" && entry.type !== "mention_user") return entry;
|
|
149
193
|
var images = [];
|
|
150
194
|
for (var ri = 0; ri < entry.imageRefs.length; ri++) {
|
|
@@ -152,8 +196,8 @@ function createProjectContext(opts) {
|
|
|
152
196
|
images.push({ mediaType: ref.mediaType, url: "/p/" + slug + "/images/" + ref.file });
|
|
153
197
|
}
|
|
154
198
|
var hydrated = {};
|
|
155
|
-
for (var
|
|
156
|
-
if (
|
|
199
|
+
for (var k2 in entry) {
|
|
200
|
+
if (k2 !== "imageRefs") hydrated[k2] = entry[k2];
|
|
157
201
|
}
|
|
158
202
|
hydrated.images = images;
|
|
159
203
|
return hydrated;
|
|
@@ -247,6 +291,60 @@ function createProjectContext(opts) {
|
|
|
247
291
|
// --- Per-project clients ---
|
|
248
292
|
var clients = new Set();
|
|
249
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
|
+
|
|
250
348
|
function send(obj) {
|
|
251
349
|
var data = JSON.stringify(obj);
|
|
252
350
|
for (var ws of clients) {
|
|
@@ -459,6 +557,45 @@ function createProjectContext(opts) {
|
|
|
459
557
|
mateDisplayName: opts.mateDisplayName || "",
|
|
460
558
|
isMate: isMate,
|
|
461
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
|
+
})(),
|
|
462
599
|
onProcessingChanged: onProcessingChanged,
|
|
463
600
|
onTurnDone: isMate ? function (session, preview) { digestDmTurn(session, preview); } : null,
|
|
464
601
|
scheduleMessage: function (session, text, resetsAt) {
|
|
@@ -1235,6 +1372,9 @@ function createProjectContext(opts) {
|
|
|
1235
1372
|
}
|
|
1236
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 });
|
|
1237
1374
|
sendTo(ws, { type: "term_list", terminals: tm.list() });
|
|
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 });
|
|
1238
1378
|
sendTo(ws, { type: "notes_list", notes: nm.list() });
|
|
1239
1379
|
sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
|
|
1240
1380
|
|
|
@@ -1383,7 +1523,19 @@ function createProjectContext(opts) {
|
|
|
1383
1523
|
}
|
|
1384
1524
|
sendTo(ws, hydrateImageRefs(_hitem));
|
|
1385
1525
|
}
|
|
1386
|
-
|
|
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 });
|
|
1387
1539
|
|
|
1388
1540
|
if (active.isProcessing) {
|
|
1389
1541
|
sendTo(ws, { type: "status", status: "processing" });
|
|
@@ -1505,6 +1657,7 @@ function createProjectContext(opts) {
|
|
|
1505
1657
|
}
|
|
1506
1658
|
session._mentionInProgress = false;
|
|
1507
1659
|
sendToSession(session.localId, { type: "mention_done", mateId: mateId, stopped: true });
|
|
1660
|
+
send({ type: "mention_processing", mateId: mateId, active: false });
|
|
1508
1661
|
}
|
|
1509
1662
|
return;
|
|
1510
1663
|
}
|
|
@@ -1514,6 +1667,10 @@ function createProjectContext(opts) {
|
|
|
1514
1667
|
handleDebateStart(ws, msg);
|
|
1515
1668
|
return;
|
|
1516
1669
|
}
|
|
1670
|
+
if (msg.type === "debate_hand_raise") {
|
|
1671
|
+
handleDebateHandRaise(ws);
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1517
1674
|
if (msg.type === "debate_comment") {
|
|
1518
1675
|
handleDebateComment(ws, msg);
|
|
1519
1676
|
return;
|
|
@@ -1530,6 +1687,10 @@ function createProjectContext(opts) {
|
|
|
1530
1687
|
handleDebateConfirmBrief(ws);
|
|
1531
1688
|
return;
|
|
1532
1689
|
}
|
|
1690
|
+
if (msg.type === "debate_user_floor_response") {
|
|
1691
|
+
handleDebateUserFloorResponse(ws, msg);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1533
1694
|
|
|
1534
1695
|
// --- Knowledge file management ---
|
|
1535
1696
|
if (msg.type === "knowledge_list") {
|
|
@@ -1694,80 +1855,10 @@ function createProjectContext(opts) {
|
|
|
1694
1855
|
return;
|
|
1695
1856
|
}
|
|
1696
1857
|
|
|
1697
|
-
// --- Memory (session digests) management ---
|
|
1698
|
-
if (msg.type === "memory_list") {
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
var entries = [];
|
|
1702
|
-
var summary = "";
|
|
1703
|
-
try {
|
|
1704
|
-
var raw = fs.readFileSync(digestFile, "utf8").trim();
|
|
1705
|
-
if (raw) {
|
|
1706
|
-
var lines = raw.split("\n");
|
|
1707
|
-
for (var mi = 0; mi < lines.length; mi++) {
|
|
1708
|
-
try {
|
|
1709
|
-
var obj = JSON.parse(lines[mi]);
|
|
1710
|
-
obj.index = mi;
|
|
1711
|
-
entries.push(obj);
|
|
1712
|
-
} catch (e) {}
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
} catch (e) { /* file may not exist */ }
|
|
1716
|
-
try {
|
|
1717
|
-
if (fs.existsSync(summaryFile)) {
|
|
1718
|
-
summary = fs.readFileSync(summaryFile, "utf8").trim();
|
|
1719
|
-
}
|
|
1720
|
-
} catch (e) {}
|
|
1721
|
-
// Return newest first
|
|
1722
|
-
entries.reverse();
|
|
1723
|
-
sendTo(ws, { type: "memory_list", entries: entries, summary: summary });
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
if (msg.type === "memory_search") {
|
|
1728
|
-
if (!msg.query || typeof msg.query !== "string") {
|
|
1729
|
-
sendTo(ws, { type: "memory_search_results", results: [], query: "" });
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
|
|
1733
|
-
try {
|
|
1734
|
-
var results = sessionSearch.searchDigests(digestFile, msg.query, {
|
|
1735
|
-
maxResults: msg.maxResults || 10,
|
|
1736
|
-
minScore: msg.minScore || 0.5,
|
|
1737
|
-
dateFrom: msg.dateFrom || null,
|
|
1738
|
-
dateTo: msg.dateTo || null
|
|
1739
|
-
});
|
|
1740
|
-
sendTo(ws, {
|
|
1741
|
-
type: "memory_search_results",
|
|
1742
|
-
results: sessionSearch.formatForMemoryUI(results),
|
|
1743
|
-
query: msg.query
|
|
1744
|
-
});
|
|
1745
|
-
} catch (e) {
|
|
1746
|
-
console.error("[session-search] Search failed:", e.message);
|
|
1747
|
-
sendTo(ws, { type: "memory_search_results", results: [], query: msg.query });
|
|
1748
|
-
}
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
if (msg.type === "memory_delete") {
|
|
1753
|
-
if (typeof msg.index !== "number") return;
|
|
1754
|
-
var digestFile = path.join(cwd, "knowledge", "session-digests.jsonl");
|
|
1755
|
-
try {
|
|
1756
|
-
var raw = fs.readFileSync(digestFile, "utf8").trim();
|
|
1757
|
-
var lines = raw ? raw.split("\n") : [];
|
|
1758
|
-
if (msg.index >= 0 && msg.index < lines.length) {
|
|
1759
|
-
lines.splice(msg.index, 1);
|
|
1760
|
-
if (lines.length === 0) {
|
|
1761
|
-
fs.unlinkSync(digestFile);
|
|
1762
|
-
} else {
|
|
1763
|
-
fs.writeFileSync(digestFile, lines.join("\n") + "\n");
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
} catch (e) {}
|
|
1767
|
-
sendTo(ws, { type: "memory_deleted", index: msg.index });
|
|
1768
|
-
handleMessage(ws, { type: "memory_list" });
|
|
1769
|
-
return;
|
|
1770
|
-
}
|
|
1858
|
+
// --- Memory (session digests) management (delegated to project-memory.js) ---
|
|
1859
|
+
if (msg.type === "memory_list") { _memory.handleMemoryList(ws); return; }
|
|
1860
|
+
if (msg.type === "memory_search") { _memory.handleMemorySearch(ws, msg); return; }
|
|
1861
|
+
if (msg.type === "memory_delete") { _memory.handleMemoryDelete(ws, msg); return; }
|
|
1771
1862
|
|
|
1772
1863
|
if (msg.type === "push_subscribe") {
|
|
1773
1864
|
var _pushUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
@@ -2204,6 +2295,8 @@ function createProjectContext(opts) {
|
|
|
2204
2295
|
if (msg.type === "rewind_preview") {
|
|
2205
2296
|
var session = getSessionForWs(ws);
|
|
2206
2297
|
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2298
|
+
// Reject preview requests while a rewind is executing
|
|
2299
|
+
if (session._rewindInProgress) return;
|
|
2207
2300
|
|
|
2208
2301
|
(async function () {
|
|
2209
2302
|
var result;
|
|
@@ -2233,6 +2326,12 @@ function createProjectContext(opts) {
|
|
|
2233
2326
|
if (msg.type === "rewind_execute") {
|
|
2234
2327
|
var session = getSessionForWs(ws);
|
|
2235
2328
|
if (!session || !session.cliSessionId || !msg.uuid) return;
|
|
2329
|
+
// Guard against concurrent rewind executions
|
|
2330
|
+
if (session._rewindInProgress) {
|
|
2331
|
+
sendTo(ws, { type: "rewind_error", text: "Rewind already in progress." });
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
session._rewindInProgress = true;
|
|
2236
2335
|
var mode = msg.mode || "both";
|
|
2237
2336
|
|
|
2238
2337
|
(async function () {
|
|
@@ -2293,6 +2392,7 @@ function createProjectContext(opts) {
|
|
|
2293
2392
|
} catch (err) {
|
|
2294
2393
|
sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
|
|
2295
2394
|
} finally {
|
|
2395
|
+
session._rewindInProgress = false;
|
|
2296
2396
|
if (result && result.isTemp) result.cleanup();
|
|
2297
2397
|
}
|
|
2298
2398
|
})();
|
|
@@ -3328,7 +3428,7 @@ function createProjectContext(opts) {
|
|
|
3328
3428
|
return;
|
|
3329
3429
|
}
|
|
3330
3430
|
}
|
|
3331
|
-
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws));
|
|
3431
|
+
var t = tm.create(msg.cols || 80, msg.rows || 24, getOsUserInfoForWs(ws), ws);
|
|
3332
3432
|
if (!t) {
|
|
3333
3433
|
sendTo(ws, { type: "term_error", error: "Cannot create terminal (node-pty not available or limit reached)" });
|
|
3334
3434
|
return;
|
|
@@ -3356,7 +3456,7 @@ function createProjectContext(opts) {
|
|
|
3356
3456
|
|
|
3357
3457
|
if (msg.type === "term_resize") {
|
|
3358
3458
|
if (msg.id && msg.cols > 0 && msg.rows > 0) {
|
|
3359
|
-
tm.resize(msg.id, msg.cols, msg.rows);
|
|
3459
|
+
tm.resize(msg.id, msg.cols, msg.rows, ws);
|
|
3360
3460
|
}
|
|
3361
3461
|
return;
|
|
3362
3462
|
}
|
|
@@ -3365,6 +3465,14 @@ function createProjectContext(opts) {
|
|
|
3365
3465
|
if (msg.id) {
|
|
3366
3466
|
tm.close(msg.id);
|
|
3367
3467
|
send({ type: "term_list", terminals: tm.list() });
|
|
3468
|
+
// Remove closed terminal from context sources
|
|
3469
|
+
var saved = loadContextSources(slug);
|
|
3470
|
+
var termKey = "term:" + msg.id;
|
|
3471
|
+
var filtered = saved.filter(function(id) { return id !== termKey; });
|
|
3472
|
+
if (filtered.length !== saved.length) {
|
|
3473
|
+
saveContextSources(slug, filtered);
|
|
3474
|
+
send({ type: "context_sources_state", active: filtered });
|
|
3475
|
+
}
|
|
3368
3476
|
}
|
|
3369
3477
|
return;
|
|
3370
3478
|
}
|
|
@@ -3377,6 +3485,34 @@ function createProjectContext(opts) {
|
|
|
3377
3485
|
return;
|
|
3378
3486
|
}
|
|
3379
3487
|
|
|
3488
|
+
// --- Context Sources ---
|
|
3489
|
+
if (msg.type === "context_sources_save") {
|
|
3490
|
+
var activeIds = msg.active || [];
|
|
3491
|
+
saveContextSources(slug, activeIds);
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
// --- Browser Extension ---
|
|
3496
|
+
if (msg.type === "browser_tab_list") {
|
|
3497
|
+
_extensionWs = ws; // Track which client has the extension
|
|
3498
|
+
var tabs = msg.tabs || [];
|
|
3499
|
+
_browserTabList = {};
|
|
3500
|
+
for (var bti = 0; bti < tabs.length; bti++) {
|
|
3501
|
+
_browserTabList[tabs[bti].id] = tabs[bti];
|
|
3502
|
+
}
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
if (msg.type === "extension_result") {
|
|
3507
|
+
var pending = pendingExtensionRequests[msg.requestId];
|
|
3508
|
+
if (pending) {
|
|
3509
|
+
clearTimeout(pending.timer);
|
|
3510
|
+
pending.resolve(msg.result);
|
|
3511
|
+
delete pendingExtensionRequests[msg.requestId];
|
|
3512
|
+
}
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3380
3516
|
// --- Scheduled tasks permission gate ---
|
|
3381
3517
|
if (msg.type === "loop_start" || msg.type === "loop_stop" || msg.type === "loop_registry_files" ||
|
|
3382
3518
|
msg.type === "loop_registry_list" || msg.type === "loop_registry_update" || msg.type === "loop_registry_rename" ||
|
|
@@ -3757,6 +3893,26 @@ function createProjectContext(opts) {
|
|
|
3757
3893
|
return;
|
|
3758
3894
|
}
|
|
3759
3895
|
|
|
3896
|
+
if (msg.type === "send_scheduled_now") {
|
|
3897
|
+
var nowSession = getSessionForWs(ws);
|
|
3898
|
+
if (!nowSession || !nowSession.scheduledMessage) return;
|
|
3899
|
+
var schedText = nowSession.scheduledMessage.text;
|
|
3900
|
+
clearTimeout(nowSession.scheduledMessage.timer);
|
|
3901
|
+
nowSession.scheduledMessage = null;
|
|
3902
|
+
console.log("[project] Scheduled message sent immediately for session " + nowSession.localId);
|
|
3903
|
+
sm.sendAndRecord(nowSession, { type: "scheduled_message_sent" });
|
|
3904
|
+
var userMsg = { type: "user_message", text: schedText };
|
|
3905
|
+
nowSession.history.push(userMsg);
|
|
3906
|
+
sm.appendToSessionFile(nowSession, userMsg);
|
|
3907
|
+
sendToSession(nowSession.localId, userMsg);
|
|
3908
|
+
nowSession.isProcessing = true;
|
|
3909
|
+
onProcessingChanged();
|
|
3910
|
+
sendToSession(nowSession.localId, { type: "status", status: "processing" });
|
|
3911
|
+
sdk.startQuery(nowSession, schedText, null, getLinuxUserForSession(nowSession));
|
|
3912
|
+
sm.broadcastSessionList();
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3760
3916
|
if (msg.type !== "message") return;
|
|
3761
3917
|
if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
|
|
3762
3918
|
|
|
@@ -3835,1326 +3991,315 @@ function createProjectContext(opts) {
|
|
|
3835
3991
|
fullText = mentionPrefix + "\n\n" + fullText;
|
|
3836
3992
|
}
|
|
3837
3993
|
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
}
|
|
3916
|
-
if (lastIdx === -1 || lastIdx >= recentTurns.length - 1) return "";
|
|
3917
|
-
|
|
3918
|
-
// Collect turns after the last mention response
|
|
3919
|
-
var lines = ["[Conversation since your last response:]", "---"];
|
|
3920
|
-
for (var j = lastIdx + 1; j < recentTurns.length; j++) {
|
|
3921
|
-
var turn = recentTurns[j];
|
|
3922
|
-
lines.push(turn.role + ": " + turn.text);
|
|
3923
|
-
}
|
|
3924
|
-
lines.push("---");
|
|
3925
|
-
return lines.join("\n");
|
|
3926
|
-
}
|
|
3927
|
-
|
|
3928
|
-
function buildMentionContext(userName, recentTurns) {
|
|
3929
|
-
var lines = [
|
|
3930
|
-
"You were @mentioned in a project session by " + userName + ".",
|
|
3931
|
-
"You are responding inline in their conversation. Keep your response focused on what was asked.",
|
|
3932
|
-
"You have read-only access to the project files but cannot make changes.",
|
|
3933
|
-
"",
|
|
3934
|
-
"Recent conversation context:",
|
|
3935
|
-
"---",
|
|
3936
|
-
];
|
|
3937
|
-
for (var i = 0; i < recentTurns.length; i++) {
|
|
3938
|
-
var turn = recentTurns[i];
|
|
3939
|
-
lines.push(turn.role + ": " + turn.text);
|
|
3940
|
-
}
|
|
3941
|
-
lines.push("---");
|
|
3942
|
-
return lines.join("\n");
|
|
3943
|
-
}
|
|
3944
|
-
|
|
3945
|
-
// --- Shared digest worker: one reusable Haiku session for gate+digest ---
|
|
3946
|
-
// Combines gate check and digest generation into a single prompt,
|
|
3947
|
-
// processes jobs sequentially from a queue, reuses the session across calls.
|
|
3948
|
-
// Session is recycled after DIGEST_WORKER_MAX_TURNS to prevent context bloat.
|
|
3949
|
-
var _digestWorker = null;
|
|
3950
|
-
var _digestQueue = [];
|
|
3951
|
-
var _digestBusy = false;
|
|
3952
|
-
var _digestWorkerTurns = 0;
|
|
3953
|
-
var DIGEST_WORKER_MAX_TURNS = 20;
|
|
3954
|
-
|
|
3955
|
-
function enqueueDigest(job) {
|
|
3956
|
-
_digestQueue.push(job);
|
|
3957
|
-
if (!_digestBusy) processDigestQueue();
|
|
3958
|
-
}
|
|
3959
|
-
|
|
3960
|
-
function processDigestQueue() {
|
|
3961
|
-
if (_digestQueue.length === 0) { _digestBusy = false; return; }
|
|
3962
|
-
_digestBusy = true;
|
|
3963
|
-
var job = _digestQueue.shift();
|
|
3964
|
-
|
|
3965
|
-
var mateDir = matesModule.getMateDir(job.mateCtx, job.mateId);
|
|
3966
|
-
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
3967
|
-
|
|
3968
|
-
// Load mate role for gate context
|
|
3969
|
-
var mateRole = "";
|
|
3970
|
-
try {
|
|
3971
|
-
var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
|
|
3972
|
-
var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
|
|
3973
|
-
if (roleMatch) mateRole = roleMatch[1].trim();
|
|
3974
|
-
} catch (e) {}
|
|
3975
|
-
|
|
3976
|
-
// Combined gate + digest in one prompt (saves a full round-trip vs separate gate)
|
|
3977
|
-
var prompt = [
|
|
3978
|
-
"[SYSTEM: Memory Gate + Digest]",
|
|
3979
|
-
"You are a memory system for an AI Mate (role: " + (mateRole || "assistant") + ").",
|
|
3980
|
-
"",
|
|
3981
|
-
"Conversation (" + job.type + "):",
|
|
3982
|
-
job.conversationContent,
|
|
3983
|
-
"",
|
|
3984
|
-
"STEP 1: Should this be saved to memory?",
|
|
3985
|
-
'Answer "no" ONLY if the entire conversation is trivial (e.g. just "hi"/"hello").',
|
|
3986
|
-
"When in doubt, save it.",
|
|
3987
|
-
"",
|
|
3988
|
-
'STEP 2: If yes, output a JSON digest. If no, output exactly: {"skip":true}',
|
|
3989
|
-
"",
|
|
3990
|
-
"JSON schema (output ONLY the JSON, no markdown, no fences):",
|
|
3991
|
-
"{",
|
|
3992
|
-
' "date": "YYYY-MM-DD",',
|
|
3993
|
-
' "type": "' + job.type + '",',
|
|
3994
|
-
' "topic": "short topic description",',
|
|
3995
|
-
' "summary": "2-3 sentence summary",',
|
|
3996
|
-
' "key_quotes": ["user quotes, verbatim, max 5"],',
|
|
3997
|
-
' "user_context": "personal/project context or null",',
|
|
3998
|
-
' "my_position": "what I said/recommended",',
|
|
3999
|
-
job.type === "dm" ? ' "user_intent": "what the user wanted",' : ' "other_perspectives": "key points from others",',
|
|
4000
|
-
' "decisions": "what was decided or null",',
|
|
4001
|
-
' "open_items": "what remains unresolved",',
|
|
4002
|
-
' "user_sentiment": "how user felt",',
|
|
4003
|
-
' "confidence": "high|medium|low",',
|
|
4004
|
-
' "revisit_later": true/false,',
|
|
4005
|
-
' "tags": ["topic", "tags"],',
|
|
4006
|
-
' "user_observations": [{"category":"pattern|decision|reaction|preference","observation":"...","evidence":"..."}]',
|
|
4007
|
-
"}",
|
|
4008
|
-
"",
|
|
4009
|
-
"user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
|
|
4010
|
-
"Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
|
|
4011
|
-
"Omit the field entirely if nothing notable about the user.",
|
|
4012
|
-
].join("\n");
|
|
4013
|
-
|
|
4014
|
-
function handleResult(text) {
|
|
4015
|
-
var cleaned = text.trim();
|
|
4016
|
-
if (cleaned.indexOf("```") === 0) {
|
|
4017
|
-
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
|
-
var digestObj = null;
|
|
4021
|
-
try { digestObj = JSON.parse(cleaned); } catch (e) {
|
|
4022
|
-
console.error("[digest-worker] Parse failed for " + job.mateId + ":", e.message);
|
|
4023
|
-
digestObj = { date: new Date().toISOString().slice(0, 10), topic: "parse_failed", raw: text.substring(0, 500) };
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
|
-
if (digestObj && digestObj.skip) {
|
|
4027
|
-
console.log("[digest-worker] Gate declined for " + job.mateId);
|
|
4028
|
-
if (job.onDone) job.onDone();
|
|
4029
|
-
processDigestQueue();
|
|
4030
|
-
return;
|
|
4031
|
-
}
|
|
4032
|
-
|
|
4033
|
-
try {
|
|
4034
|
-
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
4035
|
-
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
4036
|
-
fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
|
|
4037
|
-
} catch (e) {
|
|
4038
|
-
console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
|
|
4039
|
-
}
|
|
4040
|
-
|
|
4041
|
-
// Write user observations if present
|
|
4042
|
-
if (digestObj.user_observations && digestObj.user_observations.length > 0) {
|
|
4043
|
-
try {
|
|
4044
|
-
var obsFile = path.join(knowledgeDir, "user-observations.jsonl");
|
|
4045
|
-
var obsMate = matesModule.getMate(job.mateCtx, job.mateId);
|
|
4046
|
-
var obsMateName = (obsMate && obsMate.name) || job.mateId;
|
|
4047
|
-
var obsLines = [];
|
|
4048
|
-
for (var oi = 0; oi < digestObj.user_observations.length; oi++) {
|
|
4049
|
-
var obs = digestObj.user_observations[oi];
|
|
4050
|
-
obsLines.push(JSON.stringify({
|
|
4051
|
-
date: digestObj.date || new Date().toISOString().slice(0, 10),
|
|
4052
|
-
category: obs.category || "pattern",
|
|
4053
|
-
observation: obs.observation || "",
|
|
4054
|
-
evidence: obs.evidence || "",
|
|
4055
|
-
confidence: digestObj.confidence || "medium",
|
|
4056
|
-
mateName: obsMateName,
|
|
4057
|
-
mateId: job.mateId
|
|
4058
|
-
}));
|
|
3994
|
+
// Inject active terminal context sources (delta only: send new output since last message)
|
|
3995
|
+
var TERM_CONTEXT_MAX = 8192; // 8KB max per terminal per message
|
|
3996
|
+
var TERM_HEAD_SIZE = 2048; // keep first 2KB for error context
|
|
3997
|
+
var TERM_TAIL_SIZE = 6144; // keep last 6KB for recent state
|
|
3998
|
+
var ctxSources = loadContextSources(slug);
|
|
3999
|
+
if (ctxSources.length > 0) {
|
|
4000
|
+
if (!session._termContextCursors) session._termContextCursors = {};
|
|
4001
|
+
var termContextParts = [];
|
|
4002
|
+
for (var ci = 0; ci < ctxSources.length; ci++) {
|
|
4003
|
+
var srcId = ctxSources[ci];
|
|
4004
|
+
if (srcId.startsWith("term:")) {
|
|
4005
|
+
var termId = parseInt(srcId.split(":")[1], 10);
|
|
4006
|
+
var sb = tm.getScrollback(termId);
|
|
4007
|
+
if (sb) {
|
|
4008
|
+
var lastCursor;
|
|
4009
|
+
if (termId in session._termContextCursors) {
|
|
4010
|
+
lastCursor = session._termContextCursors[termId];
|
|
4011
|
+
// Terminal was recycled (closed and reopened with same ID) — reset cursor
|
|
4012
|
+
if (lastCursor > sb.totalBytesWritten) lastCursor = 0;
|
|
4013
|
+
} else {
|
|
4014
|
+
// First time seeing this terminal — include last 8KB (what user can see now)
|
|
4015
|
+
lastCursor = Math.max(0, sb.totalBytesWritten - TERM_CONTEXT_MAX);
|
|
4016
|
+
}
|
|
4017
|
+
var newBytes = sb.totalBytesWritten - lastCursor;
|
|
4018
|
+
session._termContextCursors[termId] = sb.totalBytesWritten;
|
|
4019
|
+
if (newBytes <= 0) continue;
|
|
4020
|
+
// Build timestamped delta from chunks
|
|
4021
|
+
var deltaChunks = [];
|
|
4022
|
+
var bytePos = sb.bufferStart;
|
|
4023
|
+
for (var chunkIdx = 0; chunkIdx < sb.chunks.length; chunkIdx++) {
|
|
4024
|
+
var chunk = sb.chunks[chunkIdx];
|
|
4025
|
+
var chunkEnd = bytePos + chunk.data.length;
|
|
4026
|
+
if (chunkEnd > lastCursor) {
|
|
4027
|
+
// This chunk has new content
|
|
4028
|
+
var chunkData = chunk.data;
|
|
4029
|
+
if (bytePos < lastCursor) {
|
|
4030
|
+
// Partial chunk: only the part after lastCursor
|
|
4031
|
+
chunkData = chunkData.slice(lastCursor - bytePos);
|
|
4032
|
+
}
|
|
4033
|
+
deltaChunks.push({ ts: chunk.ts, data: chunkData });
|
|
4034
|
+
}
|
|
4035
|
+
bytePos = chunkEnd;
|
|
4036
|
+
}
|
|
4037
|
+
if (deltaChunks.length === 0) continue;
|
|
4038
|
+
// Format with timestamps: group by second to avoid excessive timestamps
|
|
4039
|
+
var lines = [];
|
|
4040
|
+
var lastTimeSec = 0;
|
|
4041
|
+
for (var di = 0; di < deltaChunks.length; di++) {
|
|
4042
|
+
var dc = deltaChunks[di];
|
|
4043
|
+
var cleaned = dc.data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
4044
|
+
if (!cleaned) continue;
|
|
4045
|
+
var timeSec = Math.floor(dc.ts / 1000);
|
|
4046
|
+
if (timeSec !== lastTimeSec) {
|
|
4047
|
+
var d = new Date(dc.ts);
|
|
4048
|
+
var timeStr = d.toTimeString().slice(0, 8); // HH:MM:SS
|
|
4049
|
+
lines.push("[" + timeStr + "] " + cleaned);
|
|
4050
|
+
lastTimeSec = timeSec;
|
|
4051
|
+
} else {
|
|
4052
|
+
lines.push(cleaned);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
var delta = lines.join("").trim();
|
|
4056
|
+
if (!delta) continue;
|
|
4057
|
+
var termInfo = tm.list().find(function(t) { return t.id === termId; });
|
|
4058
|
+
var termTitle = termInfo ? termInfo.title : "Terminal " + termId;
|
|
4059
|
+
var header;
|
|
4060
|
+
if (delta.length > TERM_CONTEXT_MAX) {
|
|
4061
|
+
var head = delta.slice(0, TERM_HEAD_SIZE);
|
|
4062
|
+
var tail = delta.slice(-TERM_TAIL_SIZE);
|
|
4063
|
+
var omittedBytes = delta.length - TERM_HEAD_SIZE - TERM_TAIL_SIZE;
|
|
4064
|
+
var omittedLines = delta.slice(TERM_HEAD_SIZE, delta.length - TERM_TAIL_SIZE).split("\n").length;
|
|
4065
|
+
delta = head + "\n\n... (" + omittedLines + " lines / " + Math.round(omittedBytes / 1024) + "KB omitted) ...\n\n" + tail;
|
|
4066
|
+
header = "[New terminal output from " + termTitle + " (large output, head+tail shown)]";
|
|
4067
|
+
} else {
|
|
4068
|
+
header = "[New terminal output from " + termTitle + "]";
|
|
4069
|
+
}
|
|
4070
|
+
termContextParts.push(header + "\n```\n" + delta + "\n```");
|
|
4059
4071
|
}
|
|
4060
|
-
fs.appendFileSync(obsFile, obsLines.join("\n") + "\n");
|
|
4061
|
-
} catch (e) {
|
|
4062
|
-
console.error("[digest-worker] Observations write failed for " + job.mateId + ":", e.message);
|
|
4063
4072
|
}
|
|
4064
4073
|
}
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
|
|
4068
|
-
if (job.onDone) job.onDone();
|
|
4069
|
-
processDigestQueue();
|
|
4070
|
-
}
|
|
4071
|
-
|
|
4072
|
-
// Recycle worker session if it has exceeded max turns
|
|
4073
|
-
if (_digestWorker && _digestWorkerTurns >= DIGEST_WORKER_MAX_TURNS) {
|
|
4074
|
-
try { _digestWorker.close(); } catch (e) {}
|
|
4075
|
-
_digestWorker = null;
|
|
4076
|
-
_digestWorkerTurns = 0;
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
|
-
var responseText = "";
|
|
4080
|
-
if (_digestWorker && _digestWorker.isAlive()) {
|
|
4081
|
-
_digestWorkerTurns++;
|
|
4082
|
-
_digestWorker.pushMessage(prompt, {
|
|
4083
|
-
onActivity: function () {},
|
|
4084
|
-
onDelta: function (d) { responseText += d; },
|
|
4085
|
-
onDone: function () { handleResult(responseText); },
|
|
4086
|
-
onError: function (err) {
|
|
4087
|
-
console.error("[digest-worker] Error:", err);
|
|
4088
|
-
_digestWorker = null;
|
|
4089
|
-
_digestWorkerTurns = 0;
|
|
4090
|
-
if (job.onDone) job.onDone();
|
|
4091
|
-
processDigestQueue();
|
|
4092
|
-
},
|
|
4093
|
-
});
|
|
4094
|
-
} else {
|
|
4095
|
-
sdk.createMentionSession({
|
|
4096
|
-
claudeMd: "",
|
|
4097
|
-
model: "haiku",
|
|
4098
|
-
initialContext: "[Digest Worker] You generate memory digests. Respond with ONLY JSON.",
|
|
4099
|
-
initialMessage: prompt,
|
|
4100
|
-
onActivity: function () {},
|
|
4101
|
-
onDelta: function (d) { responseText += d; },
|
|
4102
|
-
onDone: function () { handleResult(responseText); },
|
|
4103
|
-
onError: function (err) {
|
|
4104
|
-
console.error("[digest-worker] Create error:", err);
|
|
4105
|
-
_digestWorker = null;
|
|
4106
|
-
if (job.onDone) job.onDone();
|
|
4107
|
-
processDigestQueue();
|
|
4108
|
-
},
|
|
4109
|
-
}).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function () {
|
|
4110
|
-
if (job.onDone) job.onDone();
|
|
4111
|
-
processDigestQueue();
|
|
4112
|
-
});
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
|
-
function digestMentionSession(session, mateId, mateCtx, mateResponse, userQuestion) {
|
|
4117
|
-
if (!session._mentionSessions || !session._mentionSessions[mateId]) return;
|
|
4118
|
-
var mentionSession = session._mentionSessions[mateId];
|
|
4119
|
-
if (!mentionSession.isAlive()) return;
|
|
4120
|
-
|
|
4121
|
-
mentionSession._digesting = true;
|
|
4122
|
-
|
|
4123
|
-
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4124
|
-
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
4125
|
-
|
|
4126
|
-
// Migration: generate initial summary if missing
|
|
4127
|
-
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4128
|
-
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
4129
|
-
if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
|
|
4130
|
-
initMemorySummary(mateCtx, mateId, function () {});
|
|
4131
|
-
}
|
|
4132
|
-
|
|
4133
|
-
var userQ = userQuestion || "(unknown)";
|
|
4134
|
-
var mateR = mateResponse || "(unknown)";
|
|
4135
|
-
var conversationContent = "User: " + (userQ.length > 2000 ? userQ.substring(0, 2000) + "..." : userQ) +
|
|
4136
|
-
"\nMate: " + (mateR.length > 2000 ? mateR.substring(0, 2000) + "..." : mateR);
|
|
4137
|
-
|
|
4138
|
-
enqueueDigest({
|
|
4139
|
-
mateCtx: mateCtx,
|
|
4140
|
-
mateId: mateId,
|
|
4141
|
-
type: "mention",
|
|
4142
|
-
conversationContent: conversationContent,
|
|
4143
|
-
onDone: function () { mentionSession._digesting = false; },
|
|
4144
|
-
});
|
|
4145
|
-
}
|
|
4146
|
-
|
|
4147
|
-
// Digest DM turn for mate projects - uses shared digest worker
|
|
4148
|
-
var _dmDigestPending = false;
|
|
4149
|
-
function digestDmTurn(session, responsePreview) {
|
|
4150
|
-
if (!isMate || _dmDigestPending) return;
|
|
4151
|
-
var mateId = path.basename(cwd);
|
|
4152
|
-
var mateCtx = matesModule.buildMateCtx(projectOwnerId);
|
|
4153
|
-
if (!matesModule.isMate(mateCtx, mateId)) return;
|
|
4154
|
-
|
|
4155
|
-
// Collect full conversation from session history (all user + mate turns)
|
|
4156
|
-
var conversationParts = [];
|
|
4157
|
-
var totalLen = 0;
|
|
4158
|
-
var CONV_CAP = 6000;
|
|
4159
|
-
for (var hi = 0; hi < session.history.length; hi++) {
|
|
4160
|
-
var entry = session.history[hi];
|
|
4161
|
-
if (entry.type === "user_message" && entry.text) {
|
|
4162
|
-
var uText = entry.text;
|
|
4163
|
-
if (totalLen + uText.length > CONV_CAP) {
|
|
4164
|
-
uText = uText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
|
|
4165
|
-
}
|
|
4166
|
-
conversationParts.push("User: " + uText);
|
|
4167
|
-
totalLen += uText.length;
|
|
4168
|
-
} else if (entry.type === "assistant_message" && entry.text) {
|
|
4169
|
-
var aText = entry.text;
|
|
4170
|
-
if (totalLen + aText.length > CONV_CAP) {
|
|
4171
|
-
aText = aText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
|
|
4172
|
-
}
|
|
4173
|
-
conversationParts.push("Mate: " + aText);
|
|
4174
|
-
totalLen += aText.length;
|
|
4175
|
-
}
|
|
4176
|
-
if (totalLen >= CONV_CAP) break;
|
|
4177
|
-
}
|
|
4178
|
-
var lastResponseText = responsePreview || "";
|
|
4179
|
-
if (lastResponseText && conversationParts.length > 0) {
|
|
4180
|
-
var lastPart = conversationParts[conversationParts.length - 1];
|
|
4181
|
-
if (lastPart.indexOf("Mate:") !== 0 || lastPart.indexOf(lastResponseText.substring(0, 50)) === -1) {
|
|
4182
|
-
var rText = lastResponseText;
|
|
4183
|
-
if (totalLen + rText.length > CONV_CAP) {
|
|
4184
|
-
rText = rText.substring(0, Math.max(200, CONV_CAP - totalLen)) + "...";
|
|
4185
|
-
}
|
|
4186
|
-
conversationParts.push("Mate: " + rText);
|
|
4074
|
+
if (termContextParts.length > 0) {
|
|
4075
|
+
fullText = termContextParts.join("\n\n") + "\n\n" + fullText;
|
|
4187
4076
|
}
|
|
4188
4077
|
}
|
|
4189
|
-
if (conversationParts.length === 0) return;
|
|
4190
|
-
|
|
4191
|
-
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4192
|
-
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
4193
|
-
|
|
4194
|
-
// Migration: if memory-summary.md missing but digests exist, generate initial summary
|
|
4195
|
-
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4196
|
-
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
4197
|
-
if (!fs.existsSync(summaryFile) && fs.existsSync(digestFile)) {
|
|
4198
|
-
initMemorySummary(mateCtx, mateId, function () {
|
|
4199
|
-
console.log("[memory-migrate] Initial summary generated for mate " + mateId);
|
|
4200
|
-
});
|
|
4201
|
-
}
|
|
4202
4078
|
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
conversationContent: conversationParts.join("\n"),
|
|
4210
|
-
onDone: function () { _dmDigestPending = false; },
|
|
4079
|
+
// Collect browser tab context (async: requires round-trip to client extension)
|
|
4080
|
+
var tabSources = ctxSources.filter(function(id) {
|
|
4081
|
+
if (!id.startsWith("tab:")) return false;
|
|
4082
|
+
// Only include tabs that currently exist in the browser
|
|
4083
|
+
var tid = parseInt(id.split(":")[1], 10);
|
|
4084
|
+
return !!_browserTabList[tid];
|
|
4211
4085
|
});
|
|
4212
|
-
}
|
|
4213
|
-
|
|
4214
|
-
function handleMention(ws, msg) {
|
|
4215
|
-
if (!msg.mateId) return;
|
|
4216
|
-
if (!msg.text && (!msg.images || msg.images.length === 0) && (!msg.pastes || msg.pastes.length === 0)) return;
|
|
4217
|
-
|
|
4218
|
-
var session = getSessionForWs(ws);
|
|
4219
|
-
if (!session) return;
|
|
4220
|
-
|
|
4221
|
-
// Block mentions during an active debate
|
|
4222
|
-
if (session._debate && session._debate.phase === "live") {
|
|
4223
|
-
sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Cannot use @mentions during an active debate." });
|
|
4224
|
-
return;
|
|
4225
|
-
}
|
|
4226
|
-
|
|
4227
|
-
// Check if a mention is already in progress for this session
|
|
4228
|
-
if (session._mentionInProgress) {
|
|
4229
|
-
sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "A mention is already in progress." });
|
|
4230
|
-
return;
|
|
4231
|
-
}
|
|
4232
|
-
|
|
4233
|
-
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
4234
|
-
var mateCtx = matesModule.buildMateCtx(userId);
|
|
4235
|
-
var mate = matesModule.getMate(mateCtx, msg.mateId);
|
|
4236
|
-
if (!mate) {
|
|
4237
|
-
sendTo(ws, { type: "mention_error", mateId: msg.mateId, error: "Mate not found" });
|
|
4238
|
-
return;
|
|
4239
|
-
}
|
|
4240
|
-
|
|
4241
|
-
var mateName = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
|
|
4242
|
-
var avatarColor = (mate.profile && mate.profile.avatarColor) || "#6c5ce7";
|
|
4243
|
-
var avatarStyle = (mate.profile && mate.profile.avatarStyle) || "bottts";
|
|
4244
|
-
var avatarSeed = (mate.profile && mate.profile.avatarSeed) || mate.id;
|
|
4245
|
-
|
|
4246
|
-
// Build full mention text (include pasted content)
|
|
4247
|
-
var mentionFullInput = msg.text || "";
|
|
4248
|
-
if (msg.pastes && msg.pastes.length > 0) {
|
|
4249
|
-
for (var pi = 0; pi < msg.pastes.length; pi++) {
|
|
4250
|
-
if (mentionFullInput) mentionFullInput += "\n\n";
|
|
4251
|
-
mentionFullInput += msg.pastes[pi];
|
|
4252
|
-
}
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
|
-
// Save images to disk (same pattern as regular messages)
|
|
4256
|
-
var imageRefs = [];
|
|
4257
|
-
if (msg.images && msg.images.length > 0) {
|
|
4258
|
-
for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
|
|
4259
|
-
var img = msg.images[imgIdx];
|
|
4260
|
-
var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
|
|
4261
|
-
if (savedName) {
|
|
4262
|
-
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
4263
|
-
}
|
|
4264
|
-
}
|
|
4265
|
-
}
|
|
4266
|
-
|
|
4267
|
-
// Save mention user message to session history
|
|
4268
|
-
var mentionUserEntry = { type: "mention_user", text: msg.text, mateId: msg.mateId, mateName: mateName };
|
|
4269
|
-
if (msg.pastes && msg.pastes.length > 0) mentionUserEntry.pastes = msg.pastes;
|
|
4270
|
-
if (imageRefs.length > 0) mentionUserEntry.imageRefs = imageRefs;
|
|
4271
|
-
session.history.push(mentionUserEntry);
|
|
4272
|
-
sm.appendToSessionFile(session, mentionUserEntry);
|
|
4273
|
-
sendToSessionOthers(ws, session.localId, hydrateImageRefs(mentionUserEntry));
|
|
4274
|
-
|
|
4275
|
-
// Extract recent turns for continuity check
|
|
4276
|
-
var recentTurns = getRecentTurns(session, MENTION_WINDOW);
|
|
4277
|
-
|
|
4278
|
-
// Determine user name for context
|
|
4279
|
-
var userName = "User";
|
|
4280
|
-
if (ws._clayUser) {
|
|
4281
|
-
var p = ws._clayUser.profile || {};
|
|
4282
|
-
userName = p.name || ws._clayUser.displayName || ws._clayUser.username || "User";
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
|
-
session._mentionInProgress = true;
|
|
4286
|
-
|
|
4287
|
-
// Send mention start indicator
|
|
4288
|
-
sendToSession(session.localId, {
|
|
4289
|
-
type: "mention_start",
|
|
4290
|
-
mateId: msg.mateId,
|
|
4291
|
-
mateName: mateName,
|
|
4292
|
-
avatarColor: avatarColor,
|
|
4293
|
-
avatarStyle: avatarStyle,
|
|
4294
|
-
avatarSeed: avatarSeed,
|
|
4295
|
-
});
|
|
4296
|
-
|
|
4297
|
-
// Shared callbacks for both new and continued sessions
|
|
4298
|
-
var mentionCallbacks = {
|
|
4299
|
-
onActivity: function (activity) {
|
|
4300
|
-
sendToSession(session.localId, {
|
|
4301
|
-
type: "mention_activity",
|
|
4302
|
-
mateId: msg.mateId,
|
|
4303
|
-
activity: activity,
|
|
4304
|
-
});
|
|
4305
|
-
},
|
|
4306
|
-
onDelta: function (delta) {
|
|
4307
|
-
sendToSession(session.localId, {
|
|
4308
|
-
type: "mention_stream",
|
|
4309
|
-
mateId: msg.mateId,
|
|
4310
|
-
mateName: mateName,
|
|
4311
|
-
delta: delta,
|
|
4312
|
-
});
|
|
4313
|
-
},
|
|
4314
|
-
onDone: function (fullText) {
|
|
4315
|
-
session._mentionInProgress = false;
|
|
4316
|
-
|
|
4317
|
-
// Save mention response to session history
|
|
4318
|
-
var mentionResponseEntry = {
|
|
4319
|
-
type: "mention_response",
|
|
4320
|
-
mateId: msg.mateId,
|
|
4321
|
-
mateName: mateName,
|
|
4322
|
-
text: fullText,
|
|
4323
|
-
avatarColor: avatarColor,
|
|
4324
|
-
avatarStyle: avatarStyle,
|
|
4325
|
-
avatarSeed: avatarSeed,
|
|
4326
|
-
};
|
|
4327
|
-
session.history.push(mentionResponseEntry);
|
|
4328
|
-
sm.appendToSessionFile(session, mentionResponseEntry);
|
|
4329
|
-
|
|
4330
|
-
// Queue mention context for injection into the current agent's next turn
|
|
4331
|
-
if (!session.pendingMentionContexts) session.pendingMentionContexts = [];
|
|
4332
|
-
session.pendingMentionContexts.push(
|
|
4333
|
-
"[Context: @" + mateName + " was mentioned and responded]\n\n" +
|
|
4334
|
-
"User asked @" + mateName + ": " + msg.text + "\n" +
|
|
4335
|
-
mateName + " responded: " + fullText + "\n\n" +
|
|
4336
|
-
"[End of @mention context. This is for your reference only. Do not re-execute or repeat this response.]"
|
|
4337
|
-
);
|
|
4338
|
-
|
|
4339
|
-
sendToSession(session.localId, { type: "mention_done", mateId: msg.mateId });
|
|
4340
|
-
|
|
4341
|
-
// Check if the mate wrote a debate brief during this turn
|
|
4342
|
-
checkForDmDebateBrief(session, msg.mateId, mateCtx);
|
|
4343
|
-
|
|
4344
|
-
// Generate session digest for mate's long-term memory
|
|
4345
|
-
digestMentionSession(session, msg.mateId, mateCtx, fullText, msg.text);
|
|
4346
|
-
},
|
|
4347
|
-
onError: function (errMsg) {
|
|
4348
|
-
session._mentionInProgress = false;
|
|
4349
|
-
// Clean up dead session
|
|
4350
|
-
if (session._mentionSessions && session._mentionSessions[msg.mateId]) {
|
|
4351
|
-
delete session._mentionSessions[msg.mateId];
|
|
4352
|
-
}
|
|
4353
|
-
console.error("[mention] Error for mate " + msg.mateId + ":", errMsg);
|
|
4354
|
-
sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: errMsg });
|
|
4355
|
-
},
|
|
4356
|
-
};
|
|
4357
|
-
|
|
4358
|
-
// Initialize mention sessions map if needed
|
|
4359
|
-
if (!session._mentionSessions) session._mentionSessions = {};
|
|
4360
|
-
|
|
4361
|
-
// Session continuity: check if this mate has a response in the recent window
|
|
4362
|
-
var existingSession = session._mentionSessions[msg.mateId];
|
|
4363
|
-
// Don't reuse a session that's still generating a digest (would mix digest output into mention stream)
|
|
4364
|
-
var canContinue = existingSession && existingSession.isAlive() && !existingSession._digesting && hasMateInWindow(recentTurns, msg.mateId);
|
|
4365
4086
|
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
// Load Mate CLAUDE.md
|
|
4379
|
-
var mateDir = matesModule.getMateDir(mateCtx, msg.mateId);
|
|
4380
|
-
var claudeMd = "";
|
|
4381
|
-
try {
|
|
4382
|
-
claudeMd = fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
|
|
4383
|
-
} catch (e) {
|
|
4384
|
-
// CLAUDE.md may not exist for new mates
|
|
4385
|
-
}
|
|
4386
|
-
|
|
4387
|
-
// Load session digests (unified: uses memory-summary.md if available)
|
|
4388
|
-
// Pass user's message as query for BM25 search of relevant past sessions
|
|
4389
|
-
var recentDigests = loadMateDigests(mateCtx, msg.mateId, mentionFullInput);
|
|
4390
|
-
|
|
4391
|
-
// Build initial mention context
|
|
4392
|
-
var mentionContext = buildMentionContext(userName, recentTurns) + recentDigests;
|
|
4393
|
-
|
|
4394
|
-
// Create new persistent mention session
|
|
4395
|
-
sdk.createMentionSession({
|
|
4396
|
-
claudeMd: claudeMd,
|
|
4397
|
-
initialContext: mentionContext,
|
|
4398
|
-
initialMessage: mentionFullInput,
|
|
4399
|
-
initialImages: msg.images || null,
|
|
4400
|
-
onActivity: mentionCallbacks.onActivity,
|
|
4401
|
-
onDelta: mentionCallbacks.onDelta,
|
|
4402
|
-
onDone: mentionCallbacks.onDone,
|
|
4403
|
-
onError: mentionCallbacks.onError,
|
|
4404
|
-
canUseTool: function (toolName, input, toolOpts) {
|
|
4405
|
-
var autoAllow = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
|
|
4406
|
-
if (autoAllow[toolName]) {
|
|
4407
|
-
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
4408
|
-
}
|
|
4409
|
-
// Route through the project session's permission system
|
|
4410
|
-
return new Promise(function (resolve) {
|
|
4411
|
-
var requestId = crypto.randomUUID();
|
|
4412
|
-
session.pendingPermissions[requestId] = {
|
|
4413
|
-
resolve: resolve,
|
|
4414
|
-
requestId: requestId,
|
|
4415
|
-
toolName: toolName,
|
|
4416
|
-
toolInput: input,
|
|
4417
|
-
toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
|
|
4418
|
-
decisionReason: (toolOpts && toolOpts.decisionReason) || "",
|
|
4419
|
-
mateId: msg.mateId,
|
|
4420
|
-
};
|
|
4421
|
-
sendToSession(session.localId, {
|
|
4422
|
-
type: "permission_request",
|
|
4423
|
-
requestId: requestId,
|
|
4424
|
-
toolName: toolName,
|
|
4425
|
-
toolInput: input,
|
|
4426
|
-
toolUseId: toolOpts ? toolOpts.toolUseID : undefined,
|
|
4427
|
-
decisionReason: (toolOpts && toolOpts.decisionReason) || "",
|
|
4428
|
-
mateId: msg.mateId,
|
|
4429
|
-
});
|
|
4430
|
-
onProcessingChanged();
|
|
4431
|
-
if (toolOpts && toolOpts.signal) {
|
|
4432
|
-
toolOpts.signal.addEventListener("abort", function () {
|
|
4433
|
-
delete session.pendingPermissions[requestId];
|
|
4434
|
-
sendToSession(session.localId, { type: "permission_cancel", requestId: requestId });
|
|
4435
|
-
onProcessingChanged();
|
|
4436
|
-
resolve({ behavior: "deny", message: "Request cancelled" });
|
|
4437
|
-
});
|
|
4438
|
-
}
|
|
4439
|
-
});
|
|
4440
|
-
},
|
|
4441
|
-
}).then(function (mentionSession) {
|
|
4442
|
-
if (mentionSession) {
|
|
4443
|
-
session._mentionSessions[msg.mateId] = mentionSession;
|
|
4444
|
-
}
|
|
4445
|
-
}).catch(function (err) {
|
|
4446
|
-
session._mentionInProgress = false;
|
|
4447
|
-
console.error("[mention] Failed to create session for mate " + msg.mateId + ":", err.message || err);
|
|
4448
|
-
sendToSession(session.localId, { type: "mention_error", mateId: msg.mateId, error: "Failed to create mention session." });
|
|
4449
|
-
});
|
|
4450
|
-
}
|
|
4451
|
-
}
|
|
4452
|
-
|
|
4453
|
-
// --- Shared mate helpers (used by debate module and other code) ---
|
|
4454
|
-
|
|
4455
|
-
function escapeRegex(str) {
|
|
4456
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4457
|
-
}
|
|
4458
|
-
|
|
4459
|
-
function getMateProfile(mateCtx, mateId) {
|
|
4460
|
-
var mate = matesModule.getMate(mateCtx, mateId);
|
|
4461
|
-
if (!mate) return { name: "Mate", avatarColor: "#6c5ce7", avatarStyle: "bottts", avatarSeed: mateId };
|
|
4462
|
-
return {
|
|
4463
|
-
name: (mate.profile && mate.profile.displayName) || mate.name || "Mate",
|
|
4464
|
-
avatarColor: (mate.profile && mate.profile.avatarColor) || "#6c5ce7",
|
|
4465
|
-
avatarStyle: (mate.profile && mate.profile.avatarStyle) || "bottts",
|
|
4466
|
-
avatarSeed: (mate.profile && mate.profile.avatarSeed) || mateId,
|
|
4467
|
-
};
|
|
4468
|
-
}
|
|
4469
|
-
|
|
4470
|
-
function loadMateClaudeMd(mateCtx, mateId) {
|
|
4471
|
-
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4472
|
-
try {
|
|
4473
|
-
return fs.readFileSync(path.join(mateDir, "CLAUDE.md"), "utf8");
|
|
4474
|
-
} catch (e) {
|
|
4475
|
-
return "";
|
|
4476
|
-
}
|
|
4477
|
-
}
|
|
4478
|
-
|
|
4479
|
-
function formatRawDigests(rawLines, headerLabel) {
|
|
4480
|
-
if (!rawLines || rawLines.length === 0) return "";
|
|
4481
|
-
var lines = ["\n\n" + (headerLabel || "Your recent session memories:")];
|
|
4482
|
-
for (var i = 0; i < rawLines.length; i++) {
|
|
4483
|
-
try {
|
|
4484
|
-
var d = JSON.parse(rawLines[i]);
|
|
4485
|
-
if (d.type === "debate" && d.my_role) {
|
|
4486
|
-
// Debate memories are role-played positions, not genuine opinions
|
|
4487
|
-
lines.push("- [" + (d.date || "?") + "] DEBATE (role: " + d.my_role + ") " + (d.topic || "unknown") +
|
|
4488
|
-
": argued " + (d.my_position || "N/A") + " (assigned role, not my actual opinion)" +
|
|
4489
|
-
(d.outcome ? " | Outcome: " + d.outcome : "") +
|
|
4490
|
-
(d.open_items ? " | Open: " + d.open_items : ""));
|
|
4087
|
+
function dispatchToSdk(finalText) {
|
|
4088
|
+
if (!session.isProcessing) {
|
|
4089
|
+
session.isProcessing = true;
|
|
4090
|
+
onProcessingChanged();
|
|
4091
|
+
session.sentToolResults = {};
|
|
4092
|
+
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
4093
|
+
if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
|
|
4094
|
+
// No active query (or worker idle between queries): start a new query
|
|
4095
|
+
session._queryStartTs = Date.now();
|
|
4096
|
+
console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
|
|
4097
|
+
sdk.startQuery(session, finalText, msg.images, getLinuxUserForSession(session));
|
|
4491
4098
|
} else {
|
|
4492
|
-
|
|
4493
|
-
(d.decisions ? " | Decisions: " + d.decisions : "") +
|
|
4494
|
-
(d.open_items ? " | Open: " + d.open_items : ""));
|
|
4495
|
-
}
|
|
4496
|
-
} catch (e) {}
|
|
4497
|
-
}
|
|
4498
|
-
return lines.join("\n");
|
|
4499
|
-
}
|
|
4500
|
-
|
|
4501
|
-
function loadMateDigests(mateCtx, mateId, query) {
|
|
4502
|
-
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4503
|
-
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
4504
|
-
var mate = matesModule.getMate(mateCtx, mateId);
|
|
4505
|
-
var hasGlobalSearch = mate && mate.globalSearch;
|
|
4506
|
-
|
|
4507
|
-
// Load shared user profile (available to ALL mates)
|
|
4508
|
-
var userProfileResult = "";
|
|
4509
|
-
try {
|
|
4510
|
-
var matesRoot = matesModule.resolveMatesRoot(mateCtx);
|
|
4511
|
-
var userProfilePath = path.join(matesRoot, "user-profile.md");
|
|
4512
|
-
if (fs.existsSync(userProfilePath)) {
|
|
4513
|
-
var profileContent = fs.readFileSync(userProfilePath, "utf8").trim();
|
|
4514
|
-
if (profileContent && profileContent.length > 50) {
|
|
4515
|
-
userProfileResult = "\n\n" + profileContent;
|
|
4099
|
+
sdk.pushMessage(session, finalText, msg.images);
|
|
4516
4100
|
}
|
|
4101
|
+
} else {
|
|
4102
|
+
sdk.pushMessage(session, finalText, msg.images);
|
|
4517
4103
|
}
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
// Check for memory-summary.md first
|
|
4521
|
-
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4522
|
-
var hasSummary = false;
|
|
4523
|
-
var summaryContent = "";
|
|
4524
|
-
try {
|
|
4525
|
-
if (fs.existsSync(summaryFile)) {
|
|
4526
|
-
summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
|
|
4527
|
-
if (summaryContent) hasSummary = true;
|
|
4528
|
-
}
|
|
4529
|
-
} catch (e) {}
|
|
4530
|
-
|
|
4531
|
-
// Load raw digests
|
|
4532
|
-
var allLines = [];
|
|
4533
|
-
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
4534
|
-
try {
|
|
4535
|
-
if (fs.existsSync(digestFile)) {
|
|
4536
|
-
allLines = fs.readFileSync(digestFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
|
|
4537
|
-
}
|
|
4538
|
-
} catch (e) {}
|
|
4539
|
-
|
|
4540
|
-
var result = userProfileResult;
|
|
4541
|
-
|
|
4542
|
-
if (hasSummary) {
|
|
4543
|
-
// Load summary + latest 5 raw digests for richer context
|
|
4544
|
-
var recent = allLines.slice(-5);
|
|
4545
|
-
result = "\n\nYour memory summary:\n" + summaryContent;
|
|
4546
|
-
if (recent.length > 0) {
|
|
4547
|
-
result += formatRawDigests(recent, "Latest raw session memories:");
|
|
4548
|
-
}
|
|
4549
|
-
} else {
|
|
4550
|
-
// Backward compatible: latest 8 raw digests
|
|
4551
|
-
var recent = allLines.slice(-8);
|
|
4552
|
-
result = formatRawDigests(recent, "Your recent session memories:");
|
|
4104
|
+
sm.broadcastSessionList();
|
|
4553
4105
|
}
|
|
4554
4106
|
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
var
|
|
4568
|
-
|
|
4569
|
-
|
|
4107
|
+
if (tabSources.length > 0) {
|
|
4108
|
+
// Request tab context from all active browser tab sources
|
|
4109
|
+
var tabPromises = tabSources.map(function(srcId) {
|
|
4110
|
+
var tabId = parseInt(srcId.split(":")[1], 10);
|
|
4111
|
+
return requestTabContext(ws, tabId);
|
|
4112
|
+
});
|
|
4113
|
+
Promise.all(tabPromises).then(function(results) {
|
|
4114
|
+
var tabContextParts = [];
|
|
4115
|
+
var screenshotImages = [];
|
|
4116
|
+
|
|
4117
|
+
for (var ti = 0; ti < results.length; ti++) {
|
|
4118
|
+
if (!results[ti]) continue;
|
|
4119
|
+
var tabId2 = parseInt(tabSources[ti].split(":")[1], 10);
|
|
4120
|
+
var tabInfo = _browserTabList[tabId2];
|
|
4121
|
+
var tabLabel = tabInfo ? (tabInfo.title || tabInfo.url || "Tab " + tabId2) : "Tab " + tabId2;
|
|
4122
|
+
var r = results[ti];
|
|
4123
|
+
var parts = [];
|
|
4124
|
+
|
|
4125
|
+
// Console logs
|
|
4126
|
+
if (r.console && r.console.logs) {
|
|
4127
|
+
try {
|
|
4128
|
+
var logs = typeof r.console.logs === "string" ? JSON.parse(r.console.logs) : r.console.logs;
|
|
4129
|
+
if (logs && logs.length > 0) {
|
|
4130
|
+
var logLines = [];
|
|
4131
|
+
var logSlice = logs.slice(-50);
|
|
4132
|
+
for (var li = 0; li < logSlice.length; li++) {
|
|
4133
|
+
var entry = logSlice[li];
|
|
4134
|
+
var ts = entry.ts ? new Date(entry.ts).toTimeString().slice(0, 8) : "";
|
|
4135
|
+
var lvl = (entry.level || "log").toUpperCase();
|
|
4136
|
+
logLines.push("[" + ts + " " + lvl + "] " + (entry.text || ""));
|
|
4137
|
+
}
|
|
4138
|
+
parts.push("Console:\n" + logLines.join("\n"));
|
|
4139
|
+
}
|
|
4140
|
+
} catch (e) {
|
|
4141
|
+
// ignore parse errors
|
|
4142
|
+
}
|
|
4570
4143
|
}
|
|
4571
4144
|
|
|
4572
|
-
//
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4145
|
+
// Network requests
|
|
4146
|
+
if (r.network && r.network.network) {
|
|
4147
|
+
try {
|
|
4148
|
+
var netLog = typeof r.network.network === "string" ? JSON.parse(r.network.network) : r.network.network;
|
|
4149
|
+
if (netLog && netLog.length > 0) {
|
|
4150
|
+
var netLines = [];
|
|
4151
|
+
var netSlice = netLog.slice(-30);
|
|
4152
|
+
for (var ni = 0; ni < netSlice.length; ni++) {
|
|
4153
|
+
var req = netSlice[ni];
|
|
4154
|
+
var line = (req.method || "GET") + " " + (req.url || "") + " " + (req.status || 0) + " " + (req.duration || 0) + "ms";
|
|
4155
|
+
if (req.error) line += " [" + req.error + "]";
|
|
4156
|
+
netLines.push(line);
|
|
4157
|
+
}
|
|
4158
|
+
parts.push("Network (last " + netSlice.length + " requests):\n" + netLines.join("\n"));
|
|
4579
4159
|
}
|
|
4160
|
+
} catch (e) {
|
|
4161
|
+
// ignore parse errors
|
|
4580
4162
|
}
|
|
4581
|
-
} catch (e) {}
|
|
4582
|
-
}
|
|
4583
|
-
|
|
4584
|
-
// Inject team memory summaries into context
|
|
4585
|
-
if (teamSummaries.length > 0) {
|
|
4586
|
-
result += "\n\nTeam memory summaries (other mates' accumulated context):";
|
|
4587
|
-
for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
|
|
4588
|
-
var ts = teamSummaries[tsi];
|
|
4589
|
-
// Cap each summary to avoid context overflow
|
|
4590
|
-
var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
|
|
4591
|
-
result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
|
|
4592
4163
|
}
|
|
4593
|
-
}
|
|
4594
|
-
} catch (e) {}
|
|
4595
4164
|
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
|
|
4603
|
-
try {
|
|
4604
|
-
if (fs.existsSync(moFile)) {
|
|
4605
|
-
var moLines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
|
|
4606
|
-
for (var mli = 0; mli < moLines.length; mli++) {
|
|
4607
|
-
try {
|
|
4608
|
-
var moEntry = JSON.parse(moLines[mli]);
|
|
4609
|
-
moEntry._mateName = moEntry.mateName || allMatesForObs[moi].name || allMatesForObs[moi].id;
|
|
4610
|
-
allObservations.push(moEntry);
|
|
4611
|
-
} catch (e) {}
|
|
4165
|
+
// Page text (from tab_page_text command)
|
|
4166
|
+
if (r.pageText && (r.pageText.text || r.pageText.value)) {
|
|
4167
|
+
var pageContent = r.pageText.text || r.pageText.value;
|
|
4168
|
+
if (pageContent.length > 0) {
|
|
4169
|
+
if (pageContent.length > 32768) {
|
|
4170
|
+
pageContent = pageContent.substring(0, 32768) + "\n... (truncated)";
|
|
4612
4171
|
}
|
|
4172
|
+
parts.push("Page text:\n" + pageContent);
|
|
4613
4173
|
}
|
|
4614
|
-
} catch (e) {}
|
|
4615
|
-
}
|
|
4616
|
-
if (allObservations.length > 0) {
|
|
4617
|
-
// Sort by date descending
|
|
4618
|
-
allObservations.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
|
|
4619
|
-
var recentObs = allObservations.slice(0, 15);
|
|
4620
|
-
result += "\n\nRecent user observations from all mates:";
|
|
4621
|
-
for (var roi = 0; roi < recentObs.length; roi++) {
|
|
4622
|
-
var ro = recentObs[roi];
|
|
4623
|
-
result += "\n- [" + (ro.date || "?") + "] [@" + ro._mateName + "] [" + (ro.category || "?") + "] " + (ro.observation || "") + (ro.evidence ? " (evidence: " + ro.evidence + ")" : "");
|
|
4624
4174
|
}
|
|
4625
|
-
}
|
|
4626
|
-
} catch (e) {}
|
|
4627
|
-
|
|
4628
|
-
// Inject recent activity timeline across all projects (chronological)
|
|
4629
|
-
try {
|
|
4630
|
-
var timelineEntries = [];
|
|
4631
|
-
|
|
4632
|
-
// Own sessions
|
|
4633
|
-
sm.sessions.forEach(function (s) {
|
|
4634
|
-
if (s.hidden || !s.history || s.history.length === 0) return;
|
|
4635
|
-
timelineEntries.push({
|
|
4636
|
-
title: s.title || "New Session",
|
|
4637
|
-
project: null,
|
|
4638
|
-
ts: s.lastActivity || s.createdAt || 0
|
|
4639
|
-
});
|
|
4640
|
-
});
|
|
4641
4175
|
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
var line = "- [" + dateStr + "] " + te.title;
|
|
4663
|
-
if (te.project) line += " (project: " + te.project + ")";
|
|
4664
|
-
result += "\n" + line;
|
|
4176
|
+
// Screenshot — save to disk and add to images for SDK
|
|
4177
|
+
if (r.screenshot && r.screenshot.image) {
|
|
4178
|
+
try {
|
|
4179
|
+
var screenshotData = r.screenshot.image;
|
|
4180
|
+
var screenshotName = saveImageFile("image/png", screenshotData, getLinuxUserForSession(session));
|
|
4181
|
+
if (screenshotName) {
|
|
4182
|
+
var screenshotPath = path.join(imagesDir, screenshotName);
|
|
4183
|
+
// Add to images array for SDK multimodal
|
|
4184
|
+
screenshotImages.push({
|
|
4185
|
+
mediaType: "image/png",
|
|
4186
|
+
data: screenshotData,
|
|
4187
|
+
file: screenshotName,
|
|
4188
|
+
tabTitle: tabLabel,
|
|
4189
|
+
tabUrl: tabInfo ? tabInfo.url : ""
|
|
4190
|
+
});
|
|
4191
|
+
parts.push("[Screenshot saved: " + screenshotPath + "]");
|
|
4192
|
+
}
|
|
4193
|
+
} catch (e) {
|
|
4194
|
+
// ignore screenshot save errors
|
|
4195
|
+
}
|
|
4665
4196
|
}
|
|
4666
|
-
}
|
|
4667
|
-
} catch (e) {}
|
|
4668
|
-
}
|
|
4669
4197
|
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
if (query && (hasGlobalSearch || allLines.length > 5)) {
|
|
4673
|
-
try {
|
|
4674
|
-
// Collect mate's own sessions
|
|
4675
|
-
var mateSessions = [];
|
|
4676
|
-
sm.sessions.forEach(function (s) {
|
|
4677
|
-
if (!s.hidden && s.history && s.history.length > 0) {
|
|
4678
|
-
mateSessions.push(s);
|
|
4198
|
+
if (r.console && r.console.error) {
|
|
4199
|
+
parts.push("(Console error: " + r.console.error + ")");
|
|
4679
4200
|
}
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
// globalSearch: also collect sessions from all other projects + knowledge files
|
|
4683
|
-
var knowledgeFiles = [];
|
|
4684
|
-
if (hasGlobalSearch) {
|
|
4685
|
-
var crossSessions = getAllProjectSessions();
|
|
4686
|
-
for (var cs = 0; cs < crossSessions.length; cs++) {
|
|
4687
|
-
mateSessions.push(crossSessions[cs]);
|
|
4201
|
+
if (r.network && r.network.error) {
|
|
4202
|
+
parts.push("(Network error: " + r.network.error + ")");
|
|
4688
4203
|
}
|
|
4689
4204
|
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
for (var mk = 0; mk < allMatesForKnowledge.length; mk++) {
|
|
4694
|
-
var mkDir = matesModule.getMateDir(mateCtx, allMatesForKnowledge[mk].id);
|
|
4695
|
-
var mkName = allMatesForKnowledge[mk].name || allMatesForKnowledge[mk].id;
|
|
4696
|
-
var mkKnowledgeDir = path.join(mkDir, "knowledge");
|
|
4697
|
-
try {
|
|
4698
|
-
var kFiles = fs.readdirSync(mkKnowledgeDir);
|
|
4699
|
-
for (var kfi = 0; kfi < kFiles.length; kfi++) {
|
|
4700
|
-
var kfName = kFiles[kfi];
|
|
4701
|
-
// Skip system files (digests, identity, base-template)
|
|
4702
|
-
if (kfName === "session-digests.jsonl" || kfName === "memory-summary.md" ||
|
|
4703
|
-
kfName === "identity-backup.md" || kfName === "identity-history.jsonl" ||
|
|
4704
|
-
kfName === "base-template.md") continue;
|
|
4705
|
-
knowledgeFiles.push({
|
|
4706
|
-
filePath: path.join(mkKnowledgeDir, kfName),
|
|
4707
|
-
name: kfName,
|
|
4708
|
-
mateName: mkName
|
|
4709
|
-
});
|
|
4710
|
-
}
|
|
4711
|
-
} catch (e) {}
|
|
4712
|
-
}
|
|
4713
|
-
} catch (e) {}
|
|
4205
|
+
if (parts.length > 0) {
|
|
4206
|
+
tabContextParts.push("[Browser tab: " + tabLabel + "]\n" + parts.join("\n\n"));
|
|
4207
|
+
}
|
|
4714
4208
|
}
|
|
4715
4209
|
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
sessions: mateSessions,
|
|
4720
|
-
knowledgeFiles: knowledgeFiles,
|
|
4721
|
-
query: query,
|
|
4722
|
-
maxResults: hasGlobalSearch ? 12 : 5,
|
|
4723
|
-
minScore: 1.0
|
|
4724
|
-
});
|
|
4725
|
-
var contextStr = sessionSearch.formatForContext(searchResults);
|
|
4726
|
-
if (contextStr) result += contextStr;
|
|
4727
|
-
} catch (e) {
|
|
4728
|
-
console.error("[session-search] Mate search failed:", e.message);
|
|
4729
|
-
}
|
|
4730
|
-
}
|
|
4731
|
-
|
|
4732
|
-
return result;
|
|
4733
|
-
}
|
|
4734
|
-
|
|
4735
|
-
// Gate check: ask Haiku whether this conversation contains anything worth remembering
|
|
4736
|
-
function gateMemory(mateCtx, mateId, conversationContent, callback, opts) {
|
|
4737
|
-
opts = opts || {};
|
|
4738
|
-
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4739
|
-
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
4740
|
-
|
|
4741
|
-
// Load mate role/activities from mate.yaml (lightweight, no full CLAUDE.md)
|
|
4742
|
-
var mateRole = "";
|
|
4743
|
-
var mateActivities = "";
|
|
4744
|
-
try {
|
|
4745
|
-
var yamlRaw = fs.readFileSync(path.join(mateDir, "mate.yaml"), "utf8");
|
|
4746
|
-
var roleMatch = yamlRaw.match(/^relationship:\s*(.+)$/m);
|
|
4747
|
-
var actMatch = yamlRaw.match(/^activities:\s*(.+)$/m);
|
|
4748
|
-
if (roleMatch) mateRole = roleMatch[1].trim();
|
|
4749
|
-
if (actMatch) mateActivities = actMatch[1].trim();
|
|
4750
|
-
} catch (e) {}
|
|
4751
|
-
|
|
4752
|
-
// Load existing memory summary if available
|
|
4753
|
-
var summaryContent = "";
|
|
4754
|
-
try {
|
|
4755
|
-
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4756
|
-
if (fs.existsSync(summaryFile)) {
|
|
4757
|
-
summaryContent = fs.readFileSync(summaryFile, "utf8").trim();
|
|
4758
|
-
}
|
|
4759
|
-
} catch (e) {}
|
|
4760
|
-
|
|
4761
|
-
// Cap conversation content for gate
|
|
4762
|
-
var cappedContent = conversationContent;
|
|
4763
|
-
if (cappedContent.length > 3000) {
|
|
4764
|
-
cappedContent = cappedContent.substring(0, 3000) + "...";
|
|
4765
|
-
}
|
|
4766
|
-
|
|
4767
|
-
var gateContext = [
|
|
4768
|
-
"[SYSTEM: Memory Gate]",
|
|
4769
|
-
"You are a memory filter for an AI Mate.",
|
|
4770
|
-
"",
|
|
4771
|
-
"Mate role: " + (mateRole || "assistant"),
|
|
4772
|
-
"Mate activities: " + (mateActivities || "general"),
|
|
4773
|
-
"",
|
|
4774
|
-
"Current memory summary:",
|
|
4775
|
-
summaryContent || "No memory summary yet.",
|
|
4776
|
-
"",
|
|
4777
|
-
"Conversation just ended:",
|
|
4778
|
-
cappedContent,
|
|
4779
|
-
].join("\n");
|
|
4780
|
-
|
|
4781
|
-
var gatePrompt = opts.gatePrompt || [
|
|
4782
|
-
'Should this conversation be saved to long-term memory?',
|
|
4783
|
-
'Answer "yes" if ANY of these apply:',
|
|
4784
|
-
"- A new decision, commitment, or direction",
|
|
4785
|
-
"- A change in position or strategy",
|
|
4786
|
-
"- New information relevant to this Mate's role",
|
|
4787
|
-
"- A user preference, opinion, or pattern not already in the summary",
|
|
4788
|
-
"- The user shared personal context, project details, or goals",
|
|
4789
|
-
"- The user expressed what they like, dislike, or care about",
|
|
4790
|
-
"- The user gave instructions on how they want things done",
|
|
4791
|
-
"- Anything the user would reasonably expect to be remembered next time",
|
|
4792
|
-
"",
|
|
4793
|
-
'Answer "no" ONLY if:',
|
|
4794
|
-
"- It exactly duplicates what is already in the memory summary",
|
|
4795
|
-
"- The entire conversation is a single trivial exchange (e.g. just 'hi' / 'hello')",
|
|
4796
|
-
"",
|
|
4797
|
-
"When in doubt, answer yes. It is better to remember too much than to forget something important.",
|
|
4798
|
-
"",
|
|
4799
|
-
'Answer with ONLY "yes" or "no". Nothing else.',
|
|
4800
|
-
].join("\n");
|
|
4801
|
-
var defaultOnError = opts.defaultYes !== undefined ? !!opts.defaultYes : true;
|
|
4802
|
-
|
|
4803
|
-
var gateText = "";
|
|
4804
|
-
var _gateSession = null;
|
|
4805
|
-
sdk.createMentionSession({
|
|
4806
|
-
claudeMd: "",
|
|
4807
|
-
model: "haiku",
|
|
4808
|
-
initialContext: gateContext,
|
|
4809
|
-
initialMessage: gatePrompt,
|
|
4810
|
-
onActivity: function () {},
|
|
4811
|
-
onDelta: function (delta) {
|
|
4812
|
-
gateText += delta;
|
|
4813
|
-
},
|
|
4814
|
-
onDone: function () {
|
|
4815
|
-
var answer = gateText.trim().toLowerCase();
|
|
4816
|
-
var shouldRemember = answer.indexOf("yes") !== -1;
|
|
4817
|
-
if (_gateSession) try { _gateSession.close(); } catch (e) {}
|
|
4818
|
-
callback(shouldRemember);
|
|
4819
|
-
},
|
|
4820
|
-
onError: function (err) {
|
|
4821
|
-
console.error("[memory-gate] Gate check failed for mate " + mateId + ":", err);
|
|
4822
|
-
if (_gateSession) try { _gateSession.close(); } catch (e) {}
|
|
4823
|
-
callback(defaultOnError);
|
|
4824
|
-
},
|
|
4825
|
-
}).then(function (gs) {
|
|
4826
|
-
_gateSession = gs;
|
|
4827
|
-
if (!gs) callback(defaultOnError);
|
|
4828
|
-
}).catch(function (err) {
|
|
4829
|
-
console.error("[memory-gate] Failed to create gate session for mate " + mateId + ":", err);
|
|
4830
|
-
callback(defaultOnError);
|
|
4831
|
-
});
|
|
4832
|
-
}
|
|
4833
|
-
|
|
4834
|
-
// Update (or create) memory-summary.md based on a new digest
|
|
4835
|
-
function updateMemorySummary(mateCtx, mateId, digestObj) {
|
|
4836
|
-
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
4837
|
-
var knowledgeDir = path.join(mateDir, "knowledge");
|
|
4838
|
-
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4210
|
+
if (tabContextParts.length > 0) {
|
|
4211
|
+
fullText = tabContextParts.join("\n\n---\n\n") + "\n\n" + fullText;
|
|
4212
|
+
}
|
|
4839
4213
|
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4214
|
+
// If screenshots were captured, send context preview cards and add to SDK images
|
|
4215
|
+
if (screenshotImages.length > 0) {
|
|
4216
|
+
if (!msg.images) msg.images = [];
|
|
4217
|
+
for (var si = 0; si < screenshotImages.length; si++) {
|
|
4218
|
+
var ss = screenshotImages[si];
|
|
4219
|
+
// Save context_preview to history so it restores on session load
|
|
4220
|
+
var previewEntry = {
|
|
4221
|
+
type: "context_preview",
|
|
4222
|
+
tab: {
|
|
4223
|
+
title: ss.tabTitle || "",
|
|
4224
|
+
url: ss.tabUrl || "",
|
|
4225
|
+
screenshotFile: ss.file
|
|
4226
|
+
}
|
|
4227
|
+
};
|
|
4228
|
+
session.history.push(previewEntry);
|
|
4229
|
+
// Send context card to all clients
|
|
4230
|
+
sendToSession(session.localId, {
|
|
4231
|
+
type: "context_preview",
|
|
4232
|
+
tab: {
|
|
4233
|
+
title: ss.tabTitle || "",
|
|
4234
|
+
url: ss.tabUrl || "",
|
|
4235
|
+
screenshotUrl: "/p/" + slug + "/images/" + ss.file
|
|
4236
|
+
}
|
|
4237
|
+
});
|
|
4238
|
+
// Add to SDK images for multimodal
|
|
4239
|
+
msg.images.push({ mediaType: ss.mediaType, data: ss.data });
|
|
4240
|
+
}
|
|
4241
|
+
sm.saveSessionFile(session);
|
|
4242
|
+
}
|
|
4849
4243
|
|
|
4850
|
-
|
|
4851
|
-
// Try initial summary generation from existing digests (migration)
|
|
4852
|
-
initMemorySummary(mateCtx, mateId, function () {
|
|
4853
|
-
// After init, do incremental update with the new digest
|
|
4854
|
-
doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj);
|
|
4244
|
+
dispatchToSdk(fullText);
|
|
4855
4245
|
});
|
|
4856
4246
|
} else {
|
|
4857
|
-
|
|
4247
|
+
dispatchToSdk(fullText);
|
|
4858
4248
|
}
|
|
4859
4249
|
}
|
|
4860
4250
|
|
|
4861
|
-
//
|
|
4862
|
-
function doIncrementalUpdate(mateCtx, mateId, knowledgeDir, summaryFile, digestObj) {
|
|
4863
|
-
var existingSummary = "";
|
|
4864
|
-
try {
|
|
4865
|
-
if (fs.existsSync(summaryFile)) {
|
|
4866
|
-
existingSummary = fs.readFileSync(summaryFile, "utf8").trim();
|
|
4867
|
-
}
|
|
4868
|
-
} catch (e) {}
|
|
4869
|
-
|
|
4870
|
-
var updateContext = [
|
|
4871
|
-
"[SYSTEM: Memory Summary Update]",
|
|
4872
|
-
"You are updating an AI Mate's long-term memory summary.",
|
|
4873
|
-
"",
|
|
4874
|
-
"Current summary:",
|
|
4875
|
-
existingSummary || "(empty, this is the first entry)",
|
|
4876
|
-
"",
|
|
4877
|
-
"New session digest to incorporate:",
|
|
4878
|
-
JSON.stringify(digestObj, null, 2),
|
|
4879
|
-
].join("\n");
|
|
4880
|
-
|
|
4881
|
-
var updatePrompt = [
|
|
4882
|
-
"Update the summary by:",
|
|
4883
|
-
"1. Adding new information from this session",
|
|
4884
|
-
"2. Updating existing entries if positions changed",
|
|
4885
|
-
"3. Moving resolved open threads out of \"Open Threads\"",
|
|
4886
|
-
"4. Adding to \"My Track Record\" if a past prediction/recommendation can now be evaluated",
|
|
4887
|
-
"5. Removing outdated or redundant information",
|
|
4888
|
-
"6. Preserving important user quotes and context from key_quotes and user_context fields",
|
|
4889
|
-
"",
|
|
4890
|
-
"Maintain this structure:",
|
|
4891
|
-
"",
|
|
4892
|
-
"# Memory Summary",
|
|
4893
|
-
"Last updated: YYYY-MM-DD (session count: N+1)",
|
|
4894
|
-
"",
|
|
4895
|
-
"## User Context",
|
|
4896
|
-
"(who they are, what they work on, project details, goals)",
|
|
4897
|
-
"## User Patterns",
|
|
4898
|
-
"(preferences, work style, communication style, likes/dislikes)",
|
|
4899
|
-
"## Key Decisions",
|
|
4900
|
-
"## Notable Quotes",
|
|
4901
|
-
"(important things the user said, verbatim when possible)",
|
|
4902
|
-
"## My Track Record",
|
|
4903
|
-
"## Open Threads",
|
|
4904
|
-
"## Recurring Topics",
|
|
4905
|
-
"",
|
|
4906
|
-
"Keep it concise. Each section should have at most 10 bullet points.",
|
|
4907
|
-
"Drop the oldest/least relevant if needed.",
|
|
4908
|
-
"The Notable Quotes section is valuable for preserving the user's voice and intent.",
|
|
4909
|
-
"Output ONLY the updated markdown. Nothing else.",
|
|
4910
|
-
].join("\n");
|
|
4911
|
-
|
|
4912
|
-
var updateText = "";
|
|
4913
|
-
var _updateSession = null;
|
|
4914
|
-
sdk.createMentionSession({
|
|
4915
|
-
claudeMd: "",
|
|
4916
|
-
model: "haiku",
|
|
4917
|
-
initialContext: updateContext,
|
|
4918
|
-
initialMessage: updatePrompt,
|
|
4919
|
-
onActivity: function () {},
|
|
4920
|
-
onDelta: function (delta) {
|
|
4921
|
-
updateText += delta;
|
|
4922
|
-
},
|
|
4923
|
-
onDone: function () {
|
|
4924
|
-
try {
|
|
4925
|
-
var cleaned = updateText.trim();
|
|
4926
|
-
if (cleaned.indexOf("```") === 0) {
|
|
4927
|
-
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
4928
|
-
}
|
|
4929
|
-
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
4930
|
-
fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
|
|
4931
|
-
console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
|
|
4932
|
-
} catch (e) {
|
|
4933
|
-
console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
|
|
4934
|
-
}
|
|
4935
|
-
if (_updateSession) try { _updateSession.close(); } catch (e) {}
|
|
4936
|
-
},
|
|
4937
|
-
onError: function (err) {
|
|
4938
|
-
console.error("[memory-summary] Summary update failed for mate " + mateId + ":", err);
|
|
4939
|
-
if (_updateSession) try { _updateSession.close(); } catch (e) {}
|
|
4940
|
-
},
|
|
4941
|
-
}).then(function (us) {
|
|
4942
|
-
_updateSession = us;
|
|
4943
|
-
}).catch(function (err) {
|
|
4944
|
-
console.error("[memory-summary] Failed to create summary update session for mate " + mateId + ":", err);
|
|
4945
|
-
});
|
|
4946
|
-
}
|
|
4947
|
-
|
|
4948
|
-
// User profile synthesis: collect observations from all mates, synthesize unified profile
|
|
4949
|
-
var USER_PROFILE_SYNTHESIS_THRESHOLD = 8;
|
|
4950
|
-
|
|
4951
|
-
function maybeSynthesizeUserProfile(mateCtx, mateId) {
|
|
4952
|
-
var mate = matesModule.getMate(mateCtx, mateId);
|
|
4953
|
-
if (!mate || !mate.globalSearch) return; // Only primary/globalSearch mates synthesize
|
|
4954
|
-
|
|
4955
|
-
var matesRoot = matesModule.resolveMatesRoot(mateCtx);
|
|
4956
|
-
var profilePath = path.join(matesRoot, "user-profile.md");
|
|
4957
|
-
|
|
4958
|
-
// Collect all observations across all mates
|
|
4959
|
-
var allObs = [];
|
|
4960
|
-
try {
|
|
4961
|
-
var allMates = matesModule.getAllMates(mateCtx);
|
|
4962
|
-
for (var mi = 0; mi < allMates.length; mi++) {
|
|
4963
|
-
var moDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
|
|
4964
|
-
var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
|
|
4965
|
-
try {
|
|
4966
|
-
if (fs.existsSync(moFile)) {
|
|
4967
|
-
var lines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
|
|
4968
|
-
for (var li = 0; li < lines.length; li++) {
|
|
4969
|
-
try { allObs.push(JSON.parse(lines[li])); } catch (e) {}
|
|
4970
|
-
}
|
|
4971
|
-
}
|
|
4972
|
-
} catch (e) {}
|
|
4973
|
-
}
|
|
4974
|
-
} catch (e) { return; }
|
|
4975
|
-
|
|
4976
|
-
if (allObs.length === 0) return;
|
|
4251
|
+
// --- Shared helpers ---
|
|
4977
4252
|
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
var lastObsCount = 0;
|
|
4981
|
-
try {
|
|
4982
|
-
if (fs.existsSync(profilePath)) {
|
|
4983
|
-
existingProfile = fs.readFileSync(profilePath, "utf8").trim();
|
|
4984
|
-
var countMatch = existingProfile.match(/from (\d+) observations/);
|
|
4985
|
-
if (countMatch) lastObsCount = parseInt(countMatch[1], 10) || 0;
|
|
4986
|
-
}
|
|
4987
|
-
} catch (e) {}
|
|
4988
|
-
|
|
4989
|
-
if (allObs.length - lastObsCount < USER_PROFILE_SYNTHESIS_THRESHOLD) return;
|
|
4990
|
-
|
|
4991
|
-
// Sort newest first for synthesis
|
|
4992
|
-
allObs.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
|
|
4993
|
-
|
|
4994
|
-
var synthContext = [
|
|
4995
|
-
"[SYSTEM: User Profile Synthesis]",
|
|
4996
|
-
"You are synthesizing a user profile from observations collected by multiple AI teammates.",
|
|
4997
|
-
"",
|
|
4998
|
-
"Current profile:",
|
|
4999
|
-
existingProfile || "(none yet, first synthesis)",
|
|
5000
|
-
"",
|
|
5001
|
-
"All observations (" + allObs.length + " total, newest first):",
|
|
5002
|
-
allObs.map(function (o) {
|
|
5003
|
-
return "[" + (o.date || "?") + "] [@" + (o.mateName || o.mateId || "?") + "] [" + (o.category || "?") + "] " + (o.observation || "") + (o.evidence ? " (evidence: " + o.evidence + ")" : "");
|
|
5004
|
-
}).join("\n"),
|
|
5005
|
-
].join("\n");
|
|
5006
|
-
|
|
5007
|
-
var synthPrompt = [
|
|
5008
|
-
"Synthesize a unified user profile from these observations.",
|
|
5009
|
-
"",
|
|
5010
|
-
"Rules:",
|
|
5011
|
-
"1. Organize by: Communication Style, Decision Patterns, Working Habits, Technical Preferences, Emotional Signals",
|
|
5012
|
-
"2. Each point: observation + source mates and dates in parentheses",
|
|
5013
|
-
"3. If observations contradict, note both with dates. Preferences evolve.",
|
|
5014
|
-
"4. Mark patterns seen 3+ times as [strong], 2 times as [emerging]",
|
|
5015
|
-
"5. Keep under 800 words. This is a reference card, not a biography.",
|
|
5016
|
-
'6. End with: "Last synthesized: YYYY-MM-DD from N observations across M mates"',
|
|
5017
|
-
"",
|
|
5018
|
-
"Output ONLY the markdown profile. No fences, no extra text.",
|
|
5019
|
-
].join("\n");
|
|
5020
|
-
|
|
5021
|
-
var synthText = "";
|
|
5022
|
-
sdk.createMentionSession({
|
|
5023
|
-
claudeMd: "",
|
|
5024
|
-
model: "haiku",
|
|
5025
|
-
initialContext: synthContext,
|
|
5026
|
-
initialMessage: synthPrompt,
|
|
5027
|
-
onActivity: function () {},
|
|
5028
|
-
onDelta: function (delta) { synthText += delta; },
|
|
5029
|
-
onDone: function () {
|
|
5030
|
-
try {
|
|
5031
|
-
var cleaned = synthText.trim();
|
|
5032
|
-
if (cleaned.indexOf("```") === 0) {
|
|
5033
|
-
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
5034
|
-
}
|
|
5035
|
-
fs.mkdirSync(path.dirname(profilePath), { recursive: true });
|
|
5036
|
-
fs.writeFileSync(profilePath, cleaned + "\n", "utf8");
|
|
5037
|
-
console.log("[user-profile] Synthesized user-profile.md from " + allObs.length + " observations");
|
|
5038
|
-
} catch (e) {
|
|
5039
|
-
console.error("[user-profile] Failed to write user-profile.md:", e.message);
|
|
5040
|
-
}
|
|
5041
|
-
},
|
|
5042
|
-
onError: function (err) {
|
|
5043
|
-
console.error("[user-profile] Synthesis failed:", err);
|
|
5044
|
-
},
|
|
5045
|
-
}).catch(function (err) {
|
|
5046
|
-
console.error("[user-profile] Failed to create synthesis session:", err);
|
|
5047
|
-
});
|
|
4253
|
+
function escapeRegex(str) {
|
|
4254
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5048
4255
|
}
|
|
5049
4256
|
|
|
5050
|
-
//
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
"Structure:",
|
|
5097
|
-
"",
|
|
5098
|
-
"# Memory Summary",
|
|
5099
|
-
"Last updated: YYYY-MM-DD (session count: N)",
|
|
5100
|
-
"",
|
|
5101
|
-
"## User Context",
|
|
5102
|
-
"(who they are, what they work on, project details, goals)",
|
|
5103
|
-
"## User Patterns",
|
|
5104
|
-
"(preferences, work style, communication style, likes/dislikes)",
|
|
5105
|
-
"## Key Decisions",
|
|
5106
|
-
"## Notable Quotes",
|
|
5107
|
-
"(important things the user said, verbatim when possible)",
|
|
5108
|
-
"## My Track Record",
|
|
5109
|
-
"## Open Threads",
|
|
5110
|
-
"## Recurring Topics",
|
|
5111
|
-
"",
|
|
5112
|
-
"Keep it concise. Focus on patterns, decisions, and the user's own words.",
|
|
5113
|
-
"Each section should have at most 10 bullet points.",
|
|
5114
|
-
"Preserve key_quotes from digests in the Notable Quotes section.",
|
|
5115
|
-
"Set session count to " + digestsText.length + ".",
|
|
5116
|
-
"Output ONLY the markdown. Nothing else.",
|
|
5117
|
-
].join("\n");
|
|
5118
|
-
|
|
5119
|
-
var initText = "";
|
|
5120
|
-
var _initSession = null;
|
|
5121
|
-
sdk.createMentionSession({
|
|
5122
|
-
claudeMd: "",
|
|
5123
|
-
model: "haiku",
|
|
5124
|
-
initialContext: initContext,
|
|
5125
|
-
initialMessage: initPrompt,
|
|
5126
|
-
onActivity: function () {},
|
|
5127
|
-
onDelta: function (delta) {
|
|
5128
|
-
initText += delta;
|
|
5129
|
-
},
|
|
5130
|
-
onDone: function () {
|
|
5131
|
-
try {
|
|
5132
|
-
var cleaned = initText.trim();
|
|
5133
|
-
if (cleaned.indexOf("```") === 0) {
|
|
5134
|
-
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
5135
|
-
}
|
|
5136
|
-
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
5137
|
-
fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
|
|
5138
|
-
console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
|
|
5139
|
-
} catch (e) {
|
|
5140
|
-
console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
|
|
5141
|
-
}
|
|
5142
|
-
if (_initSession) try { _initSession.close(); } catch (e) {}
|
|
5143
|
-
callback();
|
|
5144
|
-
},
|
|
5145
|
-
onError: function (err) {
|
|
5146
|
-
console.error("[memory-summary] Initial summary generation failed for mate " + mateId + ":", err);
|
|
5147
|
-
if (_initSession) try { _initSession.close(); } catch (e) {}
|
|
5148
|
-
callback();
|
|
5149
|
-
},
|
|
5150
|
-
}).then(function (is) {
|
|
5151
|
-
_initSession = is;
|
|
5152
|
-
if (!is) callback();
|
|
5153
|
-
}).catch(function (err) {
|
|
5154
|
-
console.error("[memory-summary] Failed to create init summary session for mate " + mateId + ":", err);
|
|
5155
|
-
callback();
|
|
5156
|
-
});
|
|
5157
|
-
}
|
|
4257
|
+
// --- Memory engine (delegated to project-memory.js) ---
|
|
4258
|
+
var _memory = attachMemory({
|
|
4259
|
+
cwd: cwd,
|
|
4260
|
+
sm: sm,
|
|
4261
|
+
sdk: sdk,
|
|
4262
|
+
sendTo: sendTo,
|
|
4263
|
+
matesModule: matesModule,
|
|
4264
|
+
sessionSearch: sessionSearch,
|
|
4265
|
+
getAllProjectSessions: getAllProjectSessions,
|
|
4266
|
+
projectOwnerId: projectOwnerId,
|
|
4267
|
+
handleMessage: handleMessage,
|
|
4268
|
+
});
|
|
4269
|
+
var loadMateDigests = _memory.loadMateDigests;
|
|
4270
|
+
var gateMemory = _memory.gateMemory;
|
|
4271
|
+
var updateMemorySummary = _memory.updateMemorySummary;
|
|
4272
|
+
var initMemorySummary = _memory.initMemorySummary;
|
|
4273
|
+
|
|
4274
|
+
// --- Mate interaction engine (delegated to project-mate-interaction.js) ---
|
|
4275
|
+
// Note: checkForDmDebateBrief comes from _debate (initialized below),
|
|
4276
|
+
// so we use a lazy getter that resolves at call time.
|
|
4277
|
+
var _mateInteraction = attachMateInteraction({
|
|
4278
|
+
cwd: cwd,
|
|
4279
|
+
sm: sm,
|
|
4280
|
+
sdk: sdk,
|
|
4281
|
+
send: send,
|
|
4282
|
+
sendTo: sendTo,
|
|
4283
|
+
sendToSession: sendToSession,
|
|
4284
|
+
sendToSessionOthers: sendToSessionOthers,
|
|
4285
|
+
matesModule: matesModule,
|
|
4286
|
+
isMate: isMate,
|
|
4287
|
+
projectOwnerId: projectOwnerId,
|
|
4288
|
+
getSessionForWs: getSessionForWs,
|
|
4289
|
+
getLinuxUserForSession: getLinuxUserForSession,
|
|
4290
|
+
saveImageFile: saveImageFile,
|
|
4291
|
+
hydrateImageRefs: hydrateImageRefs,
|
|
4292
|
+
onProcessingChanged: onProcessingChanged,
|
|
4293
|
+
loadMateDigests: loadMateDigests,
|
|
4294
|
+
updateMemorySummary: updateMemorySummary,
|
|
4295
|
+
initMemorySummary: initMemorySummary,
|
|
4296
|
+
get checkForDmDebateBrief() { return checkForDmDebateBrief; },
|
|
4297
|
+
});
|
|
4298
|
+
var handleMention = _mateInteraction.handleMention;
|
|
4299
|
+
var getMateProfile = _mateInteraction.getMateProfile;
|
|
4300
|
+
var loadMateClaudeMd = _mateInteraction.loadMateClaudeMd;
|
|
4301
|
+
var digestDmTurn = _mateInteraction.digestDmTurn;
|
|
4302
|
+
var enqueueDigest = _mateInteraction.enqueueDigest;
|
|
5158
4303
|
|
|
5159
4304
|
// --- Debate engine (delegated to project-debate.js) ---
|
|
5160
4305
|
var _debate = attachDebate({
|
|
@@ -5176,10 +4321,12 @@ function createProjectContext(opts) {
|
|
|
5176
4321
|
initMemorySummary: initMemorySummary,
|
|
5177
4322
|
});
|
|
5178
4323
|
var handleDebateStart = _debate.handleDebateStart;
|
|
4324
|
+
var handleDebateHandRaise = _debate.handleDebateHandRaise;
|
|
5179
4325
|
var handleDebateComment = _debate.handleDebateComment;
|
|
5180
4326
|
var handleDebateStop = _debate.handleDebateStop;
|
|
5181
4327
|
var handleDebateConcludeResponse = _debate.handleDebateConcludeResponse;
|
|
5182
4328
|
var handleDebateConfirmBrief = _debate.handleDebateConfirmBrief;
|
|
4329
|
+
var handleDebateUserFloorResponse = _debate.handleDebateUserFloorResponse;
|
|
5183
4330
|
var restoreDebateState = _debate.restoreDebateState;
|
|
5184
4331
|
var checkForDmDebateBrief = _debate.checkForDmDebateBrief;
|
|
5185
4332
|
|
|
@@ -5231,6 +4378,44 @@ function createProjectContext(opts) {
|
|
|
5231
4378
|
|
|
5232
4379
|
// --- Handle project-scoped HTTP requests ---
|
|
5233
4380
|
function handleHTTP(req, res, urlPath) {
|
|
4381
|
+
// Browser MCP extension bridge: forward commands to Chrome extension
|
|
4382
|
+
if (req.method === "POST" && urlPath === "/ext-command") {
|
|
4383
|
+
parseJsonBody(req).then(function (body) {
|
|
4384
|
+
// Validate auth token
|
|
4385
|
+
if (!body.token || body.token !== _extToken) {
|
|
4386
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4387
|
+
res.end('{"error":"Invalid token"}');
|
|
4388
|
+
return;
|
|
4389
|
+
}
|
|
4390
|
+
var command = body.command;
|
|
4391
|
+
var args = body.args || {};
|
|
4392
|
+
var timeout = Math.min(body.timeout || 5000, 30000); // max 30s
|
|
4393
|
+
|
|
4394
|
+
// Special command: list_tabs (no extension round-trip needed)
|
|
4395
|
+
if (command === "list_tabs") {
|
|
4396
|
+
var tabArr = [];
|
|
4397
|
+
for (var tid in _browserTabList) {
|
|
4398
|
+
tabArr.push(_browserTabList[tid]);
|
|
4399
|
+
}
|
|
4400
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4401
|
+
res.end(JSON.stringify({ result: { tabs: tabArr } }));
|
|
4402
|
+
return;
|
|
4403
|
+
}
|
|
4404
|
+
|
|
4405
|
+
sendExtensionCommandAny(command, args, timeout).then(function (result) {
|
|
4406
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4407
|
+
res.end(JSON.stringify({ result: result || {} }));
|
|
4408
|
+
}).catch(function (err) {
|
|
4409
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4410
|
+
res.end(JSON.stringify({ error: err.message || "Extension command failed" }));
|
|
4411
|
+
});
|
|
4412
|
+
}).catch(function () {
|
|
4413
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4414
|
+
res.end('{"error":"Invalid JSON body"}');
|
|
4415
|
+
});
|
|
4416
|
+
return true;
|
|
4417
|
+
}
|
|
4418
|
+
|
|
5234
4419
|
// Serve chat images
|
|
5235
4420
|
if (req.method === "GET" && urlPath.indexOf("/images/") === 0) {
|
|
5236
4421
|
var imgName = path.basename(urlPath);
|
|
@@ -5265,7 +4450,7 @@ function createProjectContext(opts) {
|
|
|
5265
4450
|
return;
|
|
5266
4451
|
}
|
|
5267
4452
|
// Sanitize filename — strip path separators
|
|
5268
|
-
var safeName = path.basename(fileName).replace(/[
|
|
4453
|
+
var safeName = path.basename(fileName).replace(/[\x00-\x1f\/\\:*?"<>|]/g, "_");
|
|
5269
4454
|
if (!safeName) safeName = "upload";
|
|
5270
4455
|
|
|
5271
4456
|
// Check size
|