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.
@@ -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;
@@ -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
- try { fs.chmodSync(filePath, 0o600); } catch (e) {}
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
- if (query && allLines.length > 5) {
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 from session manager
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
- // Global search: collect ALL mates' digest files + memory summaries
4467
- var otherDigests = [];
4648
+ // globalSearch: also collect sessions from all other projects + knowledge files
4649
+ var knowledgeFiles = [];
4468
4650
  if (hasGlobalSearch) {
4469
- try {
4470
- var allMates = matesModule.getAllMates(mateCtx);
4471
- var teamSummaries = [];
4472
- for (var mi = 0; mi < allMates.length; mi++) {
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
- // Collect memory summaries for direct context injection
4484
- var otherSummary = path.join(otherDir, "knowledge", "memory-summary.md");
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
- if (fs.existsSync(otherSummary)) {
4487
- var summaryText = fs.readFileSync(otherSummary, "utf8").trim();
4488
- if (summaryText && summaryText.length > 50) {
4489
- teamSummaries.push({ mateName: mateName, summary: summaryText });
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 ? 8 : 5,
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); },