clay-server 2.23.2-beta.1 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/daemon.js +13 -0
- package/lib/ipv4-only.js +39 -0
- package/lib/project.js +331 -42
- package/lib/public/app.js +119 -69
- package/lib/public/claude-code-avatar.png +0 -0
- package/lib/public/css/debate.css +35 -1
- package/lib/public/css/filebrowser.css +2 -1
- package/lib/public/css/icon-strip.css +23 -0
- package/lib/public/css/input.css +66 -0
- package/lib/public/css/loop.css +0 -2
- package/lib/public/css/mates.css +113 -6
- package/lib/public/css/mention.css +26 -1
- package/lib/public/css/messages.css +97 -0
- package/lib/public/css/overlays.css +0 -4
- package/lib/public/css/server-settings.css +53 -0
- package/lib/public/css/session-search.css +1 -1
- package/lib/public/css/sidebar.css +26 -2
- package/lib/public/index.html +53 -13
- package/lib/public/modules/debate.js +158 -1
- package/lib/public/modules/filebrowser.js +11 -0
- package/lib/public/modules/input.js +20 -2
- package/lib/public/modules/markdown.js +2 -2
- package/lib/public/modules/mention.js +82 -32
- package/lib/public/modules/session-search.js +5 -5
- package/lib/public/modules/sidebar.js +39 -26
- package/lib/public/modules/theme.js +30 -0
- package/lib/public/modules/user-settings.js +61 -12
- package/lib/sdk-worker.js +29 -2
- package/lib/server.js +93 -3
- package/lib/session-search.js +40 -5
- package/lib/sessions.js +2 -2
- package/lib/users.js +38 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -650,6 +650,7 @@ var relay = createServer({
|
|
|
650
650
|
headless: !!config.headless,
|
|
651
651
|
keepAwake: !!config.keepAwake,
|
|
652
652
|
autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
|
|
653
|
+
chatLayout: config.chatLayout || "channel",
|
|
653
654
|
pinEnabled: !!config.pinHash,
|
|
654
655
|
platform: process.platform,
|
|
655
656
|
hostname: os2.hostname(),
|
|
@@ -689,6 +690,18 @@ var relay = createServer({
|
|
|
689
690
|
saveConfig(config);
|
|
690
691
|
console.log("[daemon] PIN hash auto-upgraded to scrypt");
|
|
691
692
|
},
|
|
693
|
+
onSetChatLayout: function (layout) {
|
|
694
|
+
var val = (layout === "bubble") ? "bubble" : "channel";
|
|
695
|
+
config.chatLayout = val;
|
|
696
|
+
saveConfig(config);
|
|
697
|
+
console.log("[daemon] Chat layout:", val, "(web)");
|
|
698
|
+
return { ok: true, chatLayout: val };
|
|
699
|
+
},
|
|
700
|
+
onSetMateOnboarded: function () {
|
|
701
|
+
config.mateOnboardingShown = true;
|
|
702
|
+
saveConfig(config);
|
|
703
|
+
return { ok: true };
|
|
704
|
+
},
|
|
692
705
|
onSetAutoContinue: function (value) {
|
|
693
706
|
var want = !!value;
|
|
694
707
|
config.autoContinueOnRateLimit = want;
|
package/lib/ipv4-only.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ipv4-only.js — Preload script that forces ALL network calls to IPv4.
|
|
2
|
+
// Works with both Node built-in https AND undici (used by CLI's Axios).
|
|
3
|
+
// Loaded via NODE_OPTIONS="--require /path/to/ipv4-only.js"
|
|
4
|
+
|
|
5
|
+
// 1. Patch dns.lookup to only return IPv4
|
|
6
|
+
var dns = require("dns");
|
|
7
|
+
var origLookup = dns.lookup;
|
|
8
|
+
dns.lookup = function(hostname, options, callback) {
|
|
9
|
+
if (typeof options === "function") { callback = options; options = {}; }
|
|
10
|
+
if (typeof options === "number") { options = { family: options }; }
|
|
11
|
+
options = options || {};
|
|
12
|
+
options.family = 4;
|
|
13
|
+
return origLookup.call(dns, hostname, options, callback);
|
|
14
|
+
};
|
|
15
|
+
try { dns.setDefaultResultOrder("ipv4first"); } catch (e) {}
|
|
16
|
+
|
|
17
|
+
// 2. Disable autoSelectFamily at net level
|
|
18
|
+
try {
|
|
19
|
+
var net = require("net");
|
|
20
|
+
if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false);
|
|
21
|
+
} catch (e) {}
|
|
22
|
+
|
|
23
|
+
// 3. Patch net.connect/net.createConnection to force family:4
|
|
24
|
+
var origConnect = net.connect;
|
|
25
|
+
var origCreateConnection = net.createConnection;
|
|
26
|
+
function patchOpts(args) {
|
|
27
|
+
if (args[0] && typeof args[0] === "object") {
|
|
28
|
+
args[0].family = 4;
|
|
29
|
+
args[0].autoSelectFamily = false;
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
net.connect = function() { return origConnect.apply(net, patchOpts(Array.from(arguments))); };
|
|
34
|
+
net.createConnection = function() { return origCreateConnection.apply(net, patchOpts(Array.from(arguments))); };
|
|
35
|
+
|
|
36
|
+
// 4. Patch tls.connect to force family:4 (undici uses tls.connect for HTTPS)
|
|
37
|
+
var tls = require("tls");
|
|
38
|
+
var origTlsConnect = tls.connect;
|
|
39
|
+
tls.connect = function() { return origTlsConnect.apply(tls, patchOpts(Array.from(arguments))); };
|
package/lib/project.js
CHANGED
|
@@ -107,6 +107,7 @@ function createProjectContext(opts) {
|
|
|
107
107
|
var lanHost = opts.lanHost || null;
|
|
108
108
|
var getProjectCount = opts.getProjectCount || function () { return 1; };
|
|
109
109
|
var getProjectList = opts.getProjectList || function () { return []; };
|
|
110
|
+
var getAllProjectSessions = opts.getAllProjectSessions || function () { return []; };
|
|
110
111
|
var getHubSchedules = opts.getHubSchedules || function () { return []; };
|
|
111
112
|
var moveScheduleToProject = opts.moveScheduleToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
112
113
|
var moveAllSchedulesToProject = opts.moveAllSchedulesToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
@@ -146,7 +147,7 @@ function createProjectContext(opts) {
|
|
|
146
147
|
return hydrated;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
function saveImageFile(mediaType, base64data) {
|
|
150
|
+
function saveImageFile(mediaType, base64data, ownerLinuxUser) {
|
|
150
151
|
try { fs.mkdirSync(imagesDir, { recursive: true }); } catch (e) {}
|
|
151
152
|
var ext = mediaType === "image/png" ? ".png" : mediaType === "image/gif" ? ".gif" : mediaType === "image/webp" ? ".webp" : ".jpg";
|
|
152
153
|
var hash = crypto.createHash("sha256").update(base64data).digest("hex").substring(0, 16);
|
|
@@ -155,7 +156,26 @@ function createProjectContext(opts) {
|
|
|
155
156
|
try {
|
|
156
157
|
fs.writeFileSync(filePath, Buffer.from(base64data, "base64"));
|
|
157
158
|
if (process.platform !== "win32") {
|
|
158
|
-
|
|
159
|
+
// 644 so all local users can read (needed for git, copy, etc.)
|
|
160
|
+
try { fs.chmodSync(filePath, 0o644); } catch (e) {}
|
|
161
|
+
// In OS-user mode the daemon runs as root, so chown the file
|
|
162
|
+
// (and parent dirs) to the session owner to avoid permission issues.
|
|
163
|
+
if (ownerLinuxUser) {
|
|
164
|
+
try {
|
|
165
|
+
var osUsersMod = require("./os-users");
|
|
166
|
+
var uid = osUsersMod.getLinuxUserUid(ownerLinuxUser);
|
|
167
|
+
if (uid != null) {
|
|
168
|
+
require("child_process").execSync("chown " + uid + " " + JSON.stringify(filePath));
|
|
169
|
+
// Also fix parent dirs if root-owned
|
|
170
|
+
try {
|
|
171
|
+
var dirStat = fs.statSync(imagesDir);
|
|
172
|
+
if (dirStat.uid !== uid) {
|
|
173
|
+
require("child_process").execSync("chown " + uid + " " + JSON.stringify(imagesDir));
|
|
174
|
+
}
|
|
175
|
+
} catch (e2) {}
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {}
|
|
178
|
+
}
|
|
159
179
|
}
|
|
160
180
|
return fileName;
|
|
161
181
|
} catch (e) {
|
|
@@ -3730,7 +3750,7 @@ function createProjectContext(opts) {
|
|
|
3730
3750
|
var imageRefs = [];
|
|
3731
3751
|
for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
|
|
3732
3752
|
var img = msg.images[imgIdx];
|
|
3733
|
-
var savedName = saveImageFile(img.mediaType, img.data);
|
|
3753
|
+
var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
|
|
3734
3754
|
if (savedName) {
|
|
3735
3755
|
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
3736
3756
|
savedImagePaths.push(path.join(imagesDir, savedName));
|
|
@@ -3948,8 +3968,13 @@ function createProjectContext(opts) {
|
|
|
3948
3968
|
' "user_sentiment": "how user felt",',
|
|
3949
3969
|
' "confidence": "high|medium|low",',
|
|
3950
3970
|
' "revisit_later": true/false,',
|
|
3951
|
-
' "tags": ["topic", "tags"]',
|
|
3971
|
+
' "tags": ["topic", "tags"],',
|
|
3972
|
+
' "user_observations": [{"category":"pattern|decision|reaction|preference","observation":"...","evidence":"..."}]',
|
|
3952
3973
|
"}",
|
|
3974
|
+
"",
|
|
3975
|
+
"user_observations: OPTIONAL array. Include ONLY if you noticed meaningful patterns about the USER themselves (not the topic).",
|
|
3976
|
+
"Categories: pattern (repeated behavior 2+ times), decision (explicit choice with reasoning), reaction (emotional/attitude signal), preference (tool/style/communication preference).",
|
|
3977
|
+
"Omit the field entirely if nothing notable about the user.",
|
|
3953
3978
|
].join("\n");
|
|
3954
3979
|
|
|
3955
3980
|
function handleResult(text) {
|
|
@@ -3979,7 +4004,33 @@ function createProjectContext(opts) {
|
|
|
3979
4004
|
console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
|
|
3980
4005
|
}
|
|
3981
4006
|
|
|
4007
|
+
// Write user observations if present
|
|
4008
|
+
if (digestObj.user_observations && digestObj.user_observations.length > 0) {
|
|
4009
|
+
try {
|
|
4010
|
+
var obsFile = path.join(knowledgeDir, "user-observations.jsonl");
|
|
4011
|
+
var obsMate = matesModule.getMate(job.mateCtx, job.mateId);
|
|
4012
|
+
var obsMateName = (obsMate && obsMate.name) || job.mateId;
|
|
4013
|
+
var obsLines = [];
|
|
4014
|
+
for (var oi = 0; oi < digestObj.user_observations.length; oi++) {
|
|
4015
|
+
var obs = digestObj.user_observations[oi];
|
|
4016
|
+
obsLines.push(JSON.stringify({
|
|
4017
|
+
date: digestObj.date || new Date().toISOString().slice(0, 10),
|
|
4018
|
+
category: obs.category || "pattern",
|
|
4019
|
+
observation: obs.observation || "",
|
|
4020
|
+
evidence: obs.evidence || "",
|
|
4021
|
+
confidence: digestObj.confidence || "medium",
|
|
4022
|
+
mateName: obsMateName,
|
|
4023
|
+
mateId: job.mateId
|
|
4024
|
+
}));
|
|
4025
|
+
}
|
|
4026
|
+
fs.appendFileSync(obsFile, obsLines.join("\n") + "\n");
|
|
4027
|
+
} catch (e) {
|
|
4028
|
+
console.error("[digest-worker] Observations write failed for " + job.mateId + ":", e.message);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
|
|
3982
4032
|
updateMemorySummary(job.mateCtx, job.mateId, digestObj);
|
|
4033
|
+
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
|
|
3983
4034
|
if (job.onDone) job.onDone();
|
|
3984
4035
|
processDigestQueue();
|
|
3985
4036
|
}
|
|
@@ -4172,7 +4223,7 @@ function createProjectContext(opts) {
|
|
|
4172
4223
|
if (msg.images && msg.images.length > 0) {
|
|
4173
4224
|
for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
|
|
4174
4225
|
var img = msg.images[imgIdx];
|
|
4175
|
-
var savedName = saveImageFile(img.mediaType, img.data);
|
|
4226
|
+
var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
|
|
4176
4227
|
if (savedName) {
|
|
4177
4228
|
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
4178
4229
|
}
|
|
@@ -4419,6 +4470,19 @@ function createProjectContext(opts) {
|
|
|
4419
4470
|
var mate = matesModule.getMate(mateCtx, mateId);
|
|
4420
4471
|
var hasGlobalSearch = mate && mate.globalSearch;
|
|
4421
4472
|
|
|
4473
|
+
// Load shared user profile (available to ALL mates)
|
|
4474
|
+
var userProfileResult = "";
|
|
4475
|
+
try {
|
|
4476
|
+
var matesRoot = matesModule.resolveMatesRoot(mateCtx);
|
|
4477
|
+
var userProfilePath = path.join(matesRoot, "user-profile.md");
|
|
4478
|
+
if (fs.existsSync(userProfilePath)) {
|
|
4479
|
+
var profileContent = fs.readFileSync(userProfilePath, "utf8").trim();
|
|
4480
|
+
if (profileContent && profileContent.length > 50) {
|
|
4481
|
+
userProfileResult = "\n\n" + profileContent;
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
} catch (e) {}
|
|
4485
|
+
|
|
4422
4486
|
// Check for memory-summary.md first
|
|
4423
4487
|
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4424
4488
|
var hasSummary = false;
|
|
@@ -4439,7 +4503,7 @@ function createProjectContext(opts) {
|
|
|
4439
4503
|
}
|
|
4440
4504
|
} catch (e) {}
|
|
4441
4505
|
|
|
4442
|
-
var result =
|
|
4506
|
+
var result = userProfileResult;
|
|
4443
4507
|
|
|
4444
4508
|
if (hasSummary) {
|
|
4445
4509
|
// Load summary + latest 5 raw digests for richer context
|
|
@@ -4454,10 +4518,126 @@ function createProjectContext(opts) {
|
|
|
4454
4518
|
result = formatRawDigests(recent, "Your recent session memories:");
|
|
4455
4519
|
}
|
|
4456
4520
|
|
|
4521
|
+
// Global search: always load team memory summaries for globalSearch mates
|
|
4522
|
+
var otherDigests = [];
|
|
4523
|
+
if (hasGlobalSearch) {
|
|
4524
|
+
try {
|
|
4525
|
+
var allMates = matesModule.getAllMates(mateCtx);
|
|
4526
|
+
var teamSummaries = [];
|
|
4527
|
+
for (var mi = 0; mi < allMates.length; mi++) {
|
|
4528
|
+
if (allMates[mi].id === mateId) continue;
|
|
4529
|
+
var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
|
|
4530
|
+
var mateName = allMates[mi].name || allMates[mi].id;
|
|
4531
|
+
|
|
4532
|
+
// Collect digest files for BM25 search
|
|
4533
|
+
var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
|
|
4534
|
+
if (fs.existsSync(otherDigest)) {
|
|
4535
|
+
otherDigests.push({ path: otherDigest, mateName: mateName });
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
// Collect memory summaries for direct context injection
|
|
4539
|
+
var otherSummary = path.join(otherDir, "knowledge", "memory-summary.md");
|
|
4540
|
+
try {
|
|
4541
|
+
if (fs.existsSync(otherSummary)) {
|
|
4542
|
+
var summaryText = fs.readFileSync(otherSummary, "utf8").trim();
|
|
4543
|
+
if (summaryText && summaryText.length > 50) {
|
|
4544
|
+
teamSummaries.push({ mateName: mateName, summary: summaryText });
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
} catch (e) {}
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
// Inject team memory summaries into context
|
|
4551
|
+
if (teamSummaries.length > 0) {
|
|
4552
|
+
result += "\n\nTeam memory summaries (other mates' accumulated context):";
|
|
4553
|
+
for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
|
|
4554
|
+
var ts = teamSummaries[tsi];
|
|
4555
|
+
// Cap each summary to avoid context overflow
|
|
4556
|
+
var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
|
|
4557
|
+
result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
} catch (e) {}
|
|
4561
|
+
|
|
4562
|
+
// Inject recent user observations from all mates (newest first, max 15)
|
|
4563
|
+
try {
|
|
4564
|
+
var allObservations = [];
|
|
4565
|
+
var allMatesForObs = matesModule.getAllMates(mateCtx);
|
|
4566
|
+
for (var moi = 0; moi < allMatesForObs.length; moi++) {
|
|
4567
|
+
var moDir = matesModule.getMateDir(mateCtx, allMatesForObs[moi].id);
|
|
4568
|
+
var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
|
|
4569
|
+
try {
|
|
4570
|
+
if (fs.existsSync(moFile)) {
|
|
4571
|
+
var moLines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
|
|
4572
|
+
for (var mli = 0; mli < moLines.length; mli++) {
|
|
4573
|
+
try {
|
|
4574
|
+
var moEntry = JSON.parse(moLines[mli]);
|
|
4575
|
+
moEntry._mateName = moEntry.mateName || allMatesForObs[moi].name || allMatesForObs[moi].id;
|
|
4576
|
+
allObservations.push(moEntry);
|
|
4577
|
+
} catch (e) {}
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
} catch (e) {}
|
|
4581
|
+
}
|
|
4582
|
+
if (allObservations.length > 0) {
|
|
4583
|
+
// Sort by date descending
|
|
4584
|
+
allObservations.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
|
|
4585
|
+
var recentObs = allObservations.slice(0, 15);
|
|
4586
|
+
result += "\n\nRecent user observations from all mates:";
|
|
4587
|
+
for (var roi = 0; roi < recentObs.length; roi++) {
|
|
4588
|
+
var ro = recentObs[roi];
|
|
4589
|
+
result += "\n- [" + (ro.date || "?") + "] [@" + ro._mateName + "] [" + (ro.category || "?") + "] " + (ro.observation || "") + (ro.evidence ? " (evidence: " + ro.evidence + ")" : "");
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
} catch (e) {}
|
|
4593
|
+
|
|
4594
|
+
// Inject recent activity timeline across all projects (chronological)
|
|
4595
|
+
try {
|
|
4596
|
+
var timelineEntries = [];
|
|
4597
|
+
|
|
4598
|
+
// Own sessions
|
|
4599
|
+
sm.sessions.forEach(function (s) {
|
|
4600
|
+
if (s.hidden || !s.history || s.history.length === 0) return;
|
|
4601
|
+
timelineEntries.push({
|
|
4602
|
+
title: s.title || "New Session",
|
|
4603
|
+
project: null,
|
|
4604
|
+
ts: s.lastActivity || s.createdAt || 0
|
|
4605
|
+
});
|
|
4606
|
+
});
|
|
4607
|
+
|
|
4608
|
+
// Cross-project sessions
|
|
4609
|
+
var crossForTimeline = getAllProjectSessions();
|
|
4610
|
+
for (var cti = 0; cti < crossForTimeline.length; cti++) {
|
|
4611
|
+
var cs = crossForTimeline[cti];
|
|
4612
|
+
timelineEntries.push({
|
|
4613
|
+
title: cs.title || "New Session",
|
|
4614
|
+
project: cs._projectTitle || null,
|
|
4615
|
+
ts: cs.lastActivity || cs.createdAt || 0
|
|
4616
|
+
});
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
// Sort by time descending, take latest 20
|
|
4620
|
+
timelineEntries.sort(function (a, b) { return b.ts - a.ts; });
|
|
4621
|
+
timelineEntries = timelineEntries.slice(0, 20);
|
|
4622
|
+
|
|
4623
|
+
if (timelineEntries.length > 0) {
|
|
4624
|
+
result += "\n\nRecent activity timeline (newest first):";
|
|
4625
|
+
for (var ti = 0; ti < timelineEntries.length; ti++) {
|
|
4626
|
+
var te = timelineEntries[ti];
|
|
4627
|
+
var dateStr = te.ts ? new Date(te.ts).toISOString().replace("T", " ").substring(0, 16) : "?";
|
|
4628
|
+
var line = "- [" + dateStr + "] " + te.title;
|
|
4629
|
+
if (te.project) line += " (project: " + te.project + ")";
|
|
4630
|
+
result += "\n" + line;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
} catch (e) {}
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4457
4636
|
// BM25 unified search: digests + session history for current topic
|
|
4458
|
-
|
|
4637
|
+
// globalSearch mates always search (they see everything); others need enough digests
|
|
4638
|
+
if (query && (hasGlobalSearch || allLines.length > 5)) {
|
|
4459
4639
|
try {
|
|
4460
|
-
// Collect mate's sessions
|
|
4640
|
+
// Collect mate's own sessions
|
|
4461
4641
|
var mateSessions = [];
|
|
4462
4642
|
sm.sessions.forEach(function (s) {
|
|
4463
4643
|
if (!s.hidden && s.history && s.history.length > 0) {
|
|
@@ -4465,45 +4645,37 @@ function createProjectContext(opts) {
|
|
|
4465
4645
|
}
|
|
4466
4646
|
});
|
|
4467
4647
|
|
|
4468
|
-
//
|
|
4469
|
-
var
|
|
4648
|
+
// globalSearch: also collect sessions from all other projects + knowledge files
|
|
4649
|
+
var knowledgeFiles = [];
|
|
4470
4650
|
if (hasGlobalSearch) {
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
if (allMates[mi].id === mateId) continue;
|
|
4476
|
-
var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
|
|
4477
|
-
var mateName = allMates[mi].name || allMates[mi].id;
|
|
4478
|
-
|
|
4479
|
-
// Collect digest files for BM25 search
|
|
4480
|
-
var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
|
|
4481
|
-
if (fs.existsSync(otherDigest)) {
|
|
4482
|
-
otherDigests.push({ path: otherDigest, mateName: mateName });
|
|
4483
|
-
}
|
|
4651
|
+
var crossSessions = getAllProjectSessions();
|
|
4652
|
+
for (var cs = 0; cs < crossSessions.length; cs++) {
|
|
4653
|
+
mateSessions.push(crossSessions[cs]);
|
|
4654
|
+
}
|
|
4484
4655
|
|
|
4485
|
-
|
|
4486
|
-
|
|
4656
|
+
// Collect knowledge files from all mates
|
|
4657
|
+
try {
|
|
4658
|
+
var allMatesForKnowledge = matesModule.getAllMates(mateCtx);
|
|
4659
|
+
for (var mk = 0; mk < allMatesForKnowledge.length; mk++) {
|
|
4660
|
+
var mkDir = matesModule.getMateDir(mateCtx, allMatesForKnowledge[mk].id);
|
|
4661
|
+
var mkName = allMatesForKnowledge[mk].name || allMatesForKnowledge[mk].id;
|
|
4662
|
+
var mkKnowledgeDir = path.join(mkDir, "knowledge");
|
|
4487
4663
|
try {
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4664
|
+
var kFiles = fs.readdirSync(mkKnowledgeDir);
|
|
4665
|
+
for (var kfi = 0; kfi < kFiles.length; kfi++) {
|
|
4666
|
+
var kfName = kFiles[kfi];
|
|
4667
|
+
// Skip system files (digests, identity, base-template)
|
|
4668
|
+
if (kfName === "session-digests.jsonl" || kfName === "memory-summary.md" ||
|
|
4669
|
+
kfName === "identity-backup.md" || kfName === "identity-history.jsonl" ||
|
|
4670
|
+
kfName === "base-template.md") continue;
|
|
4671
|
+
knowledgeFiles.push({
|
|
4672
|
+
filePath: path.join(mkKnowledgeDir, kfName),
|
|
4673
|
+
name: kfName,
|
|
4674
|
+
mateName: mkName
|
|
4675
|
+
});
|
|
4493
4676
|
}
|
|
4494
4677
|
} catch (e) {}
|
|
4495
4678
|
}
|
|
4496
|
-
|
|
4497
|
-
// Inject team memory summaries into context
|
|
4498
|
-
if (teamSummaries.length > 0) {
|
|
4499
|
-
result += "\n\nTeam memory summaries (other mates' accumulated context):";
|
|
4500
|
-
for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
|
|
4501
|
-
var ts = teamSummaries[tsi];
|
|
4502
|
-
// Cap each summary to avoid context overflow
|
|
4503
|
-
var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
|
|
4504
|
-
result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
|
|
4505
|
-
}
|
|
4506
|
-
}
|
|
4507
4679
|
} catch (e) {}
|
|
4508
4680
|
}
|
|
4509
4681
|
|
|
@@ -4511,8 +4683,9 @@ function createProjectContext(opts) {
|
|
|
4511
4683
|
digestFilePath: digestFile,
|
|
4512
4684
|
otherDigests: otherDigests,
|
|
4513
4685
|
sessions: mateSessions,
|
|
4686
|
+
knowledgeFiles: knowledgeFiles,
|
|
4514
4687
|
query: query,
|
|
4515
|
-
maxResults: hasGlobalSearch ?
|
|
4688
|
+
maxResults: hasGlobalSearch ? 12 : 5,
|
|
4516
4689
|
minScore: 1.0
|
|
4517
4690
|
});
|
|
4518
4691
|
var contextStr = sessionSearch.formatForContext(searchResults);
|
|
@@ -4738,6 +4911,108 @@ function createProjectContext(opts) {
|
|
|
4738
4911
|
});
|
|
4739
4912
|
}
|
|
4740
4913
|
|
|
4914
|
+
// User profile synthesis: collect observations from all mates, synthesize unified profile
|
|
4915
|
+
var USER_PROFILE_SYNTHESIS_THRESHOLD = 8;
|
|
4916
|
+
|
|
4917
|
+
function maybeSynthesizeUserProfile(mateCtx, mateId) {
|
|
4918
|
+
var mate = matesModule.getMate(mateCtx, mateId);
|
|
4919
|
+
if (!mate || !mate.globalSearch) return; // Only primary/globalSearch mates synthesize
|
|
4920
|
+
|
|
4921
|
+
var matesRoot = matesModule.resolveMatesRoot(mateCtx);
|
|
4922
|
+
var profilePath = path.join(matesRoot, "user-profile.md");
|
|
4923
|
+
|
|
4924
|
+
// Collect all observations across all mates
|
|
4925
|
+
var allObs = [];
|
|
4926
|
+
try {
|
|
4927
|
+
var allMates = matesModule.getAllMates(mateCtx);
|
|
4928
|
+
for (var mi = 0; mi < allMates.length; mi++) {
|
|
4929
|
+
var moDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
|
|
4930
|
+
var moFile = path.join(moDir, "knowledge", "user-observations.jsonl");
|
|
4931
|
+
try {
|
|
4932
|
+
if (fs.existsSync(moFile)) {
|
|
4933
|
+
var lines = fs.readFileSync(moFile, "utf8").trim().split("\n").filter(function (l) { return l.trim(); });
|
|
4934
|
+
for (var li = 0; li < lines.length; li++) {
|
|
4935
|
+
try { allObs.push(JSON.parse(lines[li])); } catch (e) {}
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
} catch (e) {}
|
|
4939
|
+
}
|
|
4940
|
+
} catch (e) { return; }
|
|
4941
|
+
|
|
4942
|
+
if (allObs.length === 0) return;
|
|
4943
|
+
|
|
4944
|
+
// Check if synthesis is needed (threshold since last synthesis)
|
|
4945
|
+
var existingProfile = "";
|
|
4946
|
+
var lastObsCount = 0;
|
|
4947
|
+
try {
|
|
4948
|
+
if (fs.existsSync(profilePath)) {
|
|
4949
|
+
existingProfile = fs.readFileSync(profilePath, "utf8").trim();
|
|
4950
|
+
var countMatch = existingProfile.match(/from (\d+) observations/);
|
|
4951
|
+
if (countMatch) lastObsCount = parseInt(countMatch[1], 10) || 0;
|
|
4952
|
+
}
|
|
4953
|
+
} catch (e) {}
|
|
4954
|
+
|
|
4955
|
+
if (allObs.length - lastObsCount < USER_PROFILE_SYNTHESIS_THRESHOLD) return;
|
|
4956
|
+
|
|
4957
|
+
// Sort newest first for synthesis
|
|
4958
|
+
allObs.sort(function (a, b) { return (b.date || "").localeCompare(a.date || ""); });
|
|
4959
|
+
|
|
4960
|
+
var synthContext = [
|
|
4961
|
+
"[SYSTEM: User Profile Synthesis]",
|
|
4962
|
+
"You are synthesizing a user profile from observations collected by multiple AI teammates.",
|
|
4963
|
+
"",
|
|
4964
|
+
"Current profile:",
|
|
4965
|
+
existingProfile || "(none yet, first synthesis)",
|
|
4966
|
+
"",
|
|
4967
|
+
"All observations (" + allObs.length + " total, newest first):",
|
|
4968
|
+
allObs.map(function (o) {
|
|
4969
|
+
return "[" + (o.date || "?") + "] [@" + (o.mateName || o.mateId || "?") + "] [" + (o.category || "?") + "] " + (o.observation || "") + (o.evidence ? " (evidence: " + o.evidence + ")" : "");
|
|
4970
|
+
}).join("\n"),
|
|
4971
|
+
].join("\n");
|
|
4972
|
+
|
|
4973
|
+
var synthPrompt = [
|
|
4974
|
+
"Synthesize a unified user profile from these observations.",
|
|
4975
|
+
"",
|
|
4976
|
+
"Rules:",
|
|
4977
|
+
"1. Organize by: Communication Style, Decision Patterns, Working Habits, Technical Preferences, Emotional Signals",
|
|
4978
|
+
"2. Each point: observation + source mates and dates in parentheses",
|
|
4979
|
+
"3. If observations contradict, note both with dates. Preferences evolve.",
|
|
4980
|
+
"4. Mark patterns seen 3+ times as [strong], 2 times as [emerging]",
|
|
4981
|
+
"5. Keep under 800 words. This is a reference card, not a biography.",
|
|
4982
|
+
'6. End with: "Last synthesized: YYYY-MM-DD from N observations across M mates"',
|
|
4983
|
+
"",
|
|
4984
|
+
"Output ONLY the markdown profile. No fences, no extra text.",
|
|
4985
|
+
].join("\n");
|
|
4986
|
+
|
|
4987
|
+
var synthText = "";
|
|
4988
|
+
sdk.createMentionSession({
|
|
4989
|
+
claudeMd: "",
|
|
4990
|
+
model: "haiku",
|
|
4991
|
+
initialContext: synthContext,
|
|
4992
|
+
initialMessage: synthPrompt,
|
|
4993
|
+
onActivity: function () {},
|
|
4994
|
+
onDelta: function (delta) { synthText += delta; },
|
|
4995
|
+
onDone: function () {
|
|
4996
|
+
try {
|
|
4997
|
+
var cleaned = synthText.trim();
|
|
4998
|
+
if (cleaned.indexOf("```") === 0) {
|
|
4999
|
+
cleaned = cleaned.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "").trim();
|
|
5000
|
+
}
|
|
5001
|
+
fs.mkdirSync(path.dirname(profilePath), { recursive: true });
|
|
5002
|
+
fs.writeFileSync(profilePath, cleaned + "\n", "utf8");
|
|
5003
|
+
console.log("[user-profile] Synthesized user-profile.md from " + allObs.length + " observations");
|
|
5004
|
+
} catch (e) {
|
|
5005
|
+
console.error("[user-profile] Failed to write user-profile.md:", e.message);
|
|
5006
|
+
}
|
|
5007
|
+
},
|
|
5008
|
+
onError: function (err) {
|
|
5009
|
+
console.error("[user-profile] Synthesis failed:", err);
|
|
5010
|
+
},
|
|
5011
|
+
}).catch(function (err) {
|
|
5012
|
+
console.error("[user-profile] Failed to create synthesis session:", err);
|
|
5013
|
+
});
|
|
5014
|
+
}
|
|
5015
|
+
|
|
4741
5016
|
// Initial summary generation (migration): read latest 20 digests and generate first summary
|
|
4742
5017
|
function initMemorySummary(mateCtx, mateId, callback) {
|
|
4743
5018
|
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
@@ -4980,6 +5255,19 @@ function createProjectContext(opts) {
|
|
|
4980
5255
|
try {
|
|
4981
5256
|
var buf = Buffer.from(fileData, "base64");
|
|
4982
5257
|
fs.writeFileSync(destPath, buf);
|
|
5258
|
+
// Make readable by all local users and chown to session owner
|
|
5259
|
+
try { fs.chmodSync(destPath, 0o644); } catch (e2) {}
|
|
5260
|
+
try { fs.chmodSync(tmpDir, 0o755); } catch (e2) {}
|
|
5261
|
+
if (req._clayUser && req._clayUser.linuxUser) {
|
|
5262
|
+
try {
|
|
5263
|
+
var _osUM = require("./os-users");
|
|
5264
|
+
var _uid = _osUM.getLinuxUserUid(req._clayUser.linuxUser);
|
|
5265
|
+
if (_uid != null) {
|
|
5266
|
+
require("child_process").execSync("chown " + _uid + " " + JSON.stringify(destPath));
|
|
5267
|
+
require("child_process").execSync("chown " + _uid + " " + JSON.stringify(tmpDir));
|
|
5268
|
+
}
|
|
5269
|
+
} catch (e2) {}
|
|
5270
|
+
}
|
|
4983
5271
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4984
5272
|
res.end(JSON.stringify({ path: destPath, name: safeName }));
|
|
4985
5273
|
} catch (e) {
|
|
@@ -5659,6 +5947,7 @@ function createProjectContext(opts) {
|
|
|
5659
5947
|
handleDisconnection: handleDisconnection,
|
|
5660
5948
|
handleHTTP: handleHTTP,
|
|
5661
5949
|
getStatus: getStatus,
|
|
5950
|
+
getSessionManager: function () { return sm; },
|
|
5662
5951
|
getSchedules: function () { return loopRegistry.getAll(); },
|
|
5663
5952
|
importSchedule: function (data) { return loopRegistry.register(data); },
|
|
5664
5953
|
removeSchedule: function (id) { return loopRegistry.remove(id); },
|