clay-server 2.23.0 → 2.23.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.
package/lib/public/app.js CHANGED
@@ -31,7 +31,7 @@ import { initMateWizard, openMateWizard, closeMateWizard, handleMateCreated } fr
31
31
  import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } from './modules/command-palette.js';
32
32
  import { initLongPress } from './modules/longpress.js';
33
33
  import { initMention, handleMentionStart, handleMentionStream, handleMentionDone, handleMentionError, handleMentionActivity, renderMentionUser, renderMentionResponse } from './modules/mention.js';
34
- import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, openQuickDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState } from './modules/debate.js';
34
+ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateResumed, handleDebateTurn, handleDebateActivity, handleDebateStream, handleDebateTurnDone, handleDebateCommentQueued, handleDebateCommentInjected, handleDebateEnded, handleDebateError, renderDebateStarted, renderDebateTurnDone, renderDebateEnded, renderDebateCommentInjected, renderDebateUserResume, openDebateModal, closeDebateModal, openQuickDebateModal, handleDebateBriefReady, renderDebateBriefReady, isDebateActive, resetDebateState } from './modules/debate.js';
35
35
 
36
36
  // --- Base path for multi-project routing ---
37
37
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -4979,6 +4979,7 @@ import { initDebate, handleDebateStarted, handleDebateResumed, handleDebateTurn,
4979
4979
  // --- Debate ---
4980
4980
  case "debate_preparing":
4981
4981
  showDebateSticky("preparing", msg);
4982
+ handleDebatePreparing(msg);
4982
4983
  break;
4983
4984
 
4984
4985
  case "debate_brief_ready":
@@ -331,6 +331,54 @@
331
331
  color: var(--text);
332
332
  }
333
333
 
334
+ /* --- Preparing indicator (shown while brief is being generated) --- */
335
+ .debate-preparing-indicator {
336
+ max-width: var(--content-width);
337
+ margin: 32px auto;
338
+ padding: 0 20px;
339
+ }
340
+
341
+ .debate-preparing-inner {
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 12px;
345
+ padding: 16px 20px;
346
+ border-radius: 12px;
347
+ background: color-mix(in srgb, var(--accent2) 8%, transparent);
348
+ border: 1px solid color-mix(in srgb, var(--accent2) 15%, transparent);
349
+ }
350
+
351
+ .debate-preparing-spinner {
352
+ display: inline-flex;
353
+ flex-shrink: 0;
354
+ color: var(--accent2);
355
+ animation: spin 1.2s linear infinite;
356
+ }
357
+
358
+ .debate-preparing-spinner .lucide {
359
+ width: 20px;
360
+ height: 20px;
361
+ }
362
+
363
+ .debate-preparing-text {
364
+ font-size: 14px;
365
+ color: var(--text-secondary);
366
+ line-height: 1.5;
367
+ }
368
+
369
+ .debate-preparing-dots {
370
+ animation: blink-dots 1.4s steps(4, end) infinite;
371
+ }
372
+
373
+ @keyframes spin {
374
+ to { transform: rotate(360deg); }
375
+ }
376
+
377
+ @keyframes blink-dots {
378
+ 0%, 20% { opacity: 0; }
379
+ 40% { opacity: 1; }
380
+ }
381
+
334
382
  /* --- Activity bar inside turns --- */
