clay-server 2.26.0-beta.9 → 2.26.0

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/sdk-bridge.js CHANGED
@@ -437,9 +437,38 @@ function createSDKBridge(opts) {
437
437
  session.pendingAskUser = {};
438
438
  session.activeTaskToolIds = {};
439
439
  session.taskIdMap = {};
440
+ // Only clear rateLimitResetsAt on genuine success (non-zero cost).
441
+ // When rate-limited, the SDK sends result with zero cost right after
442
+ // rate_limit_event; clearing here would prevent auto-continue scheduling.
443
+ if (parsed.total_cost_usd && parsed.total_cost_usd > 0) {
444
+ session.rateLimitResetsAt = null;
445
+ }
446
+ console.log("[sdk-bridge] result handler: session " + session.localId + " cost=" + parsed.total_cost_usd + " rateLimitResetsAt=" + session.rateLimitResetsAt);
447
+
448
+ // Handle SDK execution errors: show the error to the user instead of
449
+ // silently swallowing it. These have subtype "error_during_execution".
450
+ if (parsed.subtype === "error_during_execution") {
451
+ var execErrors = parsed.errors || [];
452
+ var execError = execErrors.length > 0
453
+ ? execErrors.join("; ")
454
+ : "Unknown SDK error";
455
+ if (parsed.terminal_reason) execError += " (reason: " + parsed.terminal_reason + ")";
456
+ console.error("[sdk-bridge] Execution error for session " + session.localId + ": " + execError);
457
+ session.isProcessing = false;
458
+ onProcessingChanged();
459
+ sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
460
+ sendAndRecord(session, { type: "done", code: 1 });
461
+ sm.broadcastSessionList();
462
+ return;
463
+ }
464
+
440
465
  session.isProcessing = false;
441
- session.rateLimitResetsAt = null; // clear on success
442
466
  onProcessingChanged();
467
+ // Detect "Not logged in" scenario early for the check below
468
+ var previewTrimmed = (session.responsePreview || "").trim();
469
+ var isZeroCost = !parsed.total_cost_usd || parsed.total_cost_usd === 0;
470
+ var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
471
+ && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
443
472
  // Fetch rich context usage breakdown (fire-and-forget, non-blocking)
444
473
  if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
445
474
  session.queryInstance.getContextUsage().then(function(ctxUsage) {
@@ -465,10 +494,6 @@ function createSDKBridge(opts) {
465
494
  }
466
495
  // Detect "Not logged in · Please run /login" from SDK.
467
496
  // This is a short canned response with zero cost, not actual AI output.
468
- var previewTrimmed = (session.responsePreview || "").trim();
469
- var isZeroCost = !parsed.total_cost_usd || parsed.total_cost_usd === 0;
470
- var isLoginPrompt = isZeroCost && previewTrimmed.length < 100
471
- && /not logged in/i.test(previewTrimmed) && /\/login/i.test(previewTrimmed);
472
497
  if (isLoginPrompt) {
473
498
  var authUser = session.ownerId ? usersModule.findUserById(session.ownerId) : null;
474
499
  var authLinuxUser = authUser && authUser.linuxUser ? authUser.linuxUser : null;
@@ -573,6 +598,7 @@ function createSDKBridge(opts) {
573
598
 
574
599
  } else if (parsed.type === "rate_limit_event" && parsed.rate_limit_info) {
575
600
  var info = parsed.rate_limit_info;
601
+ console.log("[sdk-bridge] rate_limit_event for session " + session.localId + ": status=" + info.status + " resetsAt=" + info.resetsAt + " isUsingOverage=" + info.isUsingOverage + " isProcessing=" + session.isProcessing);
576
602
 
577
603
  // Broadcast reset time for top-bar usage link
578
604
  if (info.rateLimitType && info.resetsAt) {
@@ -597,6 +623,33 @@ function createSDKBridge(opts) {
597
623
  // Track rejection for auto-continue / scheduled message support
598
624
  if (info.status === "rejected" && info.resetsAt) {
599
625
  session.rateLimitResetsAt = info.resetsAt * 1000;
626
+
627
+ // Schedule auto-continue immediately on rejection (don't wait for
628
+ // query completion which has timing issues with worker/non-worker paths).
629
+ if (!session.scheduledMessage && !session.destroying) {
630
+ var acEnabled = session.onQueryComplete ||
631
+ (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
632
+ console.log("[sdk-bridge] rate_limit rejected: acEnabled=" + acEnabled + " overage=" + !!info.isUsingOverage + " session=" + session.localId);
633
+ if (acEnabled) {
634
+ session.rateLimitAutoContinuePending = true;
635
+ if (info.isUsingOverage) {
636
+ // Extra usage available: send continue immediately (5s delay for query to finish)
637
+ console.log("[sdk-bridge] Overage available, sending immediate continue for session " + session.localId);
638
+ session.rateLimitResetsAt = null;
639
+ if (typeof opts.scheduleMessage === "function") {
640
+ opts.scheduleMessage(session, "continue", Date.now());
641
+ }
642
+ } else {
643
+ // No overage: schedule after rate limit resets
644
+ var acResetsAt = session.rateLimitResetsAt;
645
+ session.rateLimitResetsAt = null;
646
+ console.log("[sdk-bridge] Scheduling auto-continue on rate limit rejection for session " + session.localId);
647
+ if (typeof opts.scheduleMessage === "function") {
648
+ opts.scheduleMessage(session, "continue", acResetsAt);
649
+ }
650
+ }
651
+ }
652
+ }
600
653
  }
601
654
  }
602
655
 
@@ -1230,7 +1283,24 @@ function createSDKBridge(opts) {
1230
1283
  sm.broadcastSessionList();
1231
1284
  }
1232
1285
  cleanupSessionWorker(session, worker);
1233
- if (session.onQueryComplete) {
1286
+ // Mark session as done so late rate_limit_event can detect race condition
1287
+ session.isProcessing = false;
1288
+ // Auto-continue on rate limit (scheduler sessions, or user setting)
1289
+ var doneDidScheduleAC = false;
1290
+ var doneACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
1291
+ console.log("[sdk-bridge] query_done: session " + session.localId + " rateLimitResetsAt=" + session.rateLimitResetsAt + " acEnabled=" + doneACEnabled + " destroying=" + session.destroying + " scheduledMessage=" + !!session.scheduledMessage);
1292
+ if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
1293
+ && doneACEnabled && !session.destroying) {
1294
+ var doneResetsAt = session.rateLimitResetsAt;
1295
+ session.rateLimitResetsAt = null;
1296
+ session.rateLimitAutoContinuePending = true;
1297
+ doneDidScheduleAC = true;
1298
+ console.log("[sdk-bridge] Rate limited (worker/query_done), scheduling auto-continue for session " + session.localId);
1299
+ if (typeof opts.scheduleMessage === "function") {
1300
+ opts.scheduleMessage(session, "continue", doneResetsAt);
1301
+ }
1302
+ }
1303
+ if (session.onQueryComplete && !doneDidScheduleAC) {
1234
1304
  try { session.onQueryComplete(session); } catch (err) {
1235
1305
  console.error("[sdk-bridge] onQueryComplete error:", err.message || err);
1236
1306
  }
@@ -1321,6 +1391,8 @@ function createSDKBridge(opts) {
1321
1391
  sm.broadcastSessionList();
1322
1392
  }
1323
1393
  cleanupSessionWorker(session, worker);
1394
+ // Mark session as done so late rate_limit_event can detect race condition
1395
+ session.isProcessing = false;
1324
1396
  // Auto-continue on rate limit (scheduler sessions, or user setting)
1325
1397
  var workerDidScheduleAC = false;
1326
1398
  var workerACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
@@ -1495,24 +1567,15 @@ function createSDKBridge(opts) {
1495
1567
 
1496
1568
  // --- SDK query lifecycle ---
1497
1569
 
1498
- function handleCanUseTool(session, toolName, input, opts) {
1499
- // Ralph Loop execution: auto-approve all tools, deny interactive ones.
1500
- // Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
1501
- if (session.loop && session.loop.active && session.loop.role !== "crafting") {
1502
- if (toolName === "AskUserQuestion") {
1503
- return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
1504
- }
1505
- if (toolName === "EnterPlanMode") {
1506
- return Promise.resolve({ behavior: "deny", message: "Do not enter plan mode. Execute directly." });
1507
- }
1508
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1509
- }
1510
-
1570
+ // Check if a tool should be auto-approved based on whitelist rules.
1571
+ // Returns { behavior: "allow", updatedInput } if whitelisted, or null if not.
1572
+ // Shared by handleCanUseTool and mate mention canUseTool handlers.
1573
+ function checkToolWhitelist(toolName, input) {
1511
1574
  // Auto-approve read-only tools for ALL sessions.
1512
1575
  // These tools only inspect files and fetch data — no side effects.
1513
1576
  var readOnlyTools = { Read: true, Glob: true, Grep: true, WebFetch: true, WebSearch: true };
1514
1577
  if (readOnlyTools[toolName]) {
1515
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1578
+ return { behavior: "allow", updatedInput: input };
1516
1579
  }
1517
1580
 
1518
1581
  // Auto-approve safe browser MCP tools.
@@ -1523,10 +1586,17 @@ function createSDKBridge(opts) {
1523
1586
  if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
1524
1587
  var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
1525
1588
  if (safeBrowserTools[mcpToolName]) {
1526
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1589
+ return { behavior: "allow", updatedInput: input };
1527
1590
  }
1528
1591
  }
1529
1592
 
1593
+ // Auto-approve debate MCP tools (propose_debate).
1594
+ // These are user-facing tools that show inline approval cards,
1595
+ // so the permission prompt is redundant.
1596
+ if (toolName.indexOf("mcp__clay-debate__") === 0) {
1597
+ return { behavior: "allow", updatedInput: input };
1598
+ }
1599
+
1530
1600
  // Auto-approve safe Bash commands (read-only, non-destructive)
1531
1601
  // Applies to ALL sessions (mates and regular projects alike).
1532
1602
  // These are purely read-only commands that cannot modify files, install
@@ -1591,10 +1661,32 @@ function createSDKBridge(opts) {
1591
1661
  if (!safeBashCommands[firstWord]) { allSafe = false; break; }
1592
1662
  }
1593
1663
  if (allSafe) {
1594
- return Promise.resolve({ behavior: "allow", updatedInput: input });
1664
+ return { behavior: "allow", updatedInput: input };
1595
1665
  }
1596
1666
  }
1597
1667
 
1668
+ return null; // Not whitelisted
1669
+ }
1670
+
1671
+ function handleCanUseTool(session, toolName, input, opts) {
1672
+ // Ralph Loop execution: auto-approve all tools, deny interactive ones.
1673
+ // Crafting sessions are interactive — user and Claude collaborate to build PROMPT.md / JUDGE.md.
1674
+ if (session.loop && session.loop.active && session.loop.role !== "crafting") {
1675
+ if (toolName === "AskUserQuestion") {
1676
+ return Promise.resolve({ behavior: "deny", message: "Autonomous mode. Make your own decision." });
1677
+ }
1678
+ if (toolName === "EnterPlanMode") {
1679
+ return Promise.resolve({ behavior: "deny", message: "Do not enter plan mode. Execute directly." });
1680
+ }
1681
+ return Promise.resolve({ behavior: "allow", updatedInput: input });
1682
+ }
1683
+
1684
+ // Check shared whitelist (read-only tools, safe browser tools, safe bash commands)
1685
+ var whitelisted = checkToolWhitelist(toolName, input);
1686
+ if (whitelisted) {
1687
+ return Promise.resolve(whitelisted);
1688
+ }
1689
+
1598
1690
  // AskUserQuestion: wait for user answers via WebSocket
1599
1691
  if (toolName === "AskUserQuestion") {
1600
1692
  return new Promise(function(resolve) {
@@ -1836,6 +1928,10 @@ function createSDKBridge(opts) {
1836
1928
  session.pendingElicitations = {};
1837
1929
 
1838
1930
  // Auto-continue on rate limit (scheduler sessions, or user setting)
1931
+ // Mark session as done processing so the late rate_limit_event handler
1932
+ // can detect the race condition and schedule auto-continue itself.
1933
+ session.isProcessing = false;
1934
+
1839
1935
  var didScheduleAutoContinue = false;
1840
1936
  var acEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
1841
1937
  if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
@@ -1848,6 +1944,10 @@ function createSDKBridge(opts) {
1848
1944
  if (typeof opts.scheduleMessage === "function") {
1849
1945
  opts.scheduleMessage(session, "continue", acResetsAt);
1850
1946
  }
1947
+ } else if (acEnabled && !session.destroying) {
1948
+ // Log why auto-continue was not scheduled (for debugging)
1949
+ console.log("[sdk-bridge] Query done, auto-continue enabled but not scheduled: rateLimitResetsAt=" +
1950
+ session.rateLimitResetsAt + " (will rely on late rate_limit_event handler)");
1851
1951
  }
1852
1952
 
1853
1953
  // Ralph Loop: notify completion so loop orchestrator can proceed
@@ -2449,6 +2549,7 @@ function createSDKBridge(opts) {
2449
2549
  return {
2450
2550
  createMessageQueue: createMessageQueue,
2451
2551
  processSDKMessage: processSDKMessage,
2552
+ checkToolWhitelist: checkToolWhitelist,
2452
2553
  handleCanUseTool: handleCanUseTool,
2453
2554
  handleElicitation: handleElicitation,
2454
2555
  processQueryStream: processQueryStream,
package/lib/sessions.js CHANGED
@@ -237,10 +237,8 @@ function createSessionManager(opts) {
237
237
  return [...sessions.values()].filter(function (s) {
238
238
  if (s.hidden) return false;
239
239
  if (!multiUser) {
240
- // Single-user mode: only show sessions without ownerId
241
240
  return !s.ownerId;
242
241
  }
243
- // Multi-user mode: include all sessions (per-user filtering done by canAccessSession)
244
242
  return true;
245
243
  });
246
244
  }
@@ -507,6 +505,8 @@ function createSessionManager(opts) {
507
505
  }
508
506
 
509
507
  function doSendAndRecord(session, obj) {
508
+ // Stamp every recorded message so history replay preserves original times
509
+ if (!obj._ts) obj._ts = Date.now();
510
510
  session.history.push(obj);
511
511
  appendToSessionFile(session, obj);
512
512
  if (sendEach) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.9",
3
+ "version": "2.26.0",
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",