clay-server 3.4.0-beta.3 → 3.4.0-beta.5

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.
@@ -116,7 +116,7 @@ function attachConnection(ctx) {
116
116
  if (!_warmedUp) {
117
117
  _warmedUp = true;
118
118
  if (typeof warmup === "function") {
119
- try { warmup(); }
119
+ try { warmup(wsUser && wsUser.linuxUser ? wsUser.linuxUser : null); }
120
120
  catch (e) { console.error("[project-connection] warmup failed for " + slug + ":", e && e.message ? e.message : e); }
121
121
  }
122
122
  }
@@ -604,6 +604,7 @@ function attachDebate(ctx) {
604
604
  var _modMate = matesModule.getMate(mateCtx, debate.moderatorId);
605
605
  ctx.sdk.createMentionSession({
606
606
  vendor: _modMate ? _modMate.vendor : null,
607
+ linuxUser: mateCtx.linuxUser || undefined,
607
608
  claudeMd: claudeMd,
608
609
  initialContext: digests,
609
610
  initialMessage: quickBriefPrompt,
@@ -799,6 +800,7 @@ function attachDebate(ctx) {
799
800
  var _modMate2 = matesModule.getMate(mateCtx, debate.moderatorId);
800
801
  ctx.sdk.createMentionSession({
801
802
  vendor: _modMate2 ? _modMate2.vendor : null,
803
+ linuxUser: mateCtx.linuxUser || undefined,
802
804
  claudeMd: claudeMd,
803
805
  initialContext: moderatorContext,
804
806
  initialMessage: "Begin the debate on: " + debate.topic,
@@ -994,6 +996,7 @@ function attachDebate(ctx) {
994
996
  var _panMate = matesModule.getMate(debate.mateCtx, mateId);
995
997
  ctx.sdk.createMentionSession({
996
998
  vendor: _panMate ? _panMate.vendor : null,
999
+ linuxUser: debate.mateCtx && debate.mateCtx.linuxUser ? debate.mateCtx.linuxUser : undefined,
997
1000
  claudeMd: claudeMd,
998
1001
  initialContext: panelistContext + historyContext,
999
1002
  initialMessage: "The moderator addresses you:\n\n" + moderatorText,
@@ -1530,6 +1533,7 @@ function attachDebate(ctx) {
1530
1533
  var _modMate3 = matesModule.getMate(mateCtx, debate.moderatorId);
1531
1534
  ctx.sdk.createMentionSession({
1532
1535
  vendor: _modMate3 ? _modMate3.vendor : null,
1536
+ linuxUser: mateCtx.linuxUser || undefined,
1533
1537
  claudeMd: claudeMd,
1534
1538
  initialContext: moderatorContext,
1535
1539
  initialMessage: resumePrompt,
@@ -179,6 +179,7 @@ function attachMateInteraction(ctx) {
179
179
  var _digestQueue = [];
180
180
  var _digestBusy = false;
181
181
  var _digestWorkerTurns = 0;
182
+ var _digestWorkerLinuxUser = null;
182
183
  var DIGEST_WORKER_MAX_TURNS = 20;
183
184
 
184
185
  function enqueueDigest(job) {
@@ -191,6 +192,13 @@ function attachMateInteraction(ctx) {
191
192
  _digestBusy = true;
192
193
  var job = _digestQueue.shift();
193
194
 
195
+ if (_digestWorker && _digestWorkerLinuxUser !== (job.linuxUser || null)) {
196
+ try { _digestWorker.close(); } catch (e) {}
197
+ _digestWorker = null;
198
+ _digestWorkerTurns = 0;
199
+ _digestWorkerLinuxUser = null;
200
+ }
201
+
194
202
  var mateDir = matesModule.getMateDir(job.mateCtx, job.mateId);
195
203
  var knowledgeDir = path.join(mateDir, "knowledge");
196
204
 
@@ -315,6 +323,7 @@ function attachMateInteraction(ctx) {
315
323
  try { _digestWorker.close(); } catch (e) {}
316
324
  _digestWorker = null;
317
325
  _digestWorkerTurns = 0;
326
+ _digestWorkerLinuxUser = null;
318
327
  }
319
328
 
320
329
  var responseText = "";
@@ -328,12 +337,14 @@ function attachMateInteraction(ctx) {
328
337
  console.error("[digest-worker] Error:", err);
329
338
  _digestWorker = null;
330
339
  _digestWorkerTurns = 0;
340
+ _digestWorkerLinuxUser = null;
331
341
  if (job.onError) job.onError(err);
332
342
  processDigestQueue();
333
343
  },
334
344
  });
335
345
  } else {
336
346
  sdk.createMentionSession({
347
+ linuxUser: job.linuxUser || undefined,
337
348
  claudeMd: "",
338
349
  model: "haiku",
339
350
  initialContext: "[Digest Worker] You generate memory digests. Respond with ONLY JSON.",
@@ -344,10 +355,15 @@ function attachMateInteraction(ctx) {
344
355
  onError: function (err) {
345
356
  console.error("[digest-worker] Create error:", err);
346
357
  _digestWorker = null;
358
+ _digestWorkerLinuxUser = null;
347
359
  if (job.onError) job.onError(err);
348
360
  processDigestQueue();
349
361
  },
350
- }).then(function (ws) { _digestWorker = ws; _digestWorkerTurns = 1; }).catch(function (err) {
362
+ }).then(function (ws) {
363
+ _digestWorker = ws;
364
+ _digestWorkerTurns = 1;
365
+ _digestWorkerLinuxUser = job.linuxUser || null;
366
+ }).catch(function (err) {
351
367
  if (job.onError) job.onError(err || new Error("digest worker creation failed"));
352
368
  processDigestQueue();
353
369
  });
@@ -378,6 +394,7 @@ function attachMateInteraction(ctx) {
378
394
 
379
395
  enqueueDigest({
380
396
  mateCtx: mateCtx,
397
+ linuxUser: mateCtx.linuxUser || undefined,
381
398
  mateId: mateId,
382
399
  type: "mention",
383
400
  conversationContent: conversationContent,
@@ -473,6 +490,7 @@ function attachMateInteraction(ctx) {
473
490
 
474
491
  enqueueDigest({
475
492
  mateCtx: mateCtx,
493
+ linuxUser: mateCtx.linuxUser || undefined,
476
494
  mateId: mateId,
477
495
  type: "dm",
478
496
  priorSummary: priorSummary || "",
@@ -716,6 +734,7 @@ function attachMateInteraction(ctx) {
716
734
  // Create new persistent mention session
717
735
  sdk.createMentionSession({
718
736
  vendor: mate.vendor || null,
737
+ linuxUser: getLinuxUserForSession(session) || undefined,
719
738
  claudeMd: claudeMd,
720
739
  initialContext: mentionContext,
721
740
  initialMessage: mentionFullInput,
@@ -887,6 +906,7 @@ function attachMateInteraction(ctx) {
887
906
  ].join("\n");
888
907
 
889
908
  sdk.createMentionSession({
909
+ linuxUser: mateCtx.linuxUser || undefined,
890
910
  claudeMd: "",
891
911
  model: "haiku",
892
912
  initialContext: synthesisContext,
@@ -339,6 +339,7 @@ function attachMemory(ctx) {
339
339
  var gateText = "";
340
340
  var _gateSession = null;
341
341
  sdk.createMentionSession({
342
+ linuxUser: mateCtx.linuxUser || undefined,
342
343
  claudeMd: "",
343
344
  model: "haiku",
344
345
  initialContext: gateContext,
@@ -448,6 +449,7 @@ function attachMemory(ctx) {
448
449
  var updateText = "";
449
450
  var _updateSession = null;
450
451
  sdk.createMentionSession({
452
+ linuxUser: mateCtx.linuxUser || undefined,
451
453
  claudeMd: "",
452
454
  model: "haiku",
453
455
  initialContext: updateContext,
@@ -553,6 +555,7 @@ function attachMemory(ctx) {
553
555
  var initText = "";
554
556
  var _initSession = null;
555
557
  sdk.createMentionSession({
558
+ linuxUser: mateCtx.linuxUser || undefined,
556
559
  claudeMd: "",
557
560
  model: "haiku",
558
561
  initialContext: initContext,
package/lib/project.js CHANGED
@@ -835,6 +835,7 @@ function createProjectContext(opts) {
835
835
  getNotificationsModule: function () { return _notifications; },
836
836
  mateDisplayName: opts.mateDisplayName || "",
837
837
  isMate: isMate,
838
+ osUsers: osUsers,
838
839
  dangerouslySkipPermissions: dangerouslySkipPermissions,
839
840
  mcpServers: getLocalMcpServers,
840
841
  getRemoteMcpServers: function () { return _mcp.getMcpServers(); },
@@ -1078,6 +1079,7 @@ function createProjectContext(opts) {
1078
1079
  try {
1079
1080
  var readyResult = await vendorAdapter.init({
1080
1081
  cwd: cwd,
1082
+ linuxUser: modelLinuxUser || undefined,
1081
1083
  clayPort: serverPort,
1082
1084
  clayTls: serverTls,
1083
1085
  clayAuthToken: serverAuthToken,
@@ -1686,8 +1688,8 @@ function createProjectContext(opts) {
1686
1688
  getProject: function () { return project; },
1687
1689
  // Exposed so the first websocket connection can lazily warm up the
1688
1690
  // adapters for this project (see project-connection handleConnection).
1689
- warmup: function () {
1690
- sdk.warmup();
1691
+ warmup: function (linuxUser) {
1692
+ sdk.warmup(osUsers ? linuxUser : null);
1691
1693
  sdk.startIdleReaper();
1692
1694
  if (!osUsers && !sessionTitleMigrationScheduled) {
1693
1695
  sessionTitleMigrationScheduled = true;
package/lib/public/app.js CHANGED
@@ -556,6 +556,22 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
556
556
  var newMsgBtnDefault = "\u2193 Latest";
557
557
  var newMsgBtnActivity = "\u2193 New activity";
558
558
 
559
+ // The composer grows for mobile tabs, safe areas, multiline input, and the
560
+ // keyboard. Keep the jump button immediately above its real height instead
561
+ // of relying on one fixed bottom offset.
562
+ function positionNewMsgButton() {
563
+ var inputArea = $("input-area");
564
+ if (!inputArea) return;
565
+ newMsgBtn.style.bottom = (inputArea.offsetHeight + 12) + "px";
566
+ }
567
+ var inputAreaForNewMsgBtn = $("input-area");
568
+ if (inputAreaForNewMsgBtn && window.ResizeObserver) {
569
+ new ResizeObserver(positionNewMsgButton).observe(inputAreaForNewMsgBtn);
570
+ } else {
571
+ window.addEventListener("resize", positionNewMsgButton);
572
+ }
573
+ positionNewMsgButton();
574
+
559
575
  messagesEl.addEventListener("scroll", function () {
560
576
  // While sticky-bottom is armed (e.g. just after history_done or a "New
561
577
  // activity" click), suppress "user scrolled up" detection. Growth-induced
@@ -932,6 +948,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
932
948
  messagesEl: messagesEl,
933
949
  sessionListEl: sessionListEl,
934
950
  scrollToBottom: scrollToBottom,
951
+ forceScrollToBottom: forceScrollToBottom,
935
952
  basePath: basePath,
936
953
  toggleUsagePanel: toggleUsagePanel,
937
954
  toggleStatusPanel: toggleStatusPanel,
@@ -5,12 +5,21 @@
5
5
  #messages {
6
6
  flex: 1;
7
7
  overflow-y: auto;
8
+ overflow-x: hidden;
8
9
  -webkit-overflow-scrolling: touch;
9
10
  overscroll-behavior-y: contain;
10
11
  padding: 20px 0 12px;
11
12
  position: relative;
12
13
  }
13
14
 
15
+ /* Rich message content can contain wide tables, diagrams, or generated HTML.
16
+ Keep that content inside the chat scroller; nested code blocks still own
17
+ their intentional horizontal scrolling. */
18
+ #messages > * {
19
+ min-width: 0;
20
+ max-width: 100%;
21
+ }
22
+
14
23
  /* --- Remote cursors (Figma-style) --- */
15
24
  .remote-cursor {
16
25
  pointer-events: none;
@@ -0,0 +1,17 @@
1
+ /* In an iOS Home Screen app, a 100%-tall root can be measured against the
2
+ shortened safe-area viewport. Use the physical viewport for the root while
3
+ retaining safe-area padding on controls inside the app. */
4
+ @media (max-width: 768px) {
5
+ html.pwa-standalone,
6
+ html.pwa-standalone body {
7
+ height: 100vh;
8
+ }
9
+ }
10
+
11
+ @media (max-width: 768px) and (display-mode: standalone),
12
+ (max-width: 768px) and (display-mode: fullscreen) {
13
+ html,
14
+ body {
15
+ height: 100vh;
16
+ }
17
+ }
@@ -324,6 +324,25 @@
324
324
  display: none !important;
325
325
  }
326
326
 
327
+ .title-bar-content {
328
+ height: calc(48px + var(--safe-top));
329
+ padding: var(--safe-top) 16px 0;
330
+ }
331
+
332
+ /* The inline title editor must own the header while the keyboard is open.
333
+ Leaving the other controls visible made its usable width nearly zero. */
334
+ .title-bar-content.session-renaming .status,
335
+ .title-bar-content.session-renaming #header-left > :not(.header-rename-input) {
336
+ display: none !important;
337
+ }
338
+
339
+ .title-bar-content.session-renaming .header-rename-input {
340
+ flex: 1 1 100%;
341
+ width: 100%;
342
+ min-height: 36px;
343
+ font-size: 16px;
344
+ }
345
+
327
346
  #sidebar-column {
328
347
  width: 0 !important;
329
348
  min-width: 0 !important;
@@ -30,7 +30,7 @@
30
30
  (function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
31
31
  </script>
32
32
  <script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
33
- <link rel="stylesheet" href="style.css?v=20260407">
33
+ <link rel="stylesheet" href="style.css?v=20260824f">
34
34
  <style>
35
35
  @media(max-width:768px){
36
36
  /* User messages: vertical stack, avatar on top, right-aligned */
@@ -2338,7 +2338,7 @@
2338
2338
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2339
2339
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2340
2340
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2341
- <script type="module" src="app.js"></script>
2341
+ <script type="module" src="app.js?v=20260824g"></script>
2342
2342
  <div id="pwa-install-modal" class="pwa-modal hidden">
2343
2343
  <div class="pwa-modal-backdrop"></div>
2344
2344
  <div class="pwa-modal-card">
@@ -93,6 +93,8 @@ export function initHeader() {
93
93
  input.type = "text";
94
94
  input.className = "header-rename-input";
95
95
  input.value = currentText;
96
+ var titleBar = headerTitleEl.closest(".title-bar-content");
97
+ if (titleBar) titleBar.classList.add("session-renaming");
96
98
  headerTitleEl.style.display = "none";
97
99
  headerRenameBtn.style.display = "none";
98
100
  headerTitleEl.parentNode.insertBefore(input, headerTitleEl.nextSibling);
@@ -117,6 +119,7 @@ export function initHeader() {
117
119
  }
118
120
  }
119
121
  input.remove();
122
+ if (titleBar) titleBar.classList.remove("session-renaming");
120
123
  headerTitleEl.style.display = "";
121
124
  headerRenameBtn.style.display = "";
122
125
  }
@@ -126,6 +129,7 @@ export function initHeader() {
126
129
  if (e.key === "Escape") {
127
130
  e.preventDefault();
128
131
  input.remove();
132
+ if (titleBar) titleBar.classList.remove("session-renaming");
129
133
  headerTitleEl.style.display = "";
130
134
  headerRenameBtn.style.display = "";
131
135
  }
@@ -72,6 +72,7 @@ export function initNotifications(_ctx) {
72
72
  if (window.visualViewport) {
73
73
  var layout = $("layout");
74
74
  var mobileTabBar = document.getElementById("mobile-tab-bar");
75
+ var keyboardWasOpen = false;
75
76
  // Capture initial viewport height before any keyboard interaction.
76
77
  // In iOS PWA standalone mode, window.innerHeight shrinks when the
77
78
  // keyboard opens, so we need a stable baseline for comparison.
@@ -86,7 +87,12 @@ export function initNotifications(_ctx) {
86
87
  // Toggle class so CSS can remove the tab-bar bottom padding while keyboard is up
87
88
  var keyboardOpen = vv.height < stableViewportHeight - 100;
88
89
  document.body.classList.toggle("keyboard-open", keyboardOpen);
89
- if (!keyboardOpen) ctx.scrollToBottom();
90
+ if (keyboardOpen && !keyboardWasOpen && ctx.forceScrollToBottom) {
91
+ requestAnimationFrame(function() { ctx.forceScrollToBottom(); });
92
+ } else if (!keyboardOpen) {
93
+ ctx.scrollToBottom();
94
+ }
95
+ keyboardWasOpen = keyboardOpen;
90
96
  // Hide tab bar when software keyboard is open
91
97
  if (mobileTabBar) {
92
98
  if (keyboardOpen) {
@@ -1,12 +1,12 @@
1
1
  @import url("css/base.css");
2
2
  @import url("css/icon-strip.css");
3
- @import url("css/title-bar.css");
3
+ @import url("css/title-bar.css?v=20260824");
4
4
  @import url("css/pane.css");
5
5
  @import url("css/worker-proposal.css");
6
6
  @import url("css/sidebar.css");
7
7
  @import url("css/overlays.css");
8
8
  @import url("css/menus.css");
9
- @import url("css/messages.css?v=20260407");
9
+ @import url("css/messages.css?v=20260824");
10
10
  @import url("css/rewind.css");
11
11
  @import url("css/input.css");
12
12
  @import url("css/filebrowser.css");
@@ -35,3 +35,4 @@
35
35
  @import url("css/debate.css");
36
36
  @import url("css/notifications-center.css");
37
37
  @import url("css/tui-attention.css");
38
+ @import url("css/pwa-mobile.css?v=20260824f");
package/lib/sdk-bridge.js CHANGED
@@ -110,6 +110,7 @@ function createSDKBridge(opts) {
110
110
  var adapters = opts.adapters || {};
111
111
  var mateDisplayName = opts.mateDisplayName || "";
112
112
  var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
113
+ var osUsers = !!opts.osUsers;
113
114
  var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
114
115
  // mcpServers may be either a static object or a getter function. The
115
116
  // getter form lets callers gate individual servers at call time (e.g.
@@ -323,6 +324,14 @@ function createSDKBridge(opts) {
323
324
  var mergeSkills = skills.mergeSkills;
324
325
 
325
326
  // --- Message processing (extracted to sdk-message-processor.js) ---
327
+ function getSessionLinuxUser(session) {
328
+ if (!osUsers || !session) return null;
329
+ if (session.lastLinuxUser) return session.lastLinuxUser;
330
+ if (!session.ownerId) return null;
331
+ var owner = usersModule.findUserById(session.ownerId);
332
+ return owner && owner.linuxUser ? owner.linuxUser : null;
333
+ }
334
+
326
335
  // Auto-generate a session title via YOKE adapter.generateTitle().
327
336
  // Triggered by sdk-message-processor after AUTO_TITLE_TURN_THRESHOLD turns.
328
337
  function autoGenerateTitle(session) {
@@ -345,7 +354,10 @@ function createSDKBridge(opts) {
345
354
  }
346
355
  console.log("[auto-title] Calling adapter.generateTitle with " + userMessages.length + " messages for session " + session.localId);
347
356
 
348
- sessionAdapter.generateTitle(userMessages, { cwd: cwd }).then(function(title) {
357
+ sessionAdapter.generateTitle(userMessages, {
358
+ cwd: cwd,
359
+ linuxUser: getSessionLinuxUser(session) || undefined,
360
+ }).then(function(title) {
349
361
  if (!title || title.length < 2) return;
350
362
  title = title.substring(0, 100);
351
363
  if (!session.titleManuallySet) {
@@ -1131,7 +1143,9 @@ function createSDKBridge(opts) {
1131
1143
  async function rollbackConversation(session, numTurns) {
1132
1144
  var sessionAdapter = getAdapterForSession(session);
1133
1145
  if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
1134
- await sessionAdapter.rollbackThread(session.cliSessionId, numTurns);
1146
+ await sessionAdapter.rollbackThread(session.cliSessionId, numTurns, {
1147
+ linuxUser: getSessionLinuxUser(session) || undefined,
1148
+ });
1135
1149
  }
1136
1150
  // Claude: conversation rollback is handled by rewindFiles + local history trim
1137
1151
  }
@@ -1143,7 +1157,11 @@ function createSDKBridge(opts) {
1143
1157
 
1144
1158
  async function forkSessionUnified(session, uuid) {
1145
1159
  var sessionAdapter = getAdapterForSession(session);
1146
- var result = await sessionAdapter.forkSession(session.cliSessionId, { upToMessageId: uuid, dir: cwd });
1160
+ var result = await sessionAdapter.forkSession(session.cliSessionId, {
1161
+ upToMessageId: uuid,
1162
+ dir: cwd,
1163
+ linuxUser: getSessionLinuxUser(session) || undefined,
1164
+ });
1147
1165
  if (!result || !result.sessionId) throw new Error("Fork returned no session id");
1148
1166
 
1149
1167
  // Adapters with rollbackThread (e.g. Codex) use local history copy
@@ -1471,6 +1489,7 @@ function createSDKBridge(opts) {
1471
1489
 
1472
1490
  var queryOpts = {
1473
1491
  cwd: cwd,
1492
+ linuxUser: linuxUser || undefined,
1474
1493
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1475
1494
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1476
1495
  // would strip the driver session down to the two-sentence pair preset.
@@ -2017,7 +2036,7 @@ function createSDKBridge(opts) {
2017
2036
  // Creates a mention session that can be reused across multiple mentions
2018
2037
  // within a conversation flow (session continuity).
2019
2038
  async function createMentionSession(opts) {
2020
- // opts: { vendor, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2039
+ // opts: { vendor, linuxUser, claudeMd, initialContext, initialMessage, onDelta, onDone, onError, onActivity }
2021
2040
  var abortController = new AbortController();
2022
2041
 
2023
2042
  // Current response callbacks (swapped on each pushMessage)
@@ -2037,6 +2056,7 @@ function createSDKBridge(opts) {
2037
2056
  try {
2038
2057
  handle = await mentionAdapter.createQuery({
2039
2058
  cwd: cwd,
2059
+ linuxUser: opts.linuxUser || undefined,
2040
2060
  systemPrompt: opts.claudeMd,
2041
2061
  model: opts.model || undefined,
2042
2062
  toolServers: opts.includeMcpServers ? (mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined) : undefined,
@@ -2,7 +2,13 @@
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
4
 
5
- var MEMORY_CONTRACT = "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
5
+ var MEMORY_CONTRACT =
6
+ "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
7
+ "Default to not writing. Create a note proactively only when the user explicitly asks to remember or track something, or when all of these are true: it will remain useful after the current task and session, it is not already adequately recorded in the repository or another note, and the user would likely be glad to find it on the board a week later. " +
8
+ "Good notes capture an unresolved commitment, durable product decision, user preference, constraint, or handoff that will materially change future work. " +
9
+ "Important exception for deferred defects: while doing code or technical work, actively create a sticky note when you discover a concrete defect, regression risk, security issue, or data-loss risk that is outside the current session goal and will remain unfixed when the turn ends. Do not wait for the user to ask. Include the observable evidence, affected component, likely impact, and a clear next action. Check active notes first when a duplicate is plausible. Do not create defect notes for speculation, general cleanup ideas, or problems you fixed in the current session. " +
10
+ "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
11
+ "Updates are visible too: update only when durable state materially changes, and remove a note created by your session when it stops being useful instead of turning it into a completion log. Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
6
12
 
7
13
  function getToolDefs(handlers) {
8
14
  return [
@@ -7,6 +7,7 @@ var path = require("path");
7
7
  var fs = require("fs");
8
8
  var { CodexAppServer } = require("../codex-app-server");
9
9
  var skillDiscovery = require("../skill-discovery");
10
+ var { resolveOsUserInfo } = require("../../os-users");
10
11
 
11
12
  // --- Event flattening ---
12
13
  // Converts app-server JSON-RPC notifications into flat objects with a yokeType field.
@@ -1220,6 +1221,13 @@ function createCodexAdapter(opts) {
1220
1221
  var _cwd = (opts && opts.cwd) || process.cwd();
1221
1222
  var _slug = (opts && opts.slug) || "";
1222
1223
  var _defaultInitOpts = Object.assign({}, opts || {});
1224
+ var _runtimeLinuxUser = (opts && opts.runtimeLinuxUser) || null;
1225
+ var _requiresLinuxUser = !!(opts && opts.osUsers) && !_runtimeLinuxUser;
1226
+ var _userRuntimes = Object.create(null);
1227
+ var _resolveOsUserInfo = (opts && opts.resolveOsUserInfo) || resolveOsUserInfo;
1228
+ var _createAppServer = (opts && opts.createAppServer) || function(serverOpts) {
1229
+ return new CodexAppServer(null, serverOpts);
1230
+ };
1223
1231
  // Codex models are a fixed list (the app-server doesn't enumerate them), so
1224
1232
  // model listing must not depend on a successful app-server init — otherwise a
1225
1233
  // slow/failed `initialize` leaves the picker empty and the chip shows the
@@ -1244,6 +1252,45 @@ function createCodexAdapter(opts) {
1244
1252
  var _shuttingDown = false;
1245
1253
  var _activeQueries = [];
1246
1254
 
1255
+ function withoutLinuxUser(callOpts) {
1256
+ var cleaned = Object.assign({}, callOpts || {});
1257
+ delete cleaned.linuxUser;
1258
+ return cleaned;
1259
+ }
1260
+
1261
+ function getUserRuntime(linuxUser) {
1262
+ if (!linuxUser) return null;
1263
+ if (_runtimeLinuxUser) {
1264
+ if (_runtimeLinuxUser !== linuxUser) {
1265
+ throw new Error("Codex runtime user mismatch: expected " + _runtimeLinuxUser + ", received " + linuxUser);
1266
+ }
1267
+ return adapter;
1268
+ }
1269
+ if (!_userRuntimes[linuxUser]) {
1270
+ var runtimeOpts = Object.assign({}, _defaultInitOpts, {
1271
+ runtimeLinuxUser: linuxUser,
1272
+ });
1273
+ delete runtimeOpts.linuxUser;
1274
+ _userRuntimes[linuxUser] = createCodexAdapter(runtimeOpts);
1275
+ }
1276
+ return _userRuntimes[linuxUser];
1277
+ }
1278
+
1279
+ function shutdownUserRuntimes(method, idleMs) {
1280
+ var users = Object.keys(_userRuntimes);
1281
+ if (!users.length) return Promise.resolve([]);
1282
+ return Promise.all(users.map(function(linuxUser) {
1283
+ var runtime = _userRuntimes[linuxUser];
1284
+ var result = method === "shutdownIfIdle"
1285
+ ? runtime.shutdownIfIdle(idleMs)
1286
+ : runtime.shutdown();
1287
+ return Promise.resolve(result).then(function(stopped) {
1288
+ if (method === "shutdown" || stopped) delete _userRuntimes[linuxUser];
1289
+ return stopped;
1290
+ });
1291
+ }));
1292
+ }
1293
+
1247
1294
  function updateLastActiveAt() {
1248
1295
  _lastActiveAt = Date.now();
1249
1296
  }
@@ -1396,6 +1443,16 @@ function createCodexAdapter(opts) {
1396
1443
  vendor: "codex",
1397
1444
 
1398
1445
  init: function(initOpts) {
1446
+ var requestedLinuxUser = initOpts && initOpts.linuxUser;
1447
+ if (!_runtimeLinuxUser && requestedLinuxUser) {
1448
+ return getUserRuntime(requestedLinuxUser).init(withoutLinuxUser(initOpts));
1449
+ }
1450
+ if (_requiresLinuxUser) {
1451
+ return Promise.reject(new Error("Codex requires a mapped Linux user while OS-user isolation is enabled"));
1452
+ }
1453
+ if (_runtimeLinuxUser && requestedLinuxUser && requestedLinuxUser !== _runtimeLinuxUser) {
1454
+ return Promise.reject(new Error("Codex runtime user mismatch"));
1455
+ }
1399
1456
  if (_shuttingDown) {
1400
1457
  return Promise.reject(createShutdownError());
1401
1458
  }
@@ -1412,6 +1469,9 @@ function createCodexAdapter(opts) {
1412
1469
 
1413
1470
  _initPromise = (async function() {
1414
1471
  var serverOpts = { cwd: _cwd };
1472
+ if (_runtimeLinuxUser) {
1473
+ serverOpts.osUserInfo = _resolveOsUserInfo(_runtimeLinuxUser);
1474
+ }
1415
1475
 
1416
1476
  // Extract adapter options
1417
1477
  if (effectiveInitOpts && effectiveInitOpts.adapterOptions && effectiveInitOpts.adapterOptions.CODEX) {
@@ -1478,7 +1538,7 @@ function createCodexAdapter(opts) {
1478
1538
  }
1479
1539
 
1480
1540
  // Spawn and initialize app-server
1481
- _appServer = new CodexAppServer(null, serverOpts);
1541
+ _appServer = _createAppServer(serverOpts);
1482
1542
  await _appServer.start();
1483
1543
 
1484
1544
  await _appServer.send("initialize", {
@@ -1565,6 +1625,16 @@ function createCodexAdapter(opts) {
1565
1625
  },
1566
1626
 
1567
1627
  createQuery: async function(queryOpts) {
1628
+ var requestedLinuxUser = queryOpts && queryOpts.linuxUser;
1629
+ if (!_runtimeLinuxUser && requestedLinuxUser) {
1630
+ return getUserRuntime(requestedLinuxUser).createQuery(withoutLinuxUser(queryOpts));
1631
+ }
1632
+ if (_requiresLinuxUser) {
1633
+ throw new Error("Codex requires a mapped Linux user while OS-user isolation is enabled");
1634
+ }
1635
+ if (_runtimeLinuxUser && requestedLinuxUser && requestedLinuxUser !== _runtimeLinuxUser) {
1636
+ throw new Error("Codex runtime user mismatch");
1637
+ }
1568
1638
  if (_shuttingDown) {
1569
1639
  throw createShutdownError();
1570
1640
  }
@@ -1661,6 +1731,13 @@ function createCodexAdapter(opts) {
1661
1731
 
1662
1732
  // --- Title generation ---
1663
1733
  generateTitle: async function(messages, opts) {
1734
+ var requestedLinuxUser = opts && opts.linuxUser;
1735
+ if (!_runtimeLinuxUser && requestedLinuxUser) {
1736
+ return getUserRuntime(requestedLinuxUser).generateTitle(messages, withoutLinuxUser(opts));
1737
+ }
1738
+ if (_requiresLinuxUser) {
1739
+ throw new Error("Codex requires a mapped Linux user while OS-user isolation is enabled");
1740
+ }
1664
1741
  var systemPrompt = "You are a title generator. Output only a short title (3-8 words). No quotes, no punctuation at the end, no explanation.";
1665
1742
  var prompt = "Below is a conversation between a user and an AI assistant. Generate a short, descriptive title (3-8 words) that captures the main topic. Reply with ONLY the title, nothing else.\n\n";
1666
1743
  for (var i = 0; i < messages.length; i++) {
@@ -1708,6 +1785,11 @@ function createCodexAdapter(opts) {
1708
1785
  listSessions: function() { return Promise.resolve([]); },
1709
1786
  renameSession: function() { return Promise.resolve(); },
1710
1787
  forkSession: function(threadId, opts) {
1788
+ var requestedLinuxUser = opts && opts.linuxUser;
1789
+ if (!_runtimeLinuxUser && requestedLinuxUser) {
1790
+ return getUserRuntime(requestedLinuxUser).forkSession(threadId, withoutLinuxUser(opts));
1791
+ }
1792
+ if (_requiresLinuxUser) return Promise.reject(new Error("Codex requires a mapped Linux user while OS-user isolation is enabled"));
1711
1793
  if (!_appServer || !_appServer.started) return Promise.resolve(null);
1712
1794
  return _appServer.send("thread/fork", { threadId: threadId }, 30000).then(function(result) {
1713
1795
  var newThreadId = (result && result.thread) ? result.thread.id : null;
@@ -1715,17 +1797,30 @@ function createCodexAdapter(opts) {
1715
1797
  return { sessionId: newThreadId };
1716
1798
  });
1717
1799
  },
1718
- rollbackThread: function(threadId, numTurns) {
1800
+ rollbackThread: function(threadId, numTurns, opts) {
1801
+ var requestedLinuxUser = opts && opts.linuxUser;
1802
+ if (!_runtimeLinuxUser && requestedLinuxUser) {
1803
+ return getUserRuntime(requestedLinuxUser).rollbackThread(threadId, numTurns, withoutLinuxUser(opts));
1804
+ }
1805
+ if (_requiresLinuxUser) return Promise.reject(new Error("Codex requires a mapped Linux user while OS-user isolation is enabled"));
1719
1806
  if (!_appServer || !_appServer.started) return Promise.resolve(null);
1720
1807
  return _appServer.send("thread/rollback", { threadId: threadId, numTurns: numTurns }, 30000);
1721
1808
  },
1722
1809
 
1723
1810
  // Shutdown the app-server process
1724
1811
  shutdown: function() {
1725
- return beginShutdown(true);
1812
+ return Promise.all([
1813
+ beginShutdown(true),
1814
+ shutdownUserRuntimes("shutdown"),
1815
+ ]).then(function() { return true; });
1726
1816
  },
1727
1817
 
1728
1818
  shutdownIfIdle: function(idleMs) {
1819
+ if (!_runtimeLinuxUser && Object.keys(_userRuntimes).length > 0) {
1820
+ return shutdownUserRuntimes("shutdownIfIdle", idleMs).then(function(results) {
1821
+ return results.some(function(stopped) { return !!stopped; });
1822
+ });
1823
+ }
1729
1824
  if (_shuttingDown || _shutdownPromise) return Promise.resolve(false);
1730
1825
  if (_initPromise) return Promise.resolve(false);
1731
1826
  if (!_appServer) return Promise.resolve(false);
@@ -9,6 +9,8 @@ var readline = require("readline");
9
9
  var path = require("path");
10
10
  var fs = require("fs");
11
11
  var { createRequire } = require("module");
12
+ var { buildUserEnv } = require("../build-user-env");
13
+ var { wrapSpawnAsUser } = require("../os-users");
12
14
 
13
15
  // --- Find the codex binary path ---
14
16
  // Mirrors the logic from @openai/codex-sdk findCodexPath()
@@ -120,14 +122,30 @@ function CodexAppServer(executablePath, opts) {
120
122
  this._stderrBuf = "";
121
123
  }
122
124
 
125
+ function buildSpawnSpec(executablePath, args, opts, wrapFn) {
126
+ opts = opts || {};
127
+ var osUserInfo = opts.osUserInfo || null;
128
+ var env = osUserInfo
129
+ ? Object.assign(buildUserEnv(osUserInfo), opts.env || {})
130
+ : Object.assign({}, process.env, opts.env || {});
131
+ var spawnOptions = {
132
+ stdio: ["pipe", "pipe", "pipe"],
133
+ env: env,
134
+ cwd: opts.cwd || process.cwd(),
135
+ };
136
+ if (osUserInfo) {
137
+ spawnOptions.uid = osUserInfo.uid;
138
+ spawnOptions.gid = osUserInfo.gid;
139
+ }
140
+ return (wrapFn || wrapSpawnAsUser)(executablePath, args, spawnOptions);
141
+ }
142
+
123
143
  CodexAppServer.prototype.start = function() {
124
144
  var self = this;
125
145
 
126
146
  return new Promise(function(resolve, reject) {
127
147
  try {
128
148
  var args = ["app-server"];
129
- var env = Object.assign({}, process.env, self.opts.env || {});
130
-
131
149
  // Pass config overrides via --config key=value flags
132
150
  if (self.opts.config) {
133
151
  var configArgs = serializeConfig(self.opts.config, "");
@@ -136,13 +154,11 @@ CodexAppServer.prototype.start = function() {
136
154
  }
137
155
  }
138
156
 
139
- console.log("[codex-app-server] Spawning:", self.executablePath, args.join(" "));
157
+ var spawnSpec = buildSpawnSpec(self.executablePath, args, self.opts);
158
+ var userSuffix = self.opts.osUserInfo ? " as " + self.opts.osUserInfo.user : "";
159
+ console.log("[codex-app-server] Spawning" + userSuffix + ":", spawnSpec.command, spawnSpec.args.join(" "));
140
160
 
141
- self.proc = spawn(self.executablePath, args, {
142
- stdio: ["pipe", "pipe", "pipe"],
143
- env: env,
144
- cwd: self.opts.cwd || process.cwd(),
145
- });
161
+ self.proc = spawn(spawnSpec.command, spawnSpec.args, spawnSpec.options);
146
162
 
147
163
  self.proc.on("error", function(err) {
148
164
  console.error("[codex-app-server] Process error:", err.message);
@@ -383,4 +399,5 @@ CodexAppServer.prototype.stop = function() {
383
399
  module.exports = {
384
400
  CodexAppServer: CodexAppServer,
385
401
  findCodexPath: findCodexPath,
402
+ buildSpawnSpec: buildSpawnSpec,
386
403
  };
package/lib/yoke/index.js CHANGED
@@ -350,7 +350,7 @@ function createAdapters(opts) {
350
350
 
351
351
  if (installed.codex && supportsConfiguredIsolation("codex")) {
352
352
  try {
353
- adapters.codex = createAdapter({ vendor: "codex", cwd: opts.cwd, slug: opts.slug });
353
+ adapters.codex = createAdapter({ vendor: "codex", cwd: opts.cwd, slug: opts.slug, osUsers: !!opts.osUsers });
354
354
  auth.codex = true;
355
355
  console.log("[yoke] Adapter created: codex");
356
356
  } catch (e) {
@@ -411,7 +411,12 @@ async function lazyCreateAdapter(adapters, vendor, opts) {
411
411
  if (!installed[vendor]) return null;
412
412
 
413
413
  try {
414
- var ad = createAdapter({ vendor: vendor, cwd: opts.cwd, slug: opts.slug });
414
+ var ad = createAdapter({
415
+ vendor: vendor,
416
+ cwd: opts.cwd,
417
+ slug: opts.slug,
418
+ osUsers: !!(opts.osUsers || opts.linuxUser),
419
+ });
415
420
  if (typeof ad.init === "function") {
416
421
  await ad.init(opts || {});
417
422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.4.0-beta.3",
3
+ "version": "3.4.0-beta.5",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",