335
383
  .debate-activity-bar {
336
384
  margin: 4px 0;
@@ -31,6 +31,11 @@ export function resetDebateState() {
31
31
  flushTurnStream();
32
32
  currentTurnEl = null;
33
33
  currentTurnMateId = null;
34
+ // Remove preparing indicator if present
35
+ if (ctx && ctx.messagesEl) {
36
+ var prep = ctx.messagesEl.querySelector(".debate-preparing-indicator");
37
+ if (prep) prep.remove();
38
+ }
34
39
  }
35
40
 
36
41
  function buildAvatarUrl(meta) {
@@ -85,7 +90,43 @@ export function handleDebateResumed(msg) {
85
90
  showDebateInfoFloat(msg);
86
91
  }
87
92
 
93
+ export function handleDebatePreparing(msg) {
94
+ debatePhase = "preparing";
95
+
96
+ if (!ctx.messagesEl) return;
97
+
98
+ // Remove any existing preparing indicator
99
+ var existing = ctx.messagesEl.querySelector(".debate-preparing-indicator");
100
+ if (existing) existing.remove();
101
+
102
+ var el = document.createElement("div");
103
+ el.className = "debate-preparing-indicator";
104
+
105
+ var moderatorName = msg.moderatorName || "Moderator";
106
+ var panelistNames = (msg.panelists || []).map(function (p) { return p.name; }).filter(Boolean).join(", ");
107
+
108
+ el.innerHTML =
109
+ '<div class="debate-preparing-inner">' +
110
+ '<div class="debate-preparing-spinner">' + iconHtml("loader") + '</div>' +
111
+ '<div class="debate-preparing-text">' +
112
+ '<strong>' + escapeHtml(moderatorName) + '</strong> is setting up the debate' +
113
+ (panelistNames ? ' with <strong>' + escapeHtml(panelistNames) + '</strong>' : '') +
114
+ '<span class="debate-preparing-dots">...</span>' +
115
+ '</div>' +
116
+ '</div>';
117
+
118
+ ctx.messagesEl.appendChild(el);
119
+ refreshIcons();
120
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
121
+ }
122
+
88
123
  export function handleDebateStarted(msg) {
124
+ // Remove preparing indicator when debate goes live
125
+ if (ctx.messagesEl) {
126
+ var prep = ctx.messagesEl.querySelector(".debate-preparing-indicator");
127
+ if (prep) prep.remove();
128
+ }
129
+
89
130
  debateActive = true;
90
131
  debateTopic = msg.topic || "";
91
132
  debateRound = 1;
package/lib/sdk-bridge.js CHANGED
@@ -821,9 +821,14 @@ function createSDKBridge(opts) {
821
821
  worker._readyResolve = null;
822
822
  // Let the readyPromise hang; the query_error handler will clean up
823
823
  }
824
- // Notify message handlers about unexpected exit so sessions don't hang
824
+ // Notify message handlers about unexpected exit so sessions don't hang.
825
+ // Always dispatch a fallback query_error. The handler is idempotent:
826
+ // it checks isProcessing before taking action, and cleanupSessionWorker
827
+ // guards against stale workers. This covers all exit cases including
828
+ // signal kills (code=null) and normal exits where the IPC query_error
829
+ // was lost due to connection timing.
825
830
  if (code === 0 && !worker.ready) {
826
- // Worker exited cleanly before sending "ready" — something is wrong
831
+ // Worker exited cleanly before sending "ready"
827
832
  for (var h = 0; h < worker.messageHandlers.length; h++) {
828
833
  worker.messageHandlers[h]({
829
834
  type: "query_error",
@@ -832,17 +837,35 @@ function createSDKBridge(opts) {
832
837
  stderr: worker._stderrBuf || null,
833
838
  });
834
839
  }
835
- }
836
- if (code !== 0 && code !== null) {
840
+ } else if (code !== 0 || code === null || signal) {
841
+ // Worker crashed, was killed by signal, or exited abnormally
837
842
  var stderrText = worker._stderrBuf || "";
843
+ var exitReason = signal
844
+ ? "Worker killed by " + signal
845
+ : (stderrText || "Worker exited with code " + code);
838
846
  for (var h = 0; h < worker.messageHandlers.length; h++) {
839
847
  worker.messageHandlers[h]({
840
848
  type: "query_error",
841
- error: stderrText || "Worker exited with code " + code,
849
+ error: exitReason,
842
850
  exitCode: code,
843
851
  stderr: stderrText || null,
844
852
  });
845
853
  }
854
+ } else if (worker.messageHandlers.length > 0) {
855
+ // Normal exit (code=0, ready=true). Dispatch fallback in case the
856
+ // IPC query_done/query_error was lost (e.g. connection closed early).
857
+ var fallbackMsg = worker._abortSent
858
+ ? "Worker aborted"
859
+ : "Worker exited before query completed";
860
+ for (var h = 0; h < worker.messageHandlers.length; h++) {
861
+ worker.messageHandlers[h]({
862
+ type: "query_error",
863
+ error: fallbackMsg,
864
+ exitCode: 0,
865
+ stderr: worker._stderrBuf || null,
866
+ _fallback: true,
867
+ });
868
+ }
846
869
  }
847
870
  cleanupWorker(worker);
848
871
  });
@@ -863,13 +886,17 @@ function createSDKBridge(opts) {
863
886
 
864
887
  worker.kill = function() {
865
888
  worker.send({ type: "shutdown" });
866
- // Force kill after 3 seconds if still alive
889
+ // Force kill after 5 seconds if still alive (gives SDK time to save session)
867
890
  setTimeout(function() {
868
891
  if (worker.process && !worker.process.killed) {
869
892
  try { worker.process.kill("SIGKILL"); } catch (e) {}
870
893
  }
871
- }, 3000);
872
- cleanupWorker(worker);
894
+ }, 5000);
895
+ // Don't call cleanupWorker here. Let the exit handler do it after
896
+ // the worker has had time to save SDK session state to disk.
897
+ // Closing the connection prematurely causes the worker to exit
898
+ // before the SDK can flush its session file, leading to "no
899
+ // conversation found" errors on resume (OS multi-user mode).
873
900
  };
874
901
 
875
902
  return worker;
@@ -942,7 +969,7 @@ function createSDKBridge(opts) {
942
969
  session.pendingElicitations = {};
943
970
  session.streamedText = false;
944
971
  session.responsePreview = "";
945
- session.abortController = { abort: function() { worker.send({ type: "abort" }); } };
972
+ session.abortController = { abort: function() { worker._abortSent = true; worker.send({ type: "abort" }); } };
946
973
 
947
974
  // Build initial user message content
948
975
  var content = [];
@@ -1049,6 +1076,9 @@ function createSDKBridge(opts) {
1049
1076
  break;
1050
1077
 
1051
1078
  case "query_done":
1079
+ // Mark that we received a proper IPC completion, so the exit
1080
+ // handler fallback knows not to double-process.
1081
+ worker._queryEnded = true;
1052
1082
  // Stream ended normally
1053
1083
  if (session.isProcessing && session.taskStopRequested) {
1054
1084
  session.isProcessing = false;
@@ -1066,6 +1096,10 @@ function createSDKBridge(opts) {
1066
1096
  break;
1067
1097
 
1068
1098
  case "query_error": {
1099
+ // Skip fallback errors from exit handler if we already handled the real one
1100
+ if (msg._fallback && worker._queryEnded) break;
1101
+ // Mark that we received a proper IPC completion
1102
+ worker._queryEnded = true;
1069
1103
  // Check session-not-found before isProcessing gate (it can arrive after processing is cleared)
1070
1104
  var qerrLower = (msg.error || "").toLowerCase();
1071
1105
  // Only match the exact SDK error, not generic worker stderr
package/lib/sdk-worker.js CHANGED
@@ -124,8 +124,7 @@ function handleMessage(msg) {
124
124
  handleWarmup(msg);
125
125
  break;
126
126
  case "shutdown":
127
- cleanup();
128
- process.exit(0);
127
+ gracefulExit(0);
129
128
  break;
130
129
  default:
131
130
  console.error("[sdk-worker] Unknown message type:", msg.type);
@@ -393,6 +392,7 @@ async function handleWarmup(msg) {
393
392
  }
394
393
 
395
394
  // --- Cleanup ---
395
+ var _exitScheduled = false;
396
396
  function cleanup() {
397
397
  if (_keepAlive) {
398
398
  try { clearInterval(_keepAlive); } catch (e) {}
@@ -408,6 +408,16 @@ function cleanup() {
408
408
  }
409
409
  }
410
410
 
411
+ // Exit with a grace period so the SDK can flush session state to disk.
412
+ // Without this, process.exit(0) kills pending async writes and the
413
+ // session file may be incomplete, causing "no conversation found" on resume.
414
+ function gracefulExit(code) {
415
+ if (_exitScheduled) return;
416
+ _exitScheduled = true;
417
+ cleanup();
418
+ setTimeout(function() { process.exit(code); }, 800);
419
+ }
420
+
411
421
  // Keep event loop alive — without this, Node may exit if the socket handle
412
422
  // gets unreferenced (observed on Linux with uid/gid spawn)
413
423
  var _keepAlive = setInterval(function() {}, 30000);
@@ -436,25 +446,21 @@ conn.on("data", function(chunk) {
436
446
 
437
447
  conn.on("error", function(err) {
438
448
  console.error("[sdk-worker] Socket error:", err.message);
439
- cleanup();
440
- process.exit(1);
449
+ gracefulExit(1);
441
450
  });
442
451
 
443
452
  conn.on("close", function() {
444
453
  try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: socket closed\n"); } catch (e) {}
445
- cleanup();
446
- process.exit(0);
454
+ gracefulExit(0);
447
455
  });
448
456
 
449
457
  // Handle process signals
450
458
  process.on("SIGTERM", function() {
451
459
  try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: SIGTERM\n"); } catch (e) {}
452
- cleanup();
453
- process.exit(0);
460
+ gracefulExit(0);
454
461
  });
455
462
 
456
463
  process.on("SIGINT", function() {
457
464
  try { require("fs").writeSync(2, "[sdk-worker] EXIT REASON: SIGINT\n"); } catch (e) {}
458
- cleanup();
459
- process.exit(0);
465
+ gracefulExit(0);
460
466
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.23.0",
3
+ "version": "2.23.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",