clay-server 4.0.0-beta.19 → 4.0.0-beta.20

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.
@@ -78,7 +78,7 @@ function attachSessionHandoff(ctx) {
78
78
  ) || null;
79
79
  var target = sm.createSessionRaw({
80
80
  ownerId: source.ownerId || null,
81
- sessionVisibility: source.sessionVisibility || "shared",
81
+ sessionVisibility: "private",
82
82
  vendor: targetVendor,
83
83
  model: targetModel || null,
84
84
  effort: targetEffort,
@@ -198,7 +198,7 @@ function attachSessionSpawn(ctx) {
198
198
  var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
199
199
  var session = create.call(sm, {
200
200
  ownerId: parent.ownerId || null,
201
- sessionVisibility: parent.sessionVisibility || "shared",
201
+ sessionVisibility: "private",
202
202
  vendor: vendor,
203
203
  });
204
204
  session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
@@ -5,6 +5,7 @@ var { execFileSync } = require("child_process");
5
5
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
6
6
  var updateSnooze = require("./update-snooze");
7
7
  var yoke = require("./yoke");
8
+ var sessionVisibility = require("./session-visibility");
8
9
 
9
10
  function vendorSupportsTui(vendor) {
10
11
  var info = yoke.getVendorInfo(vendor);
@@ -496,7 +497,7 @@ function attachSessions(ctx) {
496
497
  sm.sweepBlankSessions();
497
498
  var sessionOpts = {};
498
499
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
499
- if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
500
+ sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
500
501
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
501
502
  // Mode resolution: vendors without a TUI session mode are always GUI.
502
503
  // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
@@ -581,7 +582,8 @@ function attachSessions(ctx) {
581
582
  if (reusable) {
582
583
  if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
583
584
  if (!reusable.effort) reusable.effort = sessionOpts.effort;
584
- if (msg.sessionVisibility) reusable.sessionVisibility = msg.sessionVisibility;
585
+ sm.setSessionVisibility(reusable.localId, sessionOpts.sessionVisibility);
586
+ sessionVisibility.revokeViewers(ctx, reusable);
585
587
  sm.switchSession(reusable.localId, ws);
586
588
  newSess = reusable;
587
589
  } else {
@@ -609,9 +611,7 @@ function attachSessions(ctx) {
609
611
  }
610
612
 
611
613
  if (msg.type === "set_session_visibility") {
612
- if (typeof msg.sessionId === "number" && (msg.visibility === "shared" || msg.visibility === "private")) {
613
- sm.setSessionVisibility(msg.sessionId, msg.visibility);
614
- }
614
+ sessionVisibility.handleChange(ctx, ws, msg);
615
615
  return true;
616
616
  }
617
617
 
@@ -697,7 +697,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
697
697
 
698
698
  // Session visibility toggle (only the session owner can change)
699
699
  if (store.get('isMultiUserMode') && sessionData && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
700
- var currentVis = (sessionData && sessionData.sessionVisibility) || "shared";
700
+ var currentVis = (sessionData && sessionData.sessionVisibility) || "private";
701
701
  var isPrivate = currentVis === "private";
702
702
  var visItem = document.createElement("button");
703
703
  visItem.className = "session-ctx-item";
@@ -1290,7 +1290,7 @@ function renderSessionItem(s, options) {
1290
1290
  if (s.loop && s.loop.source === "debate") {
1291
1291
  textHtml += '<span class="session-debate-icon" title="Debate">' + iconHtml("mic") + '</span>';
1292
1292
  }
1293
- if (store.get('isMultiUserMode') && s.sessionVisibility === "private") {
1293
+ if (store.get('isMultiUserMode') && s.sessionVisibility !== "shared") {
1294
1294
  textHtml += '<span class="session-private-icon" title="Private session">' + iconHtml("lock") + '</span>';
1295
1295
  }
1296
1296
  textHtml += highlightMatch(s.title || "New Session", searchQuery);
@@ -0,0 +1,39 @@
1
+ function normalize(value) {
2
+ return value === "shared" ? "shared" : "private";
3
+ }
4
+
5
+ function restore(meta) {
6
+ // Older releases persisted the shared default without recording intent.
7
+ return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
8
+ }
9
+
10
+ function handleChange(ctx, ws, msg) {
11
+ if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
12
+ var session = ctx.sm.sessions.get(msg.sessionId);
13
+ if (!session) return;
14
+ if (ctx.usersModule.isMultiUser()) {
15
+ var user = ws._clayUser;
16
+ var project = ctx.getProjectAccess ? ctx.getProjectAccess() : { visibility: "public" };
17
+ if (!user || !session.ownerId || session.ownerId !== user.id ||
18
+ !ctx.usersModule.canAccessSession(user.id, session, project)) return;
19
+ } else if (session.ownerId) {
20
+ return;
21
+ }
22
+ ctx.sm.setSessionVisibility(msg.sessionId, msg.visibility);
23
+ revokeViewers(ctx, session);
24
+ }
25
+
26
+ function revokeViewers(ctx, session) {
27
+ if (ctx.usersModule.isMultiUser() && session.sessionVisibility === "private") {
28
+ ctx.clients.forEach(function(client) {
29
+ if (client._clayActiveSession !== session.localId) return;
30
+ if (client._clayUser && client._clayUser.id === session.ownerId) return;
31
+ // Clear the server-side selection before reconnecting so no further
32
+ // session events or input can cross the revoked access boundary.
33
+ client._clayActiveSession = null;
34
+ if (client.readyState === 1) client.close(1008, "Session access changed");
35
+ });
36
+ }
37
+ }
38
+
39
+ module.exports = { normalize: normalize, restore: restore, handleChange: handleChange, revokeViewers: revokeViewers };
package/lib/sessions.js CHANGED
@@ -7,6 +7,7 @@ var { CODEX_DEFAULTS } = require("./codex-defaults");
7
7
  var hygiene = require("./session-hygiene");
8
8
  var backgroundTaskTiming = require("./background-task-timing");
9
9
  var sessionProvenance = require("./session-provenance");
10
+ var sessionVisibility = require("./session-visibility");
10
11
 
11
12
  function createSessionManager(opts) {
12
13
  var cwd = opts.cwd;
@@ -166,7 +167,8 @@ function createSessionManager(opts) {
166
167
  if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
167
168
  if (session.permissionMode) metaObj.permissionMode = session.permissionMode;
168
169
  if (session.permissionModeBeforeFullAccess) metaObj.permissionModeBeforeFullAccess = session.permissionModeBeforeFullAccess;
169
- if (session.sessionVisibility) metaObj.sessionVisibility = session.sessionVisibility;
170
+ metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
171
+ if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
170
172
  if (session.bookmarked) metaObj.bookmarked = true;
171
173
  if (typeof session.favoriteOrder === "number") metaObj.favoriteOrder = session.favoriteOrder;
172
174
  if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
@@ -321,7 +323,8 @@ function createSessionManager(opts) {
321
323
  session.terminalId = null;
322
324
  session.runtimeMode = null;
323
325
  session.runtimeTerminalId = null;
324
- session.sessionVisibility = m.sessionVisibility || "shared";
326
+ session.sessionVisibility = sessionVisibility.restore(m);
327
+ session.sessionVisibilityExplicit = m.sessionVisibilityExplicit === true;
325
328
  session.bookmarked = !!m.bookmarked;
326
329
  session.favoriteOrder = typeof m.favoriteOrder === "number" ? m.favoriteOrder : null;
327
330
  sessions.set(localId, session);
@@ -447,7 +450,7 @@ function createSessionManager(opts) {
447
450
  terminalId: null,
448
451
  runtimeMode: null,
449
452
  runtimeTerminalId: null,
450
- sessionVisibility: "shared",
453
+ sessionVisibility: "private",
451
454
  bookmarked: false,
452
455
  favoriteOrder: null,
453
456
  };
@@ -536,7 +539,7 @@ function createSessionManager(opts) {
536
539
  loop: loop,
537
540
  spawn: s.spawn || null,
538
541
  ownerId: s.ownerId || null,
539
- sessionVisibility: s.sessionVisibility || "shared",
542
+ sessionVisibility: sessionVisibility.normalize(s.sessionVisibility),
540
543
  bookmarked: !!s.bookmarked,
541
544
  favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
542
545
  unread: unreadMap[s.localId] || 0,
@@ -620,7 +623,8 @@ function createSessionManager(opts) {
620
623
  history: [],
621
624
  messageUUIDs: [],
622
625
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
623
- sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
626
+ sessionVisibility: sessionVisibility.normalize(sessionOpts && sessionOpts.sessionVisibility),
627
+ sessionVisibilityExplicit: !!(sessionOpts && sessionOpts.sessionVisibility === "shared"),
624
628
  bookmarked: false,
625
629
  favoriteOrder: null,
626
630
  vendor: (sessionOpts && sessionOpts.vendor) || null,
@@ -661,7 +665,8 @@ function createSessionManager(opts) {
661
665
  history: [],
662
666
  messageUUIDs: [],
663
667
  ownerId: (sessionOpts && sessionOpts.ownerId) || null,
664
- sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility) || "shared",
668
+ sessionVisibility: sessionVisibility.normalize(sessionOpts && sessionOpts.sessionVisibility),
669
+ sessionVisibilityExplicit: !!(sessionOpts && sessionOpts.sessionVisibility === "shared"),
665
670
  bookmarked: false,
666
671
  favoriteOrder: null,
667
672
  vendor: (sessionOpts && sessionOpts.vendor) || null,
@@ -916,7 +921,8 @@ function createSessionManager(opts) {
916
921
  // Send to active clients without recording to history/disk (ephemeral data)
917
922
  if (sendEach) {
918
923
  var data = JSON.stringify(obj);
919
- sendEach(function (ws) {
924
+ sendEach(function (ws, filterFn) {
925
+ if (filterFn && !filterFn(session)) return;
920
926
  if (session.homeDebatePlanning === true) return;
921
927
  if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
922
928
  ws.send(data);
@@ -947,7 +953,8 @@ function createSessionManager(opts) {
947
953
  // Multi-user: send to clients whose active session matches this one
948
954
  var data = JSON.stringify(outgoingObj);
949
955
  var ioData = null;
950
- sendEach(function (ws) {
956
+ sendEach(function (ws, filterFn) {
957
+ if (filterFn && !filterFn(session)) return;
951
958
  if (session.homeDebatePlanning === true) return;
952
959
  if (ws._clayActiveSession === session.localId) {
953
960
  if (ws.readyState === 1) ws.send(data);
@@ -1290,7 +1297,9 @@ function createSessionManager(opts) {
1290
1297
  setSessionVisibility: function (localId, visibility) {
1291
1298
  var session = sessions.get(localId);
1292
1299
  if (!session) return { error: "Session not found" };
1300
+ if (visibility !== "shared" && visibility !== "private") return { error: "Invalid session visibility" };
1293
1301
  session.sessionVisibility = visibility;
1302
+ session.sessionVisibilityExplicit = true;
1294
1303
  saveSessionFile(session);
1295
1304
  broadcastSessionList();
1296
1305
  return { ok: true };
@@ -96,8 +96,8 @@ function attachPermissions(deps) {
96
96
  }
97
97
  // Owner can always see their own sessions
98
98
  if (session.ownerId === userId) return true;
99
- // Shared sessions are visible to all project members (default)
100
- if (!session.sessionVisibility || session.sessionVisibility === "shared") return true;
99
+ // Sharing is opt-in; missing or invalid visibility stays private.
100
+ if (session.sessionVisibility === "shared") return true;
101
101
  // Private sessions are only visible to the owner
102
102
  return false;
103
103
  }
@@ -424,22 +424,32 @@ async function handleQueryStart(msg) {
424
424
  function perf(label) { console.log("[PERF] sdk-worker: " + label + " +" + (Date.now() - t0) + "ms (local +" + (Date.now() - localT0) + "ms)"); }
425
425
  perf("handleQueryStart entered");
426
426
 
427
+ // Socket messages keep arriving while the SDK import awaits. Accept input
428
+ // and cancellation synchronously, before yielding to the next IPC message.
429
+ messageQueue = createMessageQueue();
430
+ abortController = new AbortController();
431
+ if (msg.prompt) messageQueue.push(msg.prompt);
432
+
427
433
  var sdk;
428
434
  try {
429
435
  perf("loading SDK");
430
436
  sdk = await getSDK();
431
437
  perf("SDK loaded");
432
438
  } catch (e) {
439
+ messageQueue.end();
440
+ messageQueue = null;
441
+ abortController = null;
442
+ sdkModule = null;
433
443
  sendToDaemon({ type: "query_error", error: "Failed to load SDK: " + (e.message || e), exitCode: null, stderr: null });
434
444
  return;
435
445
  }
436
446
 
437
- messageQueue = createMessageQueue();
438
- abortController = new AbortController();
439
-
440
- // Push the initial user message
441
- if (msg.prompt) {
442
- messageQueue.push(msg.prompt);
447
+ if (abortController.signal.aborted) {
448
+ messageQueue.end();
449
+ messageQueue = null;
450
+ abortController = null;
451
+ sendToDaemon({ type: "query_done" });
452
+ return;
443
453
  }
444
454
 
445
455
  // Build query options (callbacks are local, everything else from daemon)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.0.0-beta.19",
3
+ "version": "4.0.0-beta.20",
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",