clay-server 2.34.0 → 2.34.1-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.
@@ -60,6 +60,35 @@ function attachConnection(ctx) {
60
60
  // warmup once per reconnect.
61
61
  var _warmedUp = false;
62
62
 
63
+ function findRestoredActiveSession(ws, wsUser, allSessions) {
64
+ var active = null;
65
+ var presenceKey = wsUser ? wsUser.id : "_default";
66
+ var storedPresence = userPresence.getPresence(slug, presenceKey);
67
+ if (storedPresence && storedPresence.sessionId) {
68
+ if (sm.sessions.has(storedPresence.sessionId)) {
69
+ active = sm.sessions.get(storedPresence.sessionId);
70
+ } else {
71
+ sm.sessions.forEach(function (s) {
72
+ if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
73
+ });
74
+ }
75
+ if (active && usersModule.isMultiUser() && wsUser) {
76
+ if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
77
+ } else if (active && !usersModule.isMultiUser() && active.ownerId) {
78
+ active = null;
79
+ }
80
+ }
81
+ if (!active && allSessions.length > 0) {
82
+ active = allSessions[0];
83
+ for (var fi = 1; fi < allSessions.length; fi++) {
84
+ if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
85
+ active = allSessions[fi];
86
+ }
87
+ }
88
+ }
89
+ return { active: active, storedPresence: storedPresence };
90
+ }
91
+
63
92
  function handleConnection(ws, wsUser, handleMessage, handleDisconnection) {
64
93
  ws._clayUser = wsUser || null;
65
94
  clients.add(ws);
@@ -90,6 +119,18 @@ function attachConnection(ctx) {
90
119
  var title = getTitle();
91
120
  var project = getProject();
92
121
  var ownerLocked = !!(osUsers && osUsers.length > 0 && /^\/home\/[^/]+\//.test(cwd));
122
+ var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
123
+ if (usersModule.isMultiUser() && wsUser) {
124
+ allSessions = allSessions.filter(function (s) {
125
+ return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
126
+ });
127
+ } else if (!usersModule.isMultiUser()) {
128
+ allSessions = allSessions.filter(function (s) { return !s.ownerId; });
129
+ }
130
+ var restoredState = findRestoredActiveSession(ws, wsUser, allSessions);
131
+ var restoredActive = restoredState.active;
132
+ var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
133
+ var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
93
134
  sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
94
135
  // Update notifications are pushed on a scheduled interval (see
95
136
  // scheduleUpdateBroadcast). We no longer push on connect to avoid
@@ -98,8 +139,7 @@ function attachConnection(ctx) {
98
139
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
99
140
  }
100
141
  if (sm.currentModel) {
101
- // Vendor is resolved per-session in session_switched; send default here
102
- sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [], vendor: sm.defaultVendor || "claude", availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
142
+ sendTo(ws, { type: "model_info", model: sm.currentModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
103
143
  }
104
144
  sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
105
145
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
@@ -116,14 +156,6 @@ function attachConnection(ctx) {
116
156
  if (_notifications) _notifications.sendConnectionState(ws, sendTo);
117
157
 
118
158
  // Session list (filtered for access control)
119
- var allSessions = [].concat(Array.from(sm.sessions.values())).filter(function (s) { return !s.hidden; });
120
- if (usersModule.isMultiUser() && wsUser) {
121
- allSessions = allSessions.filter(function (s) {
122
- return usersModule.canAccessSession(wsUser.id, s, { visibility: "public" });
123
- });
124
- } else if (!usersModule.isMultiUser()) {
125
- allSessions = allSessions.filter(function (s) { return !s.ownerId; });
126
- }
127
159
  sendTo(ws, {
128
160
  type: "session_list",
129
161
  sessions: allSessions.map(function (s) {
@@ -152,31 +184,9 @@ function attachConnection(ctx) {
152
184
  });
153
185
 
154
186
  // Restore active session for this client from server-side presence
155
- var active = null;
187
+ var active = restoredState.active;
156
188
  var presenceKey = wsUser ? wsUser.id : "_default";
157
- var storedPresence = userPresence.getPresence(slug, presenceKey);
158
- if (storedPresence && storedPresence.sessionId) {
159
- if (sm.sessions.has(storedPresence.sessionId)) {
160
- active = sm.sessions.get(storedPresence.sessionId);
161
- } else {
162
- sm.sessions.forEach(function (s) {
163
- if (s.cliSessionId && s.cliSessionId === storedPresence.sessionId) active = s;
164
- });
165
- }
166
- if (active && usersModule.isMultiUser() && wsUser) {
167
- if (!usersModule.canAccessSession(wsUser.id, active, { visibility: "public" })) active = null;
168
- } else if (active && !usersModule.isMultiUser() && active.ownerId) {
169
- active = null;
170
- }
171
- }
172
- if (!active && allSessions.length > 0) {
173
- active = allSessions[0];
174
- for (var fi = 1; fi < allSessions.length; fi++) {
175
- if ((allSessions[fi].lastActivity || 0) > (active.lastActivity || 0)) {
176
- active = allSessions[fi];
177
- }
178
- }
179
- }
189
+ var storedPresence = restoredState.storedPresence;
180
190
  var autoCreated = false;
181
191
  if (!active) {
182
192
  var autoOpts = {};
@@ -227,6 +227,24 @@ function attachSessions(ctx) {
227
227
  if (msg.type === "resume_session") {
228
228
  if (!msg.cliSessionId) return true;
229
229
  var cliSess = require("./cli-sessions");
230
+
231
+ // If Clay already has a persisted meta file for this cliSessionId, read
232
+ // its vendor so resumeSession doesn't silently default to the project's
233
+ // primary vendor (which would break codex sessions after server restart).
234
+ var persistedVendor = null;
235
+ try {
236
+ var _fsResume = require("fs");
237
+ var _pathResume = require("path");
238
+ var metaPath = _pathResume.join(sm.sessionsDir, msg.cliSessionId + ".jsonl");
239
+ if (_fsResume.existsSync(metaPath)) {
240
+ var firstLine = _fsResume.readFileSync(metaPath, "utf8").split("\n", 1)[0];
241
+ try {
242
+ var metaObj = JSON.parse(firstLine);
243
+ if (metaObj && metaObj.type === "meta" && metaObj.vendor) persistedVendor = metaObj.vendor;
244
+ } catch (e) {}
245
+ }
246
+ } catch (e) {}
247
+
230
248
  // Try SDK for title first, then fall back to manual parsing
231
249
  var titlePromise = adapter.getSessionInfo(msg.cliSessionId, { dir: cwd }).then(function(info) {
232
250
  return (info && info.summary) ? info.summary.substring(0, 100) : null;
@@ -247,10 +265,10 @@ function attachSessions(ctx) {
247
265
  }
248
266
  }
249
267
  }
250
- var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title }, ws);
268
+ var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title, vendor: persistedVendor || undefined }, ws);
251
269
  if (resumed) ws._clayActiveSession = resumed.localId;
252
270
  }).catch(function() {
253
- var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
271
+ var resumed = sm.resumeSession(msg.cliSessionId, persistedVendor ? { vendor: persistedVendor } : undefined, ws);
254
272
  if (resumed) ws._clayActiveSession = resumed.localId;
255
273
  });
256
274
  return true;
@@ -311,7 +329,13 @@ function attachSessions(ctx) {
311
329
  if (switchTargetSess && sm.currentModel) {
312
330
  var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
313
331
  var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
314
- if (tvModels.length > 0 && tvModels.indexOf(sm.currentModel) === -1) {
332
+ var found = false;
333
+ for (var tvi = 0; tvi < tvModels.length; tvi++) {
334
+ var tvEntry = tvModels[tvi];
335
+ var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
336
+ if (tvVal === sm.currentModel) { found = true; break; }
337
+ }
338
+ if (tvModels.length > 0 && !found) {
315
339
  sm.currentModel = "";
316
340
  }
317
341
  }
@@ -507,14 +531,24 @@ function attachSessions(ctx) {
507
531
  if (msg.type === "set_vendor" && msg.vendor) {
508
532
  var vendorSession = getSessionForWs(ws);
509
533
  if (vendorSession) {
510
- vendorSession.vendor = msg.vendor;
511
- // Clear the shared model so the next query uses the vendor's default
512
- // instead of leaking the previous vendor's model into a fresh session.
513
- if (sm.currentModel) {
514
- sm.currentModel = "";
534
+ // Refuse to rebind vendor on a session that is already bound to a
535
+ // different CLI (cliSessionId is vendor-specific). This prevents a
536
+ // stale client-side vendor state from clobbering the persisted vendor
537
+ // on page reload / server restart.
538
+ var alreadyBound = vendorSession.cliSessionId && vendorSession.vendor && vendorSession.vendor !== msg.vendor;
539
+ if (alreadyBound) {
540
+ console.warn("[project] set_vendor ignored: session " + vendorSession.localId +
541
+ " is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
542
+ } else {
543
+ vendorSession.vendor = msg.vendor;
544
+ // Clear the shared model so the next query uses the vendor's default
545
+ // instead of leaking the previous vendor's model into a fresh session.
546
+ if (sm.currentModel) {
547
+ sm.currentModel = "";
548
+ }
549
+ sm.saveSessionFile(vendorSession);
550
+ sm.broadcastSessionList();
515
551
  }
516
- sm.saveSessionFile(vendorSession);
517
- sm.broadcastSessionList();
518
552
  }
519
553
  if (msg.vendor) {
520
554
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[msg.vendor]) || [];
@@ -1257,10 +1257,18 @@ export function renderIconStrip(projects, currentSlug) {
1257
1257
  wtEl.href = "/p/" + wt.slug + "/";
1258
1258
  wtEl.dataset.slug = wt.slug;
1259
1259
 
1260
- var abbrev = document.createElement("span");
1261
- abbrev.className = "wt-branch-abbrev";
1262
- abbrev.textContent = getProjectAbbrev(wt.name);
1263
- wtEl.appendChild(abbrev);
1260
+ if (wt.icon) {
1261
+ var wtEmoji = document.createElement("span");
1262
+ wtEmoji.className = "wt-branch-abbrev project-emoji";
1263
+ wtEmoji.textContent = wt.icon;
1264
+ parseEmojis(wtEmoji);
1265
+ wtEl.appendChild(wtEmoji);
1266
+ } else {
1267
+ var abbrev = document.createElement("span");
1268
+ abbrev.className = "wt-branch-abbrev";
1269
+ abbrev.textContent = getProjectAbbrev(wt.name);
1270
+ wtEl.appendChild(abbrev);
1271
+ }
1264
1272
 
1265
1273
  var wtStatus = document.createElement("span");
1266
1274
  wtStatus.className = "icon-strip-status";
package/lib/sdk-bridge.js CHANGED
@@ -182,6 +182,22 @@ function createSDKBridge(opts) {
182
182
  return sm.availableModels || [];
183
183
  }
184
184
 
185
+ // Model list entries may be plain strings (Codex) or { value, displayName }
186
+ // objects (Claude SDK). Normalize to the identifier string.
187
+ function modelEntryValue(entry) {
188
+ if (!entry) return "";
189
+ if (typeof entry === "string") return entry;
190
+ return entry.value || entry.id || "";
191
+ }
192
+
193
+ function modelListContains(list, modelId) {
194
+ if (!list || !modelId) return false;
195
+ for (var mi = 0; mi < list.length; mi++) {
196
+ if (modelEntryValue(list[mi]) === modelId) return true;
197
+ }
198
+ return false;
199
+ }
200
+
185
201
  function sendModelInfoForVendor(vendor, model) {
186
202
  send({
187
203
  type: "model_info",
@@ -1221,10 +1237,15 @@ function createSDKBridge(opts) {
1221
1237
  var sessionVendor = session.vendor || (adapter && adapter.vendor) || null;
1222
1238
  if (sessionVendor) {
1223
1239
  var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[sessionVendor]) || [];
1224
- if (vendorModels.length > 0 && queryModel && vendorModels.indexOf(queryModel) === -1) {
1225
- queryModel = vendorModels[0];
1240
+ if (vendorModels.length > 0 && queryModel && !modelListContains(vendorModels, queryModel)) {
1241
+ queryModel = modelEntryValue(vendorModels[0]);
1226
1242
  }
1227
1243
  }
1244
+ // Guard against anything upstream having set queryModel to an object
1245
+ // (e.g. a cached ModelInfo leaked through). Always coerce to string id.
1246
+ if (queryModel && typeof queryModel !== "string") {
1247
+ queryModel = modelEntryValue(queryModel) || undefined;
1248
+ }
1228
1249
 
1229
1250
  var codexConfig = getCodexConfig(sm);
1230
1251
  var mergedMcpServers = mergeMcpServers(getMcpServers(), getRemoteMcpServers) || undefined;
@@ -1480,6 +1501,10 @@ function createSDKBridge(opts) {
1480
1501
  }
1481
1502
 
1482
1503
  async function setModel(session, model) {
1504
+ // Normalize to string id in case a { value, displayName } object slips in
1505
+ if (model && typeof model !== "string") {
1506
+ model = modelEntryValue(model);
1507
+ }
1483
1508
  if (!session.queryInstance) {
1484
1509
  // No active query — just store the model for next startQuery
1485
1510
  sm.currentModel = model;
package/lib/sessions.js CHANGED
@@ -661,6 +661,8 @@ function createSessionManager(opts) {
661
661
  bookmarked: false,
662
662
  favoriteOrder: null,
663
663
  };
664
+ if (opts && opts.vendor) session.vendor = opts.vendor;
665
+ if (opts && opts.ownerId) session.ownerId = opts.ownerId;
664
666
  sessions.set(localId, session);
665
667
  saveSessionFile(session);
666
668
  switchSession(localId, targetWs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.34.0",
3
+ "version": "2.34.1-beta.1",
4
4
  "description": "Self-hosted Claude Code in your browser. Multi-session, multi-user, push notifications.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",