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