clay-server 2.23.1 → 2.24.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build-user-env.js +6 -0
- package/lib/daemon.js +13 -0
- package/lib/ipv4-only.js +39 -0
- package/lib/project.js +333 -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/notifications.js +5 -1
- 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-bridge.js +83 -78
- package/lib/sdk-worker.js +83 -3
- 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/build-user-env.js
CHANGED
|
@@ -51,6 +51,12 @@ function buildUserEnv(osUserInfo) {
|
|
|
51
51
|
env.XDG_RUNTIME_DIR = process.env.XDG_RUNTIME_DIR;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Force Node.js to prefer IPv4. Without this, the SDK CLI subprocess
|
|
55
|
+
// tries IPv6 first (happy eyeballs), times out on servers without IPv6
|
|
56
|
+
// outbound, then falls back to IPv4. This causes multi-second delays
|
|
57
|
+
// on cold start (compounded by exponential backoff retries).
|
|
58
|
+
env.NODE_OPTIONS = (env.NODE_OPTIONS ? env.NODE_OPTIONS + " " : "") + "--dns-result-order=ipv4first";
|
|
59
|
+
|
|
54
60
|
return env;
|
|
55
61
|
}
|
|
56
62
|
|
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));
|
|
@@ -3788,6 +3808,8 @@ function createProjectContext(opts) {
|
|
|
3788
3808
|
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
3789
3809
|
if (!session.queryInstance && (!session.worker || session.messageQueue !== "worker")) {
|
|
3790
3810
|
// No active query (or worker idle between queries): start a new query
|
|
3811
|
+
session._queryStartTs = Date.now();
|
|
3812
|
+
console.log("[PERF] project.js: startQuery called, localId=" + session.localId + " t=0ms");
|
|
3791
3813
|
sdk.startQuery(session, fullText, msg.images, getLinuxUserForSession(session));
|
|
3792
3814
|
} else {
|
|
3793
3815
|
sdk.pushMessage(session, fullText, msg.images);
|
|
@@ -3946,8 +3968,13 @@ function createProjectContext(opts) {
|
|
|
3946
3968
|
' "user_sentiment": "how user felt",',
|
|
3947
3969
|
' "confidence": "high|medium|low",',
|
|
3948
3970
|
' "revisit_later": true/false,',
|
|
3949
|
-
' "tags": ["topic", "tags"]',
|
|
3971
|
+
' "tags": ["topic", "tags"],',
|
|
3972
|
+
' "user_observations": [{"category":"pattern|decision|reaction|preference","observation":"...","evidence":"..."}]',
|
|
3950
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.",
|
|
3951
3978
|
].join("\n");
|
|
3952
3979
|
|
|
3953
3980
|
function handleResult(text) {
|
|
@@ -3977,7 +4004,33 @@ function createProjectContext(opts) {
|
|
|
3977
4004
|
console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
|
|
3978
4005
|
}
|
|
3979
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
|
+
|
|
3980
4032
|
updateMemorySummary(job.mateCtx, job.mateId, digestObj);
|
|
4033
|
+
maybeSynthesizeUserProfile(job.mateCtx, job.mateId);
|
|
3981
4034
|
if (job.onDone) job.onDone();
|
|
3982
4035
|
processDigestQueue();
|
|
3983
4036
|
}
|
|
@@ -4170,7 +4223,7 @@ function createProjectContext(opts) {
|
|
|
4170
4223
|
if (msg.images && msg.images.length > 0) {
|
|
4171
4224
|
for (var imgIdx = 0; imgIdx < msg.images.length; imgIdx++) {
|
|
4172
4225
|
var img = msg.images[imgIdx];
|
|
4173
|
-
var savedName = saveImageFile(img.mediaType, img.data);
|
|
4226
|
+
var savedName = saveImageFile(img.mediaType, img.data, getLinuxUserForSession(session));
|
|
4174
4227
|
if (savedName) {
|
|
4175
4228
|
imageRefs.push({ mediaType: img.mediaType, file: savedName });
|
|
4176
4229
|
}
|
|
@@ -4417,6 +4470,19 @@ function createProjectContext(opts) {
|
|
|
4417
4470
|
var mate = matesModule.getMate(mateCtx, mateId);
|
|
4418
4471
|
var hasGlobalSearch = mate && mate.globalSearch;
|
|
4419
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
|
+
|
|
4420
4486
|
// Check for memory-summary.md first
|
|
4421
4487
|
var summaryFile = path.join(knowledgeDir, "memory-summary.md");
|
|
4422
4488
|
var hasSummary = false;
|
|
@@ -4437,7 +4503,7 @@ function createProjectContext(opts) {
|
|
|
4437
4503
|
}
|
|
4438
4504
|
} catch (e) {}
|
|
4439
4505
|
|
|
4440
|
-
var result =
|
|
4506
|
+
var result = userProfileResult;
|
|
4441
4507
|
|
|
4442
4508
|
if (hasSummary) {
|
|
4443
4509
|
// Load summary + latest 5 raw digests for richer context
|
|
@@ -4452,10 +4518,126 @@ function createProjectContext(opts) {
|
|
|
4452
4518
|
result = formatRawDigests(recent, "Your recent session memories:");
|
|
4453
4519
|
}
|
|
4454
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
|
+
|
|
4455
4636
|
// BM25 unified search: digests + session history for current topic
|
|
4456
|
-
|
|
4637
|
+
// globalSearch mates always search (they see everything); others need enough digests
|
|
4638
|
+
if (query && (hasGlobalSearch || allLines.length > 5)) {
|
|
4457
4639
|
try {
|
|
4458
|
-
// Collect mate's sessions
|
|
4640
|
+
// Collect mate's own sessions
|
|
4459
4641
|
var mateSessions = [];
|
|
4460
4642
|
sm.sessions.forEach(function (s) {
|
|
4461
4643
|
if (!s.hidden && s.history && s.history.length > 0) {
|
|
@@ -4463,45 +4645,37 @@ function createProjectContext(opts) {
|
|
|
4463
4645
|
}
|
|
4464
4646
|
});
|
|
4465
4647
|
|
|
4466
|
-
//
|
|
4467
|
-
var
|
|
4648
|
+
// globalSearch: also collect sessions from all other projects + knowledge files
|
|
4649
|
+
var knowledgeFiles = [];
|
|
4468
4650
|
if (hasGlobalSearch) {
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
if (allMates[mi].id === mateId) continue;
|
|
4474
|
-
var otherDir = matesModule.getMateDir(mateCtx, allMates[mi].id);
|
|
4475
|
-
var mateName = allMates[mi].name || allMates[mi].id;
|
|
4476
|
-
|
|
4477
|
-
// Collect digest files for BM25 search
|
|
4478
|
-
var otherDigest = path.join(otherDir, "knowledge", "session-digests.jsonl");
|
|
4479
|
-
if (fs.existsSync(otherDigest)) {
|
|
4480
|
-
otherDigests.push({ path: otherDigest, mateName: mateName });
|
|
4481
|
-
}
|
|
4651
|
+
var crossSessions = getAllProjectSessions();
|
|
4652
|
+
for (var cs = 0; cs < crossSessions.length; cs++) {
|
|
4653
|
+
mateSessions.push(crossSessions[cs]);
|
|
4654
|
+
}
|
|
4482
4655
|
|
|
4483
|
-
|
|
4484
|
-
|
|
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");
|
|
4485
4663
|
try {
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
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
|
+
});
|
|
4491
4676
|
}
|
|
4492
4677
|
} catch (e) {}
|
|
4493
4678
|
}
|
|
4494
|
-
|
|
4495
|
-
// Inject team memory summaries into context
|
|
4496
|
-
if (teamSummaries.length > 0) {
|
|
4497
|
-
result += "\n\nTeam memory summaries (other mates' accumulated context):";
|
|
4498
|
-
for (var tsi = 0; tsi < teamSummaries.length; tsi++) {
|
|
4499
|
-
var ts = teamSummaries[tsi];
|
|
4500
|
-
// Cap each summary to avoid context overflow
|
|
4501
|
-
var capped = ts.summary.length > 2000 ? ts.summary.substring(0, 2000) + "\n...(truncated)" : ts.summary;
|
|
4502
|
-
result += "\n\n--- @" + ts.mateName + " ---\n" + capped;
|
|
4503
|
-
}
|
|
4504
|
-
}
|
|
4505
4679
|
} catch (e) {}
|
|
4506
4680
|
}
|
|
4507
4681
|
|
|
@@ -4509,8 +4683,9 @@ function createProjectContext(opts) {
|
|
|
4509
4683
|
digestFilePath: digestFile,
|
|
4510
4684
|
otherDigests: otherDigests,
|
|
4511
4685
|
sessions: mateSessions,
|
|
4686
|
+
knowledgeFiles: knowledgeFiles,
|
|
4512
4687
|
query: query,
|
|
4513
|
-
maxResults: hasGlobalSearch ?
|
|
4688
|
+
maxResults: hasGlobalSearch ? 12 : 5,
|
|
4514
4689
|
minScore: 1.0
|
|
4515
4690
|
});
|
|
4516
4691
|
var contextStr = sessionSearch.formatForContext(searchResults);
|
|
@@ -4736,6 +4911,108 @@ function createProjectContext(opts) {
|
|
|
4736
4911
|
});
|
|
4737
4912
|
}
|
|
4738
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
|
+
|
|
4739
5016
|
// Initial summary generation (migration): read latest 20 digests and generate first summary
|
|
4740
5017
|
function initMemorySummary(mateCtx, mateId, callback) {
|
|
4741
5018
|
var mateDir = matesModule.getMateDir(mateCtx, mateId);
|
|
@@ -4978,6 +5255,19 @@ function createProjectContext(opts) {
|
|
|
4978
5255
|
try {
|
|
4979
5256
|
var buf = Buffer.from(fileData, "base64");
|
|
4980
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
|
+
}
|
|
4981
5271
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4982
5272
|
res.end(JSON.stringify({ path: destPath, name: safeName }));
|
|
4983
5273
|
} catch (e) {
|
|
@@ -5657,6 +5947,7 @@ function createProjectContext(opts) {
|
|
|
5657
5947
|
handleDisconnection: handleDisconnection,
|
|
5658
5948
|
handleHTTP: handleHTTP,
|
|
5659
5949
|
getStatus: getStatus,
|
|
5950
|
+
getSessionManager: function () { return sm; },
|
|
5660
5951
|
getSchedules: function () { return loopRegistry.getAll(); },
|
|
5661
5952
|
importSchedule: function (data) { return loopRegistry.register(data); },
|
|
5662
5953
|
removeSchedule: function (id) { return loopRegistry.remove(id); },
|