clay-server 4.3.0-beta.1 → 4.3.0-beta.2

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.
@@ -273,7 +273,7 @@ function attachConnection(ctx) {
273
273
  }
274
274
  var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
275
275
  var activePermissionState = permissionModes.clientState(active, sm.currentPermissionMode, sm.defaultVendor);
276
- sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: activePermissionState.requestedPermissionMode, effectivePermissionMode: activePermissionState.effectivePermissionMode, permissionCapabilities: activePermissionState.permissionCapabilities, mcpPermissionModeOverrides: activePermissionState.mcpPermissionModeOverrides });
276
+ sendTo(ws, { type: "session_switched", id: active.localId, sessionOriginId: active.sessionOriginId || null, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended, permissionMode: activePermissionState.requestedPermissionMode, effectivePermissionMode: activePermissionState.effectivePermissionMode, permissionCapabilities: activePermissionState.permissionCapabilities, mcpPermissionModeOverrides: activePermissionState.mcpPermissionModeOverrides });
277
277
  if (ctx.autonomousRun) sendTo(ws, { type: "autonomous_run_state", sessionId: active.localId, run: ctx.autonomousRun.project(active.autonomousRun) });
278
278
  // Send per-session context sources
279
279
  var sessionSources = loadContextSources(slug, active.localId);
@@ -15,6 +15,25 @@ function deliveryKey(ownerId, clientMessageId) {
15
15
  return ownerId + ":" + clientMessageId;
16
16
  }
17
17
 
18
+ function validateContext(ws, session, msg, projectSlug) {
19
+ if (!msg || !msg.clientMessageId) return { ok: true };
20
+ if (msg.projectSlug == null && msg.sessionId == null && msg.accountId == null && msg.cliSessionId == null && msg.sessionOriginId == null) return { ok: true };
21
+ var expectedCliSessionId = session && session.cliSessionId ? String(session.cliSessionId) : null;
22
+ var actualCliSessionId = msg.cliSessionId ? String(msg.cliSessionId) : null;
23
+ var expectedSessionId = session && session.localId != null ? String(session.localId) : null;
24
+ var expectedOriginId = session && session.sessionOriginId ? String(session.sessionOriginId) : null;
25
+ var actualOriginId = msg.sessionOriginId ? String(msg.sessionOriginId) : null;
26
+ if (msg.projectSlug !== projectSlug) return { ok: false, reason: "Project changed" };
27
+ if (msg.accountId !== ownerKey(ws)) return { ok: false, reason: "Account changed" };
28
+ if (expectedOriginId || actualOriginId) {
29
+ if (!expectedOriginId || actualOriginId !== expectedOriginId) return { ok: false, reason: "Session identity changed" };
30
+ } else {
31
+ if (msg.sessionId == null || String(msg.sessionId) !== expectedSessionId) return { ok: false, reason: "Session changed" };
32
+ if (actualCliSessionId && expectedCliSessionId && actualCliSessionId !== expectedCliSessionId) return { ok: false, reason: "Session identity changed" };
33
+ }
34
+ return { ok: true };
35
+ }
36
+
18
37
  function ensureRecordedMessages(session) {
19
38
  if (session._recordedClientMessages) return session._recordedClientMessages;
20
39
  var recorded = new Map();
@@ -60,6 +79,7 @@ function createProjectMessageDelivery(sendTo, projectSlug) {
60
79
  }
61
80
 
62
81
  return {
82
+ validateContext: function (ws, session, msg) { return validateContext(ws, session, msg, projectSlug); },
63
83
  inspect: inspect,
64
84
  markRecorded: markRecorded,
65
85
  acknowledge: acknowledge,
@@ -399,6 +399,13 @@ function attachUserMessage(ctx) {
399
399
  if (pendingInternal && typeof pendingInternal.complete === "function") pendingInternal.complete(outcome, reason);
400
400
  }
401
401
 
402
+ var deliveryContext = messageDelivery.validateContext(ws, session, msg);
403
+ if (!deliveryContext.ok) {
404
+ sendTo(ws, { type: "error", text: deliveryContext.reason || "Message context changed." });
405
+ finishPendingEarly(false, deliveryContext.reason || "Message context changed");
406
+ return true;
407
+ }
408
+
402
409
  if (session.loopInterviewHandoff && session.loopInterviewHandoff.state === "starting") {
403
410
  sendTo(ws, { type: "error", text: "Wait for the approved Loop handoff to finish." });
404
411
  finishPendingEarly("release", "Loop handoff is still starting");
@@ -22,10 +22,8 @@ import { beginDefaultVendorConnection, requestDefaultVendor } from './default-ve
22
22
  import { clearProjectSplitState } from './split-session-boundary.js';
23
23
  import { clearPermissionModePending } from './permission-control.js';
24
24
  import { clearMcpPermissionModePending } from './mcp-ui.js';
25
+ import { createWebSocketLifecycle } from './websocket-lifecycle.js';
25
26
 
26
- var reconnectTimer = null;
27
- var reconnectDelay = 1000;
28
- var connectTimeoutId = null;
29
27
  var connectOverlay = null;
30
28
  var hasConnectedOnce = false;
31
29
 
@@ -46,7 +44,22 @@ var overlayGraceTimer = null;
46
44
  var HEARTBEAT_MS = 25000;
47
45
  var heartbeatTimer = null;
48
46
  var heartbeatDeadlineTimer = null;
47
+ var heartbeatAwaitingSocket = null;
49
48
  var disconnectedAt = 0;
49
+ var attemptStartedAt = 0;
50
+ var attemptSocket = null;
51
+ var probeTimer = null;
52
+ var probeSocket = null;
53
+ var probeEpoch = 0;
54
+ var probeStartedAt = 0;
55
+ var lifecycle = createWebSocketLifecycle({
56
+ handshakeTimeoutMs: 12000,
57
+ onHandshakeTimeout: function (epoch) {
58
+ if (lifecycle.current(epoch) && attemptSocket) {
59
+ forceReplaceSocket("handshake_timeout", 0, epoch);
60
+ }
61
+ }
62
+ });
50
63
 
51
64
  function clearOverlayGrace() {
52
65
  if (overlayGraceTimer) {
@@ -64,6 +77,45 @@ function showOverlayNow() {
64
77
  connectOverlay.classList.remove("hidden");
65
78
  }
66
79
 
80
+ function recordConnectionDiagnostic(reason, code, latency) {
81
+ var safeReasons = { close: true, error: true, handshake_timeout: true, heartbeat_timeout: true, heartbeat_error: true, probe_error: true, probe_timeout: true, auth_timeout: true, "ack-timeout": true, online: true, visible: true };
82
+ if (!safeReasons[reason]) reason = "unknown";
83
+ console.warn("[clay] WebSocket diagnostic", {
84
+ reason: reason,
85
+ code: typeof code === "number" ? code : 0,
86
+ retry: lifecycle.getRetryAttempt(),
87
+ latencyMs: typeof latency === "number" ? Math.max(0, Math.min(latency, 600000)) : null
88
+ });
89
+ }
90
+
91
+ function clearProbe() {
92
+ if (probeTimer) clearTimeout(probeTimer);
93
+ probeTimer = null;
94
+ probeSocket = null;
95
+ probeEpoch = 0;
96
+ probeStartedAt = 0;
97
+ }
98
+
99
+ function forceReplaceSocket(reason, code, epoch) {
100
+ if (!lifecycle.current(epoch)) return;
101
+ var socket = getWs();
102
+ var latency = probeStartedAt ? Date.now() - probeStartedAt : null;
103
+ clearProbe();
104
+ stopHeartbeat();
105
+ if (socket) {
106
+ socket.onopen = null;
107
+ socket.onmessage = null;
108
+ socket.onerror = null;
109
+ socket.onclose = null;
110
+ try { socket.close(); } catch (e) {}
111
+ }
112
+ setWs(null);
113
+ recordConnectionDiagnostic(reason, code, latency);
114
+ setStatus("disconnected");
115
+ lifecycle.invalidate(epoch);
116
+ scheduleReconnect(reason);
117
+ }
118
+
67
119
  export function stopHeartbeat() {
68
120
  if (heartbeatTimer) {
69
121
  clearInterval(heartbeatTimer);
@@ -73,6 +125,23 @@ export function stopHeartbeat() {
73
125
  clearTimeout(heartbeatDeadlineTimer);
74
126
  heartbeatDeadlineTimer = null;
75
127
  }
128
+ heartbeatAwaitingSocket = null;
129
+ }
130
+
131
+ function suspendSocketForOffline() {
132
+ clearProbe();
133
+ stopHeartbeat();
134
+ var socket = getWs();
135
+ attemptSocket = null;
136
+ if (socket) {
137
+ socket.onopen = null;
138
+ socket.onmessage = null;
139
+ socket.onerror = null;
140
+ socket.onclose = null;
141
+ try { socket.close(); } catch (e) {}
142
+ }
143
+ setWs(null);
144
+ if (socket || store.get('connected')) setStatus("disconnected");
76
145
  }
77
146
 
78
147
  // Started once per live socket. The socket it was started for is captured, so
@@ -80,28 +149,54 @@ export function stopHeartbeat() {
80
149
  // connection. The pong the server sends back keeps a proxy's upstream read
81
150
  // timeout from expiring. A missing pong also identifies a half-open socket,
82
151
  // which browsers can otherwise leave OPEN while silently losing sends.
83
- export function startHeartbeat(socket) {
152
+ export function startHeartbeat(socket, attemptEpoch) {
84
153
  stopHeartbeat();
85
154
  if (!socket) return;
86
155
  heartbeatTimer = setInterval(function () {
87
- if (!socket || socket.readyState !== 1 || getWs() !== socket) {
156
+ if (!socket || socket.readyState !== 1 || getWs() !== socket || !lifecycle.current(attemptEpoch)) {
88
157
  stopHeartbeat();
89
158
  return;
90
159
  }
91
- try { socket.send(JSON.stringify({ type: "ping" })); } catch (e) {}
160
+ try { socket.send(JSON.stringify({ type: "ping" })); } catch (e) {
161
+ forceReplaceSocket("heartbeat_error", 0, attemptEpoch);
162
+ return;
163
+ }
164
+ heartbeatAwaitingSocket = socket;
92
165
  if (heartbeatDeadlineTimer) clearTimeout(heartbeatDeadlineTimer);
93
166
  heartbeatDeadlineTimer = setTimeout(function () {
94
167
  heartbeatDeadlineTimer = null;
95
- if (getWs() === socket && socket.readyState === 1) connect();
168
+ if (getWs() === socket && socket.readyState === 1 && lifecycle.current(attemptEpoch)) {
169
+ forceReplaceSocket("heartbeat_timeout", 0, attemptEpoch);
170
+ }
96
171
  }, 10000);
97
172
  }, HEARTBEAT_MS);
98
173
  }
99
174
 
100
175
  export function initConnection() {
101
176
  connectOverlay = document.getElementById("connect-overlay");
177
+ if (typeof navigator !== "undefined" && navigator.onLine === false) lifecycle.setOffline(true);
102
178
  window.addEventListener("clay-message-delivery-timeout", function (event) {
103
179
  var socket = event.detail && event.detail.socket;
104
- if (!socket || getWs() === socket) connect();
180
+ recordConnectionDiagnostic("ack-timeout", 0, null);
181
+ if (!socket || getWs() === socket) probeReconnect("ack-timeout");
182
+ });
183
+ window.addEventListener("offline", function () {
184
+ lifecycle.setOffline(true);
185
+ suspendSocketForOffline();
186
+ });
187
+ window.addEventListener("online", function () {
188
+ lifecycle.wake(function () {
189
+ var socket = getWs();
190
+ if (socket && socket.readyState === 1) startHeartbeat(socket, lifecycle.getEpoch());
191
+ probeReconnect("online");
192
+ });
193
+ });
194
+ document.addEventListener("visibilitychange", function () {
195
+ if (!document.hidden) {
196
+ var socket = getWs();
197
+ if (socket && socket.readyState === 1) startHeartbeat(socket, lifecycle.getEpoch());
198
+ probeReconnect("visible");
199
+ }
105
200
  });
106
201
 
107
202
  // --- Reactive UI sync for connected/processing state ---
@@ -224,6 +319,9 @@ function onConnected() {
224
319
  }
225
320
 
226
321
  export function connect() {
322
+ if (lifecycle.isOffline()) return;
323
+ var attemptEpoch = lifecycle.beginAttempt();
324
+ attemptStartedAt = Date.now();
227
325
  var ws = getWs();
228
326
  // Tear down the previous socket's heartbeat before a new socket exists, so
229
327
  // two sockets can never both be pinging.
@@ -236,37 +334,24 @@ export function connect() {
236
334
  ws.onclose = null;
237
335
  ws.close();
238
336
  }
239
- if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
240
-
241
337
  var protocol = location.protocol === "https:" ? "wss:" : "ws:";
242
338
  var socketPath = store.get('wsPath');
243
339
  if (store.get('socketPath') && store.get('socketPath') !== socketPath) clearProjectSplitState();
244
340
  store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
245
341
  var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
246
342
  setWs(newWs);
247
-
248
- // If not connected within 3s, force retry
249
- connectTimeoutId = setTimeout(function () {
250
- if (getWs() !== newWs) return;
251
- if (!store.get('connected')) {
252
- newWs.onclose = null;
253
- newWs.onerror = null;
254
- newWs.close();
255
- connect();
256
- }
257
- }, 3000);
343
+ attemptSocket = newWs;
258
344
 
259
345
  newWs.onopen = function () {
260
- if (getWs() !== newWs) return;
261
- if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
346
+ if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
347
+ if (!lifecycle.markOpen(attemptEpoch)) return;
262
348
  if (hasConnectedOnce && disconnectedAt) {
263
349
  console.log("[clay] WebSocket reconnected after " + (Date.now() - disconnectedAt) + "ms");
264
350
  }
265
351
  disconnectedAt = 0;
266
352
  setStatus("connected");
267
- startHeartbeat(newWs);
268
- reconnectDelay = 1000;
269
- if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
353
+ startHeartbeat(newWs, attemptEpoch);
354
+ lifecycle.clearRetry();
270
355
 
271
356
  // A pane pin is one-shot per WebSocket, not per page lifetime. The server
272
357
  // intentionally does not restore pane presence after a daemon restart.
@@ -292,64 +377,97 @@ export function connect() {
292
377
  };
293
378
 
294
379
  newWs.onclose = function (e) {
295
- if (getWs() !== newWs) return;
296
- if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
380
+ if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
297
381
  stopHeartbeat();
382
+ clearProbe();
298
383
  disconnectedAt = Date.now();
299
- // Abnormal closes are the ones worth reporting: a clean close is either a
300
- // deliberate navigation or a server shutdown the user already knows about.
301
- if (e && !e.wasClean) {
302
- console.warn("[clay] WebSocket closed abnormally: code=" + e.code +
303
- " reason=" + (e.reason || "(none)") + " wasClean=false");
304
- }
384
+ recordConnectionDiagnostic("close", e && e.code, attemptStartedAt ? Date.now() - attemptStartedAt : null);
305
385
  closeDmUserPicker();
306
386
  store.set({ activeProjectSlug: null, sessionActivatedProjectSlug: null });
307
387
  setStatus("disconnected");
308
388
  setActivity(null);
309
- scheduleReconnect();
389
+ lifecycle.invalidate(attemptEpoch);
390
+ scheduleReconnect("close");
310
391
  };
311
392
 
312
- newWs.onerror = function () {};
393
+ newWs.onerror = function () {
394
+ if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
395
+ recordConnectionDiagnostic("error", 0, null);
396
+ forceReplaceSocket("error", 0, attemptEpoch);
397
+ };
313
398
 
314
399
  newWs.onmessage = function (event) {
315
- if (getWs() !== newWs) return;
400
+ if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
316
401
  // Backup: if we're receiving messages, we're connected
317
402
  if (!store.get('connected')) {
318
403
  setStatus("connected");
319
- reconnectDelay = 1000;
320
- if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
404
+ lifecycle.clearRetry();
321
405
  }
322
406
 
323
407
  blinkIO();
324
408
  var msg;
325
409
  try { msg = JSON.parse(event.data); } catch (e) { return; }
326
- if (msg.type === "pong" && heartbeatDeadlineTimer) {
410
+ lifecycle.markLive(attemptEpoch);
411
+ if (msg.type === "pong" && heartbeatDeadlineTimer && heartbeatAwaitingSocket === newWs) {
327
412
  clearTimeout(heartbeatDeadlineTimer);
328
413
  heartbeatDeadlineTimer = null;
414
+ heartbeatAwaitingSocket = null;
329
415
  }
416
+ if (msg.type === "pong" && probeSocket === newWs && lifecycle.current(probeEpoch)) clearProbe();
330
417
  processMessage(msg);
331
418
  };
332
419
  }
333
420
 
334
421
  export function cancelReconnect() {
335
- if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
422
+ clearProbe();
423
+ stopHeartbeat();
424
+ lifecycle.cancel();
336
425
  }
337
426
 
338
- export function scheduleReconnect() {
339
- if (reconnectTimer) return;
340
- reconnectTimer = setTimeout(function () {
341
- reconnectTimer = null;
342
- // Check if auth is still valid before reconnecting
343
- fetch("/info").then(function (res) {
427
+ function probeReconnect(reason) {
428
+ if (lifecycle.isOffline() || !lifecycle.current(lifecycle.getEpoch())) return;
429
+ var socket = getWs();
430
+ if (socket && socket.readyState === 0) return;
431
+ if (socket && socket.readyState === 1) {
432
+ if (probeTimer) return;
433
+ probeSocket = socket;
434
+ probeEpoch = lifecycle.getEpoch();
435
+ probeStartedAt = Date.now();
436
+ try { socket.send(JSON.stringify({ type: "ping" })); } catch (e) {
437
+ forceReplaceSocket("probe_error", 0, probeEpoch);
438
+ return;
439
+ }
440
+ probeTimer = setTimeout(function () {
441
+ probeTimer = null;
442
+ if (probeSocket === socket && lifecycle.current(probeEpoch) && getWs() === socket && socket.readyState === 1) forceReplaceSocket("probe_timeout", 0, probeEpoch);
443
+ }, 4000);
444
+ return;
445
+ }
446
+ scheduleReconnect(reason);
447
+ }
448
+
449
+ export function scheduleReconnect(reason) {
450
+ lifecycle.schedule(reason || "unknown", function (epoch) {
451
+ var authEpoch = epoch;
452
+ var controller = typeof AbortController === "function" ? new AbortController() : null;
453
+ var authToken = lifecycle.beginAuth(authEpoch, function () {
454
+ if (controller) controller.abort();
455
+ scheduleReconnect("auth_timeout");
456
+ }, function () {
457
+ if (controller) controller.abort();
458
+ });
459
+ if (!authToken) return;
460
+ fetch("/info", controller ? { signal: controller.signal } : undefined).then(function (res) {
461
+ if (!lifecycle.finishAuth(authEpoch, authToken)) return;
462
+ if (!lifecycle.canProceed(authEpoch)) return;
344
463
  if (res.status === 401) {
345
464
  location.reload();
346
465
  return;
347
466
  }
348
467
  connect();
349
468
  }).catch(function () {
350
- // Server still down, try connecting anyway
351
- connect();
469
+ var authFinished = lifecycle.finishAuth(authEpoch, authToken);
470
+ if (authFinished && lifecycle.canProceed(authEpoch)) connect();
352
471
  });
353
- }, reconnectDelay);
354
- reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
472
+ });
355
473
  }
@@ -4,7 +4,8 @@ import { handleIssuesMessageInContext as handleIssuesMessage } from './issues-co
4
4
  // All dependencies are direct imports; no context injection needed.
5
5
 
6
6
  import { store } from './store.js';
7
- import { acknowledgeMessage, replayPendingMessages } from './message-delivery.js';
7
+ import { acknowledgeMessage, activateDeliverySession, refreshDeliverySessionIdentity } from './message-delivery.js';
8
+ import { initMessageDeliveryUi } from './message-delivery-ui.js';
8
9
  import { getWs } from './ws-ref.js';
9
10
 
10
11
  // --- Leaf module imports ---
@@ -98,6 +99,7 @@ var messagesEl = document.getElementById("messages");
98
99
  var headerTitleEl = document.getElementById("header-title");
99
100
  var inputEl = document.getElementById("input");
100
101
  var connectOverlay = document.getElementById("connect-overlay");
102
+ initMessageDeliveryUi();
101
103
 
102
104
  export function processMessage(msg) {
103
105
  if (handleIssuesMessage(msg)) return;
@@ -839,10 +841,9 @@ export function processMessage(msg) {
839
841
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
840
842
  ? msg.runtimeTerminalId
841
843
  : (typeof msg.terminalId === "number" ? msg.terminalId : null);
842
- store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode'), effectivePermissionMode: msg.effectivePermissionMode || null, permissionCapabilities: msg.permissionCapabilities || { auto: false, mcpOverride: false }, mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {} });
843
844
  handleScheduledTaskSessionSwitched(msg);
844
845
  requestLoopInterviewState();
845
- replayPendingMessages(msg.id);
846
+ activateDeliverySession(msg.id, msg.cliSessionId || null, msg.sessionOriginId || null);
846
847
  // TUI sessions swap the chat UI for an embedded xterm running
847
848
  // `claude` inside a real PTY. Mount or tear down before the rest of
848
849
  // the chat-side bookkeeping runs so we don't waste work on hidden DOM.
@@ -936,6 +937,7 @@ export function processMessage(msg) {
936
937
 
937
938
  case "session_id":
938
939
  store.set({ cliSessionId: msg.cliSessionId });
940
+ refreshDeliverySessionIdentity(msg.cliSessionId, store.get('sessionOriginId') || null);
939
941
  break;
940
942
 
941
943
  case "message_uuid":
@@ -0,0 +1,63 @@
1
+ // message-delivery-ui.js - receipt recovery notices
2
+
3
+ import { store } from './store.js';
4
+ import { retryPendingMessage } from './message-delivery.js';
5
+
6
+ var initialized = false;
7
+ var rendered = {};
8
+ var unsubscribe = null;
9
+
10
+ function currentContext(receipt) {
11
+ var currentOriginId = store.get('sessionOriginId') || null;
12
+ return receipt && receipt.projectSlug === store.get('currentSlug') && String(receipt.sessionId) === String(store.get('activeSessionId')) && receipt.accountId === (store.get('myUserId') || "_default") && (!receipt.sessionOriginId || !currentOriginId || receipt.sessionOriginId === currentOriginId);
13
+ }
14
+
15
+ function renderReceipt(receipt) {
16
+ var messages = document.getElementById("messages");
17
+ if (!messages || !currentContext(receipt) || rendered[receipt.clientMessageId]) return;
18
+ var notice = document.createElement("div");
19
+ notice.className = "sys-msg error";
20
+ notice.dataset.deliveryReceipt = receipt.clientMessageId;
21
+ var text = document.createElement("span");
22
+ text.className = "sys-text";
23
+ text.textContent = "Receipt unconfirmed. Retry this message.";
24
+ notice.appendChild(text);
25
+ var retry = document.createElement("button");
26
+ retry.type = "button";
27
+ retry.className = "sys-retry";
28
+ retry.textContent = "Retry";
29
+ retry.addEventListener("click", function () { retryPendingMessage(receipt.clientMessageId); });
30
+ notice.appendChild(retry);
31
+ messages.appendChild(notice);
32
+ rendered[receipt.clientMessageId] = notice;
33
+ }
34
+
35
+ function sync(state) {
36
+ var receipts = state.deliveryReceipts || {};
37
+ Object.keys(rendered).forEach(function (id) {
38
+ if (!rendered[id].parentNode || !receipts[id] || !currentContext(receipts[id])) {
39
+ if (rendered[id] && rendered[id].parentNode) rendered[id].parentNode.removeChild(rendered[id]);
40
+ delete rendered[id];
41
+ }
42
+ });
43
+ Object.keys(receipts).forEach(function (id) { renderReceipt(receipts[id]); });
44
+ }
45
+
46
+ export function initMessageDeliveryUi() {
47
+ if (initialized) return;
48
+ initialized = true;
49
+ unsubscribe = store.subscribe(function (state, previous) {
50
+ if (state.deliveryReceipts !== previous.deliveryReceipts || state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.sessionOriginId !== previous.sessionOriginId || state.myUserId !== previous.myUserId || state.replayingHistory !== previous.replayingHistory) sync(state);
51
+ });
52
+ sync(store.snap());
53
+ }
54
+
55
+ export function disposeMessageDeliveryUi() {
56
+ Object.keys(rendered).forEach(function (id) {
57
+ if (rendered[id] && rendered[id].parentNode) rendered[id].parentNode.removeChild(rendered[id]);
58
+ });
59
+ rendered = {};
60
+ if (unsubscribe) unsubscribe();
61
+ unsubscribe = null;
62
+ initialized = false;
63
+ }
@@ -4,7 +4,10 @@ import { store } from './store.js';
4
4
  import { getWs } from './ws-ref.js';
5
5
 
6
6
  var ACK_TIMEOUT_MS = 5000;
7
+ var ACK_RETRY_LIMIT = 3;
7
8
  var ackTimers = {};
9
+ var socketRefs = {};
10
+ var activatedContext = null;
8
11
 
9
12
  function createClientMessageId() {
10
13
  if (window.crypto && typeof window.crypto.randomUUID === "function") {
@@ -21,6 +24,39 @@ function replacePending(messages) {
21
24
  store.set({ pendingOutboundMessages: messages });
22
25
  }
23
26
 
27
+ function receipts() {
28
+ return store.get('deliveryReceipts') || {};
29
+ }
30
+
31
+ function setReceipt(entry) {
32
+ var next = Object.assign({}, receipts());
33
+ next[entry.payload.clientMessageId] = {
34
+ clientMessageId: entry.payload.clientMessageId,
35
+ projectSlug: entry.projectSlug,
36
+ sessionId: entry.sessionId,
37
+ accountId: entry.accountId,
38
+ sessionOriginId: entry.sessionOriginId || null,
39
+ attempts: entry.attempts
40
+ };
41
+ store.set({ deliveryReceipts: next });
42
+ }
43
+
44
+ function clearReceipt(clientMessageId) {
45
+ var current = receipts();
46
+ if (!current[clientMessageId]) return;
47
+ var next = Object.assign({}, current);
48
+ delete next[clientMessageId];
49
+ store.set({ deliveryReceipts: next });
50
+ }
51
+
52
+ function markUnconfirmed(entry) {
53
+ var unconfirmed = Object.assign({}, entry, { status: "unconfirmed" });
54
+ replacePending(pendingMessages().map(function (candidate) {
55
+ return candidate.payload.clientMessageId === entry.payload.clientMessageId ? unconfirmed : candidate;
56
+ }));
57
+ setReceipt(unconfirmed);
58
+ }
59
+
24
60
  function clearAckTimer(clientMessageId) {
25
61
  if (!ackTimers[clientMessageId]) return;
26
62
  clearTimeout(ackTimers[clientMessageId]);
@@ -35,25 +71,92 @@ function isPending(clientMessageId) {
35
71
  return false;
36
72
  }
37
73
 
74
+ function pendingEntry(clientMessageId) {
75
+ var pending = pendingMessages();
76
+ for (var i = 0; i < pending.length; i++) {
77
+ if (pending[i].payload.clientMessageId === clientMessageId) return pending[i];
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function matchesCurrentContext(entry) {
83
+ return entry && entry.projectSlug === store.get('currentSlug') && String(entry.sessionId) === String(store.get('activeSessionId')) && entry.accountId === (store.get('myUserId') || "_default");
84
+ }
85
+
86
+ function hasAutomaticReplayIdentity(entry, socket) {
87
+ if (!activatedContext || activatedContext.socket !== socket) return false;
88
+ if (entry.sessionOriginId && activatedContext.sessionOriginId && entry.sessionOriginId !== activatedContext.sessionOriginId) return false;
89
+ if (entry.sessionOriginId && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId) return true;
90
+ if (entry.cliSessionId && activatedContext.cliSessionId) return entry.cliSessionId === activatedContext.cliSessionId;
91
+ return socketRefs[entry.payload.clientMessageId] === socket;
92
+ }
93
+
94
+ function hasKnownCliMismatch(entry) {
95
+ if (entry.sessionOriginId && activatedContext && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId) return false;
96
+ return !!entry.cliSessionId && !!activatedContext && !!activatedContext.cliSessionId && entry.cliSessionId !== activatedContext.cliSessionId;
97
+ }
98
+
99
+ function matchesActivatedContext(entry) {
100
+ var currentCliSessionId = store.get('cliSessionId') || null;
101
+ var sameOrigin = !!(entry.sessionOriginId && activatedContext && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId);
102
+ return !!activatedContext && activatedContext.socket === getWs() && matchesCurrentContext(entry) &&
103
+ activatedContext.projectSlug === store.get('currentSlug') && String(activatedContext.sessionId) === String(store.get('activeSessionId')) &&
104
+ activatedContext.accountId === (store.get('myUserId') || "_default") && (activatedContext.sessionOriginId || activatedContext.cliSessionId === currentCliSessionId) &&
105
+ (!entry.sessionOriginId || !activatedContext.sessionOriginId || entry.sessionOriginId === activatedContext.sessionOriginId) &&
106
+ (sameOrigin || !entry.cliSessionId || !activatedContext.cliSessionId || entry.cliSessionId === activatedContext.cliSessionId);
107
+ }
108
+
38
109
  function armAckTimer(clientMessageId, socket) {
39
110
  clearAckTimer(clientMessageId);
40
111
  ackTimers[clientMessageId] = setTimeout(function () {
41
112
  delete ackTimers[clientMessageId];
42
113
  if (!isPending(clientMessageId) || getWs() !== socket) return;
114
+ var entry = pendingEntry(clientMessageId);
115
+ if (!matchesActivatedContext(entry)) return;
43
116
  window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
44
117
  detail: { socket: socket, clientMessageId: clientMessageId },
45
118
  }));
119
+ var attempts = entry.attempts || 0;
120
+ if (attempts < ACK_RETRY_LIMIT && entry && socket.readyState === 1) {
121
+ var nextEntry = Object.assign({}, entry, { attempts: attempts + 1 });
122
+ replacePending(pendingMessages().map(function (candidate) {
123
+ return candidate.payload.clientMessageId === clientMessageId ? nextEntry : candidate;
124
+ }));
125
+ transmit(nextEntry, false);
126
+ return;
127
+ }
128
+ if (attempts >= ACK_RETRY_LIMIT) {
129
+ markUnconfirmed(entry);
130
+ return;
131
+ }
46
132
  }, ACK_TIMEOUT_MS);
47
133
  }
48
134
 
49
- function transmit(entry) {
135
+ function transmit(entry, automatic) {
50
136
  var socket = getWs();
51
- if (!socket || socket.readyState !== 1) return false;
137
+ if (!matchesCurrentContext(entry) || !socket || socket.readyState !== 1) return false;
138
+ if (automatic && entry.status === "unconfirmed") return false;
139
+ if (automatic && !hasAutomaticReplayIdentity(entry, socket)) {
140
+ markUnconfirmed(entry);
141
+ return false;
142
+ }
143
+ if (automatic) {
144
+ if ((entry.attempts || 0) >= ACK_RETRY_LIMIT) {
145
+ markUnconfirmed(entry);
146
+ return false;
147
+ }
148
+ entry = Object.assign({}, entry, { attempts: (entry.attempts || 0) + 1 });
149
+ replacePending(pendingMessages().map(function (candidate) {
150
+ return candidate.payload.clientMessageId === entry.payload.clientMessageId ? entry : candidate;
151
+ }));
152
+ }
153
+ socketRefs[entry.payload.clientMessageId] = socket;
52
154
  try {
53
155
  socket.send(JSON.stringify(entry.payload));
54
156
  armAckTimer(entry.payload.clientMessageId, socket);
55
157
  return true;
56
158
  } catch (e) {
159
+ markUnconfirmed(entry);
57
160
  return false;
58
161
  }
59
162
  }
@@ -63,10 +166,22 @@ export function sendAcknowledgedMessage(payload) {
63
166
  var entry = {
64
167
  projectSlug: store.get('currentSlug'),
65
168
  sessionId: store.get('activeSessionId'),
169
+ accountId: store.get('myUserId') || "_default",
170
+ cliSessionId: store.get('cliSessionId') || null,
171
+ sessionOriginId: store.get('sessionOriginId') || null,
172
+ attempts: 0,
173
+ status: "pending",
66
174
  payload: nextPayload,
67
175
  };
176
+ nextPayload.projectSlug = entry.projectSlug;
177
+ nextPayload.sessionId = entry.sessionId;
178
+ nextPayload.accountId = entry.accountId;
179
+ nextPayload.cliSessionId = entry.cliSessionId;
180
+ nextPayload.sessionOriginId = entry.sessionOriginId;
181
+ entry.payload = nextPayload;
68
182
  replacePending(pendingMessages().concat([entry]));
69
- if (!transmit(entry)) {
183
+ if (!transmit(entry, false)) {
184
+ if (!getWs() || getWs().readyState !== 1) markUnconfirmed(entry);
70
185
  window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
71
186
  detail: { socket: getWs(), clientMessageId: nextPayload.clientMessageId },
72
187
  }));
@@ -74,6 +189,21 @@ export function sendAcknowledgedMessage(payload) {
74
189
  return nextPayload.clientMessageId;
75
190
  }
76
191
 
192
+ export function retryPendingMessage(clientMessageId) {
193
+ var entry = pendingEntry(clientMessageId);
194
+ if (!matchesActivatedContext(entry) || hasKnownCliMismatch(entry)) return false;
195
+ var retryEntry = Object.assign({}, entry, { attempts: 0, status: "pending" });
196
+ replacePending(pendingMessages().map(function (candidate) {
197
+ return candidate.payload.clientMessageId === clientMessageId ? retryEntry : candidate;
198
+ }));
199
+ if (transmit(retryEntry, false)) {
200
+ clearReceipt(clientMessageId);
201
+ return true;
202
+ }
203
+ markUnconfirmed(retryEntry);
204
+ return false;
205
+ }
206
+
77
207
  export function acknowledgeMessage(clientMessageId) {
78
208
  if (typeof clientMessageId !== "string") return null;
79
209
  var pending = pendingMessages();
@@ -82,9 +212,14 @@ export function acknowledgeMessage(clientMessageId) {
82
212
  if (entry.payload.clientMessageId === clientMessageId) acknowledged = entry;
83
213
  return entry.payload.clientMessageId !== clientMessageId;
84
214
  });
85
- if (next.length === pending.length) return null;
215
+ if (next.length === pending.length) {
216
+ clearReceipt(clientMessageId);
217
+ return null;
218
+ }
86
219
  clearAckTimer(clientMessageId);
87
220
  replacePending(next);
221
+ delete socketRefs[clientMessageId];
222
+ clearReceipt(clientMessageId);
88
223
  return acknowledged;
89
224
  }
90
225
 
@@ -92,8 +227,30 @@ export function replayPendingMessages(sessionId) {
92
227
  var pending = pendingMessages();
93
228
  var projectSlug = store.get('currentSlug');
94
229
  for (var i = 0; i < pending.length; i++) {
95
- if (pending[i].projectSlug === projectSlug && pending[i].sessionId === sessionId) {
96
- transmit(pending[i]);
230
+ if (pending[i].projectSlug === projectSlug && String(pending[i].sessionId) === String(sessionId) && matchesCurrentContext(pending[i])) {
231
+ transmit(pending[i], true);
97
232
  }
98
233
  }
99
234
  }
235
+
236
+ export function activateDeliverySession(sessionId, cliSessionId, sessionOriginId) {
237
+ var socket = getWs();
238
+ activatedContext = {
239
+ socket: socket,
240
+ projectSlug: store.get('currentSlug'),
241
+ sessionId: sessionId,
242
+ accountId: store.get('myUserId') || "_default",
243
+ cliSessionId: cliSessionId || null,
244
+ sessionOriginId: sessionOriginId || null
245
+ };
246
+ replayPendingMessages(sessionId);
247
+ }
248
+
249
+ export function refreshDeliverySessionIdentity(cliSessionId, sessionOriginId) {
250
+ if (!activatedContext || activatedContext.socket !== getWs()) return false;
251
+ if (activatedContext.projectSlug !== store.get('currentSlug') || String(activatedContext.sessionId) !== String(store.get('activeSessionId')) || activatedContext.accountId !== (store.get('myUserId') || "_default")) return false;
252
+ if (activatedContext.sessionOriginId && sessionOriginId && activatedContext.sessionOriginId !== sessionOriginId) return false;
253
+ activatedContext.cliSessionId = cliSessionId || null;
254
+ if (!activatedContext.sessionOriginId && sessionOriginId) activatedContext.sessionOriginId = sessionOriginId;
255
+ return true;
256
+ }
@@ -0,0 +1,178 @@
1
+ // websocket-lifecycle.js - bounded connection attempt and retry policy
2
+
3
+ var DEFAULTS = {
4
+ handshakeTimeoutMs: 12000,
5
+ stableConnectionMs: 15000,
6
+ retryMinMs: 1000,
7
+ retryMaxMs: 15000,
8
+ jitterRatio: 0.2,
9
+ authTimeoutMs: 4000
10
+ };
11
+
12
+ function createWebSocketLifecycle(options) {
13
+ var config = Object.assign({}, DEFAULTS, options || {});
14
+ var epoch = 0;
15
+ var retryAttempt = 0;
16
+ var retryTimer = null;
17
+ var handshakeTimer = null;
18
+ var stableTimer = null;
19
+ var authTimer = null;
20
+ var authInFlight = false;
21
+ var authCancel = null;
22
+ var authGeneration = 0;
23
+ var stableEpoch = 0;
24
+ var cancelled = false;
25
+ var offline = false;
26
+
27
+ function clearTimer(timer) {
28
+ if (timer) clearTimeout(timer);
29
+ return null;
30
+ }
31
+
32
+ function clearTimers() {
33
+ retryTimer = clearTimer(retryTimer);
34
+ handshakeTimer = clearTimer(handshakeTimer);
35
+ stableTimer = clearTimer(stableTimer);
36
+ authTimer = clearTimer(authTimer);
37
+ stableEpoch = 0;
38
+ if (authCancel) authCancel();
39
+ authCancel = null;
40
+ authInFlight = false;
41
+ }
42
+
43
+ function current(value) {
44
+ return value === epoch && !cancelled;
45
+ }
46
+
47
+ function canProceed(value) {
48
+ return current(value) && !offline;
49
+ }
50
+
51
+ function beginAttempt() {
52
+ clearTimers();
53
+ cancelled = false;
54
+ epoch += 1;
55
+ var attemptEpoch = epoch;
56
+ handshakeTimer = setTimeout(function () {
57
+ handshakeTimer = null;
58
+ if (current(attemptEpoch) && config.onHandshakeTimeout) config.onHandshakeTimeout(attemptEpoch);
59
+ }, config.handshakeTimeoutMs);
60
+ return attemptEpoch;
61
+ }
62
+
63
+ function delayForAttempt() {
64
+ var base = Math.min(config.retryMaxMs, config.retryMinMs * Math.pow(1.5, retryAttempt));
65
+ retryAttempt += 1;
66
+ var jitter = base * config.jitterRatio;
67
+ var random = config.random || Math.random;
68
+ return Math.min(config.retryMaxMs, Math.max(0, Math.round(base - jitter + random() * jitter * 2)));
69
+ }
70
+
71
+ function schedule(reason, callback) {
72
+ if (cancelled || offline || retryTimer) return null;
73
+ var scheduledEpoch = epoch;
74
+ var delay = delayForAttempt();
75
+ retryTimer = setTimeout(function () {
76
+ retryTimer = null;
77
+ if (!canProceed(scheduledEpoch)) return;
78
+ callback(scheduledEpoch, reason, delay);
79
+ }, delay);
80
+ return { epoch: scheduledEpoch, delay: delay };
81
+ }
82
+
83
+ function clearRetry() {
84
+ retryTimer = clearTimer(retryTimer);
85
+ }
86
+
87
+ function beginAuth(authEpoch, onTimeout, onCancel) {
88
+ if (!canProceed(authEpoch) || authInFlight) return null;
89
+ authInFlight = true;
90
+ authGeneration += 1;
91
+ var authToken = { epoch: authEpoch, generation: authGeneration };
92
+ authCancel = onCancel || null;
93
+ authTimer = setTimeout(function () {
94
+ authTimer = null;
95
+ authInFlight = false;
96
+ authCancel = null;
97
+ if (canProceed(authEpoch) && onTimeout) onTimeout(authEpoch);
98
+ }, config.authTimeoutMs);
99
+ return authToken;
100
+ }
101
+
102
+ function finishAuth(authEpoch, authToken) {
103
+ if (!authInFlight || !current(authEpoch) || !authToken || authToken.epoch !== authEpoch || authToken.generation !== authGeneration) return false;
104
+ authInFlight = false;
105
+ authCancel = null;
106
+ authTimer = clearTimer(authTimer);
107
+ return true;
108
+ }
109
+
110
+ function markOpen(attemptEpoch) {
111
+ if (!current(attemptEpoch)) return false;
112
+ handshakeTimer = clearTimer(handshakeTimer);
113
+ return true;
114
+ }
115
+
116
+ function markLive(attemptEpoch) {
117
+ if (!current(attemptEpoch)) return false;
118
+ handshakeTimer = clearTimer(handshakeTimer);
119
+ if (stableEpoch === attemptEpoch) return true;
120
+ stableEpoch = attemptEpoch;
121
+ stableTimer = clearTimer(stableTimer);
122
+ stableTimer = setTimeout(function () {
123
+ stableTimer = null;
124
+ if (current(attemptEpoch)) retryAttempt = 0;
125
+ }, config.stableConnectionMs);
126
+ return true;
127
+ }
128
+
129
+ function invalidate(attemptEpoch) {
130
+ if (attemptEpoch !== epoch) return false;
131
+ epoch += 1;
132
+ clearTimers();
133
+ return true;
134
+ }
135
+
136
+ function cancel() {
137
+ cancelled = true;
138
+ epoch += 1;
139
+ clearTimers();
140
+ }
141
+
142
+ function setOffline(value) {
143
+ var nextOffline = value === true;
144
+ if (nextOffline === offline) return offline;
145
+ offline = nextOffline;
146
+ if (offline) {
147
+ epoch += 1;
148
+ clearTimers();
149
+ }
150
+ return offline;
151
+ }
152
+
153
+ function wake(callback) {
154
+ offline = false;
155
+ if (!cancelled && callback) callback();
156
+ }
157
+
158
+ return {
159
+ beginAttempt: beginAttempt,
160
+ beginAuth: beginAuth,
161
+ cancel: cancel,
162
+ canProceed: canProceed,
163
+ clearRetry: clearRetry,
164
+ current: current,
165
+ finishAuth: finishAuth,
166
+ invalidate: invalidate,
167
+ markOpen: markOpen,
168
+ markLive: markLive,
169
+ schedule: schedule,
170
+ setOffline: setOffline,
171
+ wake: wake,
172
+ getRetryAttempt: function () { return retryAttempt; },
173
+ getEpoch: function () { return epoch; },
174
+ isOffline: function () { return offline; }
175
+ };
176
+ }
177
+
178
+ export { createWebSocketLifecycle };
package/lib/sessions.js CHANGED
@@ -846,7 +846,7 @@ function createSessionManager(opts) {
846
846
  var _sessionVendor = session.vendor || defaultVendor || "claude";
847
847
  var _vendorCaps = _capsByVendor[_sessionVendor] || {};
848
848
  var _permissionState = require("./session-permission-mode").clientState(session, "default", defaultVendor);
849
- _send({ type: "session_switched", id: localId, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: _permissionState.requestedPermissionMode, effectivePermissionMode: _permissionState.effectivePermissionMode, permissionCapabilities: _permissionState.permissionCapabilities, mcpPermissionModeOverrides: _permissionState.mcpPermissionModeOverrides, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
849
+ _send({ type: "session_switched", id: localId, sessionOriginId: session.sessionOriginId || null, cliSessionId: session.cliSessionId || null, loop: session.loop || null, vendor: session.vendor || null, model: session.model || null, effort: session.effort || null, hasHistory: (session.history && session.history.length > 0), capabilities: _vendorCaps, isProcessing: !!session.isProcessing, mode: session.mode || "gui", terminalId: typeof session.terminalId === "number" ? session.terminalId : null, runtimeMode: session.runtimeMode || null, runtimeTerminalId: typeof session.runtimeTerminalId === "number" ? session.runtimeTerminalId : null, tuiSuspended: !!session.tuiSuspended, dangerouslySkipPermissions: !!session.dangerouslySkipPermissions, permissionMode: _permissionState.requestedPermissionMode, effectivePermissionMode: _permissionState.effectivePermissionMode, permissionCapabilities: _permissionState.permissionCapabilities, mcpPermissionModeOverrides: _permissionState.mcpPermissionModeOverrides, autonomousRun: projectAutonomousRun(session.autonomousRun), loopInterviewBrief: session.loopInterviewBrief || null, loopInterviewHandoff: session.loopInterviewHandoff || null, scheduledTaskDraft: session.scheduledTaskDraft || null });
850
850
  // Send vendor-specific slash commands
851
851
  var _vendorCmds = (session.slashCommandsByVendor && session.slashCommandsByVendor[_sessionVendor])
852
852
  || session.slashCommands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.3.0-beta.1",
3
+ "version": "4.3.0-beta.2",
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"