clay-server 2.26.0-beta.1 → 2.26.0-beta.11
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/bin/cli.js +5 -9
- package/lib/browser-mcp-server.js +496 -0
- package/lib/daemon.js +1 -1
- package/lib/os-users.js +23 -0
- package/lib/project-debate.js +243 -95
- package/lib/project-mate-interaction.js +766 -0
- package/lib/project-memory.js +677 -0
- package/lib/project.js +546 -1361
- package/lib/public/app.js +817 -175
- package/lib/public/css/debate.css +224 -2
- package/lib/public/css/icon-strip.css +10 -10
- package/lib/public/css/input.css +296 -83
- package/lib/public/css/mates.css +56 -57
- package/lib/public/css/mention.css +7 -4
- package/lib/public/css/menus.css +7 -0
- package/lib/public/css/messages.css +17 -0
- package/lib/public/css/mobile-nav.css +3 -1
- package/lib/public/css/overlays.css +181 -0
- package/lib/public/css/rewind.css +79 -0
- package/lib/public/css/server-settings.css +1 -0
- package/lib/public/css/sidebar.css +10 -0
- package/lib/public/css/title-bar.css +189 -3
- package/lib/public/index.html +53 -16
- package/lib/public/modules/context-sources.js +328 -0
- package/lib/public/modules/debate.js +184 -97
- package/lib/public/modules/input.js +18 -1
- package/lib/public/modules/mate-knowledge.js +11 -11
- package/lib/public/modules/mate-memory.js +5 -5
- package/lib/public/modules/mate-sidebar.js +13 -9
- package/lib/public/modules/mention.js +40 -2
- package/lib/public/modules/notifications.js +109 -1
- package/lib/public/modules/rewind.js +36 -0
- package/lib/public/modules/sidebar.js +107 -28
- package/lib/public/modules/terminal.js +8 -0
- package/lib/public/modules/theme.js +2 -1
- package/lib/public/modules/tools.js +69 -24
- package/lib/sdk-bridge.js +81 -7
- package/lib/sdk-worker.js +13 -1
- package/lib/server.js +42 -0
- package/lib/sessions.js +39 -7
- package/lib/terminal-manager.js +36 -6
- package/package.json +2 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
2
|
-
import { iconHtml, refreshIcons
|
|
2
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
4
4
|
import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
|
|
5
5
|
import { openFile } from './filebrowser.js';
|
|
6
6
|
import { mateAvatarUrl } from './avatar.js';
|
|
7
|
+
import { getChatLayout } from './theme.js';
|
|
7
8
|
|
|
8
9
|
var ctx;
|
|
9
10
|
|
|
@@ -496,12 +497,17 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
|
|
|
496
497
|
return;
|
|
497
498
|
}
|
|
498
499
|
|
|
499
|
-
// Mate DM:
|
|
500
|
-
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
501
|
-
|
|
500
|
+
// Channel layout or Mate DM: conversational "Can I ...?" style
|
|
501
|
+
if ((ctx.isMateDm && ctx.isMateDm()) || getChatLayout() === "channel") {
|
|
502
|
+
renderConversationalPermission(requestId, toolName, toolInput, mateId);
|
|
502
503
|
return;
|
|
503
504
|
}
|
|
504
505
|
|
|
506
|
+
// Bubble layout: formal "Permission Required" dialog
|
|
507
|
+
renderFormalPermission(requestId, toolName, toolInput, decisionReason);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function renderFormalPermission(requestId, toolName, toolInput, decisionReason) {
|
|
505
511
|
var container = document.createElement("div");
|
|
506
512
|
container.className = "permission-container";
|
|
507
513
|
container.dataset.requestId = requestId;
|
|
@@ -755,18 +761,40 @@ function matePermissionInfo(toolName, toolInput) {
|
|
|
755
761
|
return { verb: verb, target: target };
|
|
756
762
|
}
|
|
757
763
|
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
764
|
+
function resolvePermissionIdentity(mateId) {
|
|
765
|
+
// Mate DM: use DM target mate info
|
|
766
|
+
if (ctx.isMateDm && ctx.isMateDm()) {
|
|
767
|
+
var name = ctx.getMateName();
|
|
768
|
+
var avatar = ctx.getMateAvatarUrl();
|
|
769
|
+
// Override if specific mateId provided (e.g. @mention)
|
|
770
|
+
if (mateId && ctx.getMateById) {
|
|
771
|
+
var mentionMate = ctx.getMateById(mateId);
|
|
772
|
+
if (mentionMate) {
|
|
773
|
+
name = (mentionMate.profile && mentionMate.profile.displayName) || mentionMate.displayName || mentionMate.name || name;
|
|
774
|
+
avatar = mateAvatarUrl(mentionMate, 36);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return { name: name, avatar: avatar };
|
|
778
|
+
}
|
|
779
|
+
// Channel with Mate mention
|
|
763
780
|
if (mateId && ctx.getMateById) {
|
|
764
|
-
var
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|
|
781
|
+
var mate = ctx.getMateById(mateId);
|
|
782
|
+
if (mate) {
|
|
783
|
+
return {
|
|
784
|
+
name: (mate.profile && mate.profile.displayName) || mate.displayName || mate.name || "Mate",
|
|
785
|
+
avatar: mateAvatarUrl(mate, 36)
|
|
786
|
+
};
|
|
768
787
|
}
|
|
769
788
|
}
|
|
789
|
+
// Project chat (Claude Code)
|
|
790
|
+
return {
|
|
791
|
+
name: "Claude Code",
|
|
792
|
+
avatar: ctx.getClaudeAvatar ? ctx.getClaudeAvatar() : ""
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function renderConversationalPermission(requestId, toolName, toolInput, mateId) {
|
|
797
|
+
var identity = resolvePermissionIdentity(mateId);
|
|
770
798
|
var info = matePermissionInfo(toolName, toolInput);
|
|
771
799
|
var askMsg = "Can I " + info.verb + (info.target ? " " + info.target : "") + "?";
|
|
772
800
|
|
|
@@ -777,7 +805,7 @@ function renderMatePermission(requestId, toolName, toolInput, mateId) {
|
|
|
777
805
|
// Avatar (left column)
|
|
778
806
|
var avi = document.createElement("img");
|
|
779
807
|
avi.className = "dm-bubble-avatar dm-bubble-avatar-mate";
|
|
780
|
-
avi.src =
|
|
808
|
+
avi.src = identity.avatar;
|
|
781
809
|
avi.alt = "";
|
|
782
810
|
container.appendChild(avi);
|
|
783
811
|
|
|
@@ -789,7 +817,7 @@ function renderMatePermission(requestId, toolName, toolInput, mateId) {
|
|
|
789
817
|
var headerRow = document.createElement("div");
|
|
790
818
|
headerRow.className = "dm-bubble-header";
|
|
791
819
|
headerRow.innerHTML =
|
|
792
|
-
'<span class="dm-bubble-name">' + escapeHtml(
|
|
820
|
+
'<span class="dm-bubble-name">' + escapeHtml(identity.name) + '</span>' +
|
|
793
821
|
'<span class="dm-bubble-time">' + String(new Date().getHours()).padStart(2, "0") + ":" + String(new Date().getMinutes()).padStart(2, "0") + '</span>';
|
|
794
822
|
content.appendChild(headerRow);
|
|
795
823
|
|
|
@@ -1409,12 +1437,11 @@ export function startThinking() {
|
|
|
1409
1437
|
var el = thinkingGroup.el;
|
|
1410
1438
|
el.classList.remove("done");
|
|
1411
1439
|
el.querySelector(".thinking-content").textContent = "";
|
|
1412
|
-
// Mate mode: restore
|
|
1440
|
+
// Mate mode: restore dots activity row, hide thinking header
|
|
1413
1441
|
if (el.classList.contains("mate-thinking")) {
|
|
1414
1442
|
var actRow = el.querySelector(".mate-thinking-activity");
|
|
1415
1443
|
if (actRow) {
|
|
1416
1444
|
actRow.style.display = "";
|
|
1417
|
-
actRow.querySelector(".activity-text").textContent = randomThinkingVerb() + "...";
|
|
1418
1445
|
}
|
|
1419
1446
|
var header = el.querySelector(".thinking-header");
|
|
1420
1447
|
if (header) header.style.display = "none";
|
|
@@ -1423,7 +1450,7 @@ export function startThinking() {
|
|
|
1423
1450
|
refreshIcons();
|
|
1424
1451
|
ctx.scrollToBottom();
|
|
1425
1452
|
if (!el.classList.contains("mate-thinking")) {
|
|
1426
|
-
ctx.setActivity(
|
|
1453
|
+
ctx.setActivity("thinking");
|
|
1427
1454
|
}
|
|
1428
1455
|
return;
|
|
1429
1456
|
}
|
|
@@ -1439,10 +1466,7 @@ export function startThinking() {
|
|
|
1439
1466
|
'<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
|
|
1440
1467
|
'<div class="dm-bubble-content">' +
|
|
1441
1468
|
'<div class="dm-bubble-header"><span class="dm-bubble-name">' + escapeHtml(mateName) + '</span></div>' +
|
|
1442
|
-
'<div class="
|
|
1443
|
-
'<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
|
|
1444
|
-
'<span class="activity-text">' + randomThinkingVerb() + '...</span>' +
|
|
1445
|
-
'</div>' +
|
|
1469
|
+
'<div class="mate-thinking-dots mate-thinking-activity"><span></span><span></span><span></span></div>' +
|
|
1446
1470
|
'<div class="thinking-header" style="display:none">' +
|
|
1447
1471
|
'<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
|
|
1448
1472
|
'<span class="thinking-label">Thinking</span>' +
|
|
@@ -1472,7 +1496,7 @@ export function startThinking() {
|
|
|
1472
1496
|
thinkingGroup = { el: el, count: 0, totalDuration: 0 };
|
|
1473
1497
|
currentThinking = { el: el, fullText: "", startTime: Date.now() };
|
|
1474
1498
|
if (!ctx.isMateDm()) {
|
|
1475
|
-
ctx.setActivity(
|
|
1499
|
+
ctx.setActivity("thinking");
|
|
1476
1500
|
}
|
|
1477
1501
|
}
|
|
1478
1502
|
|
|
@@ -2063,13 +2087,29 @@ export function markSubagentDone(parentToolId, status, summary, usage) {
|
|
|
2063
2087
|
if (usage) updateSubagentProgress(parentToolId, usage, null);
|
|
2064
2088
|
}
|
|
2065
2089
|
|
|
2090
|
+
var _lastCumulativeCost = 0;
|
|
2091
|
+
|
|
2092
|
+
export function resetTurnMetaCost() {
|
|
2093
|
+
_lastCumulativeCost = 0;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2066
2096
|
export function addTurnMeta(cost, duration) {
|
|
2067
2097
|
closeToolGroup();
|
|
2068
2098
|
var div = document.createElement("div");
|
|
2069
2099
|
div.className = "turn-meta";
|
|
2070
2100
|
div.dataset.turn = ctx.turnCounter;
|
|
2071
2101
|
var parts = [];
|
|
2072
|
-
if (cost != null)
|
|
2102
|
+
if (cost != null) {
|
|
2103
|
+
// cost is cumulative total_cost_usd from the SDK.
|
|
2104
|
+
// When the SDK session restarts, total_cost_usd resets to 0 so cost
|
|
2105
|
+
// can drop below _lastCumulativeCost. In that case the entire cost
|
|
2106
|
+
// value IS the delta for this turn (fresh SDK session).
|
|
2107
|
+
var delta = cost - _lastCumulativeCost;
|
|
2108
|
+
if (delta < 0) delta = cost;
|
|
2109
|
+
_lastCumulativeCost = cost;
|
|
2110
|
+
var deltaStr = delta > 0 ? "+$" + delta.toFixed(4) : "$0.0000";
|
|
2111
|
+
parts.push(deltaStr + " \u2192 $" + cost.toFixed(4));
|
|
2112
|
+
}
|
|
2073
2113
|
if (duration != null) parts.push((duration / 1000).toFixed(1) + "s");
|
|
2074
2114
|
if (parts.length) {
|
|
2075
2115
|
div.textContent = parts.join(" \u00b7 ");
|
|
@@ -2114,6 +2154,7 @@ export function saveToolState() {
|
|
|
2114
2154
|
currentToolGroup: currentToolGroup,
|
|
2115
2155
|
toolGroupCounter: toolGroupCounter,
|
|
2116
2156
|
toolGroups: toolGroups,
|
|
2157
|
+
lastCumulativeCost: _lastCumulativeCost,
|
|
2117
2158
|
};
|
|
2118
2159
|
}
|
|
2119
2160
|
|
|
@@ -2126,6 +2167,7 @@ export function restoreToolState(saved) {
|
|
|
2126
2167
|
currentToolGroup = saved.currentToolGroup;
|
|
2127
2168
|
toolGroupCounter = saved.toolGroupCounter;
|
|
2128
2169
|
toolGroups = saved.toolGroups;
|
|
2170
|
+
_lastCumulativeCost = saved.lastCumulativeCost || 0;
|
|
2129
2171
|
if (todoWidgetEl) {
|
|
2130
2172
|
setupTodoObserver();
|
|
2131
2173
|
}
|
|
@@ -2146,6 +2188,9 @@ export function resetToolState() {
|
|
|
2146
2188
|
currentToolGroup = null;
|
|
2147
2189
|
toolGroupCounter = 0;
|
|
2148
2190
|
toolGroups = {};
|
|
2191
|
+
// NOTE: do NOT reset _lastCumulativeCost here — it must persist across
|
|
2192
|
+
// turns so addTurnMeta can compute per-turn deltas. It is only cleared
|
|
2193
|
+
// on new conversation via resetTurnMetaCost().
|
|
2149
2194
|
var stickyEl = document.getElementById("todo-sticky");
|
|
2150
2195
|
if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
|
|
2151
2196
|
}
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -134,6 +134,7 @@ function createSDKBridge(opts) {
|
|
|
134
134
|
var mateDisplayName = opts.mateDisplayName || "";
|
|
135
135
|
var isMate = opts.isMate || (slug.indexOf("mate-") === 0);
|
|
136
136
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
137
|
+
var mcpServers = opts.mcpServers || null;
|
|
137
138
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
138
139
|
var onTurnDone = opts.onTurnDone || null;
|
|
139
140
|
|
|
@@ -188,6 +189,10 @@ function createSDKBridge(opts) {
|
|
|
188
189
|
sm.sendAndRecord(session, obj);
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
function sendToSession(session, obj) {
|
|
193
|
+
sm.sendToSession(session, obj);
|
|
194
|
+
}
|
|
195
|
+
|
|
191
196
|
function processSDKMessage(session, parsed) {
|
|
192
197
|
// Timing: log key SDK milestones relative to query start
|
|
193
198
|
if (session._queryStartTs) {
|
|
@@ -435,6 +440,15 @@ function createSDKBridge(opts) {
|
|
|
435
440
|
session.isProcessing = false;
|
|
436
441
|
session.rateLimitResetsAt = null; // clear on success
|
|
437
442
|
onProcessingChanged();
|
|
443
|
+
// Fetch rich context usage breakdown (fire-and-forget, non-blocking)
|
|
444
|
+
if (session.queryInstance && typeof session.queryInstance.getContextUsage === "function") {
|
|
445
|
+
session.queryInstance.getContextUsage().then(function(ctxUsage) {
|
|
446
|
+
session.lastContextUsage = ctxUsage;
|
|
447
|
+
sendToSession(session, { type: "context_usage", data: ctxUsage });
|
|
448
|
+
}).catch(function(e) {
|
|
449
|
+
console.error("[sdk-bridge] getContextUsage failed (non-fatal):", e.message || e);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
438
452
|
var lastStreamInput = session.lastStreamInputTokens || null;
|
|
439
453
|
session.lastStreamInputTokens = null;
|
|
440
454
|
sendAndRecord(session, {
|
|
@@ -581,8 +595,26 @@ function createSDKBridge(opts) {
|
|
|
581
595
|
isUsingOverage: info.isUsingOverage || false,
|
|
582
596
|
});
|
|
583
597
|
// Track rejection for auto-continue / scheduled message support
|
|
584
|
-
if (
|
|
598
|
+
// Skip if using overage (extra usage) — user can continue immediately
|
|
599
|
+
if (info.status === "rejected" && info.resetsAt && !info.isUsingOverage) {
|
|
585
600
|
session.rateLimitResetsAt = info.resetsAt * 1000;
|
|
601
|
+
|
|
602
|
+
// Defensive: if query already completed before this event arrived,
|
|
603
|
+
// schedule auto-continue now (handles race condition where
|
|
604
|
+
// rate_limit_event arrives after the result/done event).
|
|
605
|
+
if (!session.isProcessing && !session.scheduledMessage && !session.destroying) {
|
|
606
|
+
var lateACEnabled = session.onQueryComplete ||
|
|
607
|
+
(typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
608
|
+
if (lateACEnabled) {
|
|
609
|
+
var lateResetsAt = session.rateLimitResetsAt;
|
|
610
|
+
session.rateLimitResetsAt = null;
|
|
611
|
+
session.rateLimitAutoContinuePending = true;
|
|
612
|
+
console.log("[sdk-bridge] Rate limit event arrived late, scheduling auto-continue for session " + session.localId);
|
|
613
|
+
if (typeof opts.scheduleMessage === "function") {
|
|
614
|
+
opts.scheduleMessage(session, "continue", lateResetsAt);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
586
618
|
}
|
|
587
619
|
}
|
|
588
620
|
|
|
@@ -1115,6 +1147,7 @@ function createSDKBridge(opts) {
|
|
|
1115
1147
|
agentProgressSummaries: true,
|
|
1116
1148
|
};
|
|
1117
1149
|
|
|
1150
|
+
if (mcpServers) queryOptions.mcpServers = mcpServers;
|
|
1118
1151
|
if (sm.currentModel) queryOptions.model = sm.currentModel;
|
|
1119
1152
|
if (sm.currentEffort) queryOptions.effort = sm.currentEffort;
|
|
1120
1153
|
if (sm.currentBetas && sm.currentBetas.length > 0) queryOptions.betas = sm.currentBetas;
|
|
@@ -1138,6 +1171,8 @@ function createSDKBridge(opts) {
|
|
|
1138
1171
|
if (session.lastRewindUuid) {
|
|
1139
1172
|
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
1140
1173
|
delete session.lastRewindUuid;
|
|
1174
|
+
// Persist the deletion so server restarts don't re-use a stale UUID
|
|
1175
|
+
sm.saveSessionFile(session);
|
|
1141
1176
|
}
|
|
1142
1177
|
}
|
|
1143
1178
|
|
|
@@ -1194,6 +1229,11 @@ function createSDKBridge(opts) {
|
|
|
1194
1229
|
});
|
|
1195
1230
|
break;
|
|
1196
1231
|
|
|
1232
|
+
case "context_usage":
|
|
1233
|
+
session.lastContextUsage = msg.data;
|
|
1234
|
+
sendToSession(session, { type: "context_usage", data: msg.data });
|
|
1235
|
+
break;
|
|
1236
|
+
|
|
1197
1237
|
case "query_done":
|
|
1198
1238
|
console.log("[sdk-bridge] IPC query_done received, pid=" + (worker.process ? worker.process.pid : "?"));
|
|
1199
1239
|
// Mark that we received a proper IPC completion, so the exit
|
|
@@ -1299,6 +1339,8 @@ function createSDKBridge(opts) {
|
|
|
1299
1339
|
sm.broadcastSessionList();
|
|
1300
1340
|
}
|
|
1301
1341
|
cleanupSessionWorker(session, worker);
|
|
1342
|
+
// Mark session as done so late rate_limit_event can detect race condition
|
|
1343
|
+
session.isProcessing = false;
|
|
1302
1344
|
// Auto-continue on rate limit (scheduler sessions, or user setting)
|
|
1303
1345
|
var workerDidScheduleAC = false;
|
|
1304
1346
|
var workerACEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
@@ -1493,6 +1535,18 @@ function createSDKBridge(opts) {
|
|
|
1493
1535
|
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1494
1536
|
}
|
|
1495
1537
|
|
|
1538
|
+
// Auto-approve safe browser MCP tools.
|
|
1539
|
+
// Only watch/unwatch: user explicitly chose which tab to share.
|
|
1540
|
+
// Everything else (screenshot, read_page, list_tabs, etc.) can expose
|
|
1541
|
+
// content from tabs the user didn't intend to share, so require approval.
|
|
1542
|
+
var safeBrowserTools = { browser_watch_tab: true, browser_unwatch_tab: true };
|
|
1543
|
+
if (toolName.indexOf("mcp__") === 0 && toolName.indexOf("__browser_") !== -1) {
|
|
1544
|
+
var mcpToolName = toolName.substring(toolName.lastIndexOf("__") + 2);
|
|
1545
|
+
if (safeBrowserTools[mcpToolName]) {
|
|
1546
|
+
return Promise.resolve({ behavior: "allow", updatedInput: input });
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1496
1550
|
// Auto-approve safe Bash commands (read-only, non-destructive)
|
|
1497
1551
|
// Applies to ALL sessions (mates and regular projects alike).
|
|
1498
1552
|
// These are purely read-only commands that cannot modify files, install
|
|
@@ -1687,10 +1741,16 @@ function createSDKBridge(opts) {
|
|
|
1687
1741
|
}
|
|
1688
1742
|
|
|
1689
1743
|
async function processQueryStream(session) {
|
|
1744
|
+
// Capture references at start so we only clean up OUR resources in finally,
|
|
1745
|
+
// not resources from a newer query that may have been created after an abort.
|
|
1746
|
+
var myQueryInstance = session.queryInstance;
|
|
1747
|
+
var myMessageQueue = session.messageQueue;
|
|
1748
|
+
var myAbortController = session.abortController;
|
|
1690
1749
|
try {
|
|
1691
|
-
for await (var msg of
|
|
1750
|
+
for await (var msg of myQueryInstance) {
|
|
1692
1751
|
processSDKMessage(session, msg);
|
|
1693
1752
|
}
|
|
1753
|
+
// (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
|
|
1694
1754
|
// Stream ended normally after a task stop — no "result" message was sent,
|
|
1695
1755
|
// so the session is still marked as processing. Send interrupted feedback.
|
|
1696
1756
|
if (session.isProcessing && session.taskStopRequested) {
|
|
@@ -1704,7 +1764,7 @@ function createSDKBridge(opts) {
|
|
|
1704
1764
|
if (session.isProcessing) {
|
|
1705
1765
|
session.isProcessing = false;
|
|
1706
1766
|
onProcessingChanged();
|
|
1707
|
-
if (err.name === "AbortError" || (
|
|
1767
|
+
if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
|
|
1708
1768
|
if (!session.destroying) {
|
|
1709
1769
|
sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
|
|
1710
1770
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
@@ -1777,22 +1837,29 @@ function createSDKBridge(opts) {
|
|
|
1777
1837
|
} finally {
|
|
1778
1838
|
// Close the SDK query to terminate the underlying claude child process.
|
|
1779
1839
|
// Without this, the process stays alive indefinitely (single-user mode).
|
|
1780
|
-
if
|
|
1840
|
+
// Only clean up if the session still references OUR resources.
|
|
1841
|
+
// A rewind + new startQuery may have already replaced these with
|
|
1842
|
+
// a newer query — clobbering them would kill the new query.
|
|
1843
|
+
if (session.queryInstance === myQueryInstance) {
|
|
1781
1844
|
try {
|
|
1782
1845
|
if (typeof session.queryInstance.close === "function") {
|
|
1783
1846
|
session.queryInstance.close();
|
|
1784
1847
|
}
|
|
1785
1848
|
} catch (e) {}
|
|
1849
|
+
session.queryInstance = null;
|
|
1786
1850
|
}
|
|
1787
|
-
session.
|
|
1788
|
-
session.
|
|
1789
|
-
session.abortController = null;
|
|
1851
|
+
if (session.messageQueue === myMessageQueue) session.messageQueue = null;
|
|
1852
|
+
if (session.abortController === myAbortController) session.abortController = null;
|
|
1790
1853
|
session.taskStopRequested = false;
|
|
1791
1854
|
session.pendingPermissions = {};
|
|
1792
1855
|
session.pendingAskUser = {};
|
|
1793
1856
|
session.pendingElicitations = {};
|
|
1794
1857
|
|
|
1795
1858
|
// Auto-continue on rate limit (scheduler sessions, or user setting)
|
|
1859
|
+
// Mark session as done processing so the late rate_limit_event handler
|
|
1860
|
+
// can detect the race condition and schedule auto-continue itself.
|
|
1861
|
+
session.isProcessing = false;
|
|
1862
|
+
|
|
1796
1863
|
var didScheduleAutoContinue = false;
|
|
1797
1864
|
var acEnabled = session.onQueryComplete || (typeof opts.getAutoContinueSetting === "function" && opts.getAutoContinueSetting(session));
|
|
1798
1865
|
if (session.rateLimitResetsAt && session.rateLimitResetsAt > Date.now()
|
|
@@ -1805,6 +1872,10 @@ function createSDKBridge(opts) {
|
|
|
1805
1872
|
if (typeof opts.scheduleMessage === "function") {
|
|
1806
1873
|
opts.scheduleMessage(session, "continue", acResetsAt);
|
|
1807
1874
|
}
|
|
1875
|
+
} else if (acEnabled && !session.destroying) {
|
|
1876
|
+
// Log why auto-continue was not scheduled (for debugging)
|
|
1877
|
+
console.log("[sdk-bridge] Query done, auto-continue enabled but not scheduled: rateLimitResetsAt=" +
|
|
1878
|
+
session.rateLimitResetsAt + " (will rely on late rate_limit_event handler)");
|
|
1808
1879
|
}
|
|
1809
1880
|
|
|
1810
1881
|
// Ralph Loop: notify completion so loop orchestrator can proceed
|
|
@@ -1911,6 +1982,7 @@ function createSDKBridge(opts) {
|
|
|
1911
1982
|
abortController: session.abortController,
|
|
1912
1983
|
promptSuggestions: true,
|
|
1913
1984
|
agentProgressSummaries: true,
|
|
1985
|
+
mcpServers: mcpServers || undefined,
|
|
1914
1986
|
canUseTool: function(toolName, input, toolOpts) {
|
|
1915
1987
|
return handleCanUseTool(session, toolName, input, toolOpts);
|
|
1916
1988
|
},
|
|
@@ -1952,6 +2024,8 @@ function createSDKBridge(opts) {
|
|
|
1952
2024
|
if (session.lastRewindUuid) {
|
|
1953
2025
|
queryOptions.resumeSessionAt = session.lastRewindUuid;
|
|
1954
2026
|
delete session.lastRewindUuid;
|
|
2027
|
+
// Persist the deletion so server restarts don't re-use a stale UUID
|
|
2028
|
+
sm.saveSessionFile(session);
|
|
1955
2029
|
}
|
|
1956
2030
|
}
|
|
1957
2031
|
|
package/lib/sdk-worker.js
CHANGED
|
@@ -351,7 +351,19 @@ async function handleQueryStart(msg) {
|
|
|
351
351
|
}
|
|
352
352
|
sendToDaemon({ type: "sdk_event", event: event });
|
|
353
353
|
}
|
|
354
|
-
perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "),
|
|
354
|
+
perf("all events streamed (counts=" + JSON.stringify(eventCounts) + "), fetching context usage");
|
|
355
|
+
// Fetch context usage breakdown before queryInstance is cleared
|
|
356
|
+
try {
|
|
357
|
+
if (queryInstance && typeof queryInstance.getContextUsage === "function") {
|
|
358
|
+
var ctxUsage = await queryInstance.getContextUsage();
|
|
359
|
+
sendToDaemon({ type: "context_usage", data: ctxUsage });
|
|
360
|
+
perf("context usage sent");
|
|
361
|
+
}
|
|
362
|
+
} catch (e) {
|
|
363
|
+
// Non-fatal: SDK may have already shut down
|
|
364
|
+
console.error("[sdk-worker] getContextUsage failed (non-fatal):", e.message);
|
|
365
|
+
}
|
|
366
|
+
perf("sending query_done");
|
|
355
367
|
sendToDaemon({ type: "query_done" });
|
|
356
368
|
} catch (err) {
|
|
357
369
|
var errMsg = err.message || String(err);
|
package/lib/server.js
CHANGED
|
@@ -38,6 +38,24 @@ function httpGet(url) {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function httpGetBinary(url) {
|
|
42
|
+
return new Promise(function (resolve, reject) {
|
|
43
|
+
var mod = url.startsWith("https") ? https : http;
|
|
44
|
+
mod.get(url, { headers: { "User-Agent": "Clay/1.0" } }, function (resp) {
|
|
45
|
+
if (resp.statusCode >= 300 && resp.statusCode < 400 && resp.headers.location) {
|
|
46
|
+
return httpGetBinary(resp.headers.location).then(resolve, reject);
|
|
47
|
+
}
|
|
48
|
+
if (resp.statusCode !== 200) {
|
|
49
|
+
return reject(new Error("HTTP " + resp.statusCode));
|
|
50
|
+
}
|
|
51
|
+
var chunks = [];
|
|
52
|
+
resp.on("data", function (c) { chunks.push(c); });
|
|
53
|
+
resp.on("end", function () { resolve(Buffer.concat(chunks)); });
|
|
54
|
+
resp.on("error", reject);
|
|
55
|
+
}).on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
function fetchSkillsPage(url) {
|
|
42
60
|
return httpGet(url).then(function (html) {
|
|
43
61
|
// Data is inside self.__next_f.push() with escaped quotes: \"initialSkills\":[{\"source\":...}]
|
|
@@ -1018,6 +1036,28 @@ function createServer(opts) {
|
|
|
1018
1036
|
return;
|
|
1019
1037
|
}
|
|
1020
1038
|
|
|
1039
|
+
// Chrome extension download (proxy from GitHub)
|
|
1040
|
+
if (fullUrl === "/api/extension/download" && req.method === "GET") {
|
|
1041
|
+
if (!isRequestAuthed(req)) {
|
|
1042
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1043
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
var archiveUrl = "https://github.com/chadbyte/clay-chrome/archive/refs/heads/main.zip";
|
|
1047
|
+
httpGetBinary(archiveUrl).then(function (buf) {
|
|
1048
|
+
res.writeHead(200, {
|
|
1049
|
+
"Content-Type": "application/zip",
|
|
1050
|
+
"Content-Disposition": 'attachment; filename="clay-chrome-extension.zip"',
|
|
1051
|
+
"Content-Length": buf.length,
|
|
1052
|
+
});
|
|
1053
|
+
res.end(buf);
|
|
1054
|
+
}).catch(function (err) {
|
|
1055
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
1056
|
+
res.end(JSON.stringify({ error: "Failed to download extension: " + (err.message || "unknown error") }));
|
|
1057
|
+
});
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1021
1061
|
// CORS preflight for cross-origin requests (HTTP onboarding → HTTPS)
|
|
1022
1062
|
if (req.method === "OPTIONS") {
|
|
1023
1063
|
res.writeHead(204, {
|
|
@@ -2889,6 +2929,8 @@ function createServer(opts) {
|
|
|
2889
2929
|
osUsers: osUsers,
|
|
2890
2930
|
currentVersion: currentVersion,
|
|
2891
2931
|
lanHost: lanHost,
|
|
2932
|
+
port: portNum,
|
|
2933
|
+
tls: !!tlsOptions,
|
|
2892
2934
|
getProjectCount: function () { return projects.size; },
|
|
2893
2935
|
getProjectList: function (userId) {
|
|
2894
2936
|
var list = [];
|
package/lib/sessions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
3
4
|
var config = require("./config");
|
|
4
5
|
var utils = require("./utils");
|
|
5
6
|
var users = require("./users");
|
|
@@ -75,7 +76,9 @@ function createSessionManager(opts) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function saveSessionFile(session) {
|
|
78
|
-
if (!session.cliSessionId)
|
|
79
|
+
if (!session.cliSessionId) {
|
|
80
|
+
session.cliSessionId = crypto.randomUUID();
|
|
81
|
+
}
|
|
79
82
|
try {
|
|
80
83
|
var metaObj = {
|
|
81
84
|
type: "meta",
|
|
@@ -96,17 +99,24 @@ function createSessionManager(opts) {
|
|
|
96
99
|
lines.push(JSON.stringify(session.history[i]));
|
|
97
100
|
}
|
|
98
101
|
var sfPath = sessionFilePath(session.cliSessionId);
|
|
99
|
-
|
|
102
|
+
// Atomic write: write to temp file then rename, so a crash mid-write
|
|
103
|
+
// cannot leave a truncated/corrupted session file.
|
|
104
|
+
var tmpPath = sfPath + ".tmp." + process.pid;
|
|
105
|
+
fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
|
|
100
106
|
if (process.platform !== "win32") {
|
|
101
|
-
try { fs.chmodSync(
|
|
107
|
+
try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
|
|
102
108
|
}
|
|
109
|
+
fs.renameSync(tmpPath, sfPath);
|
|
103
110
|
} catch(e) {
|
|
104
111
|
console.error("[session] Failed to save session file:", e.message);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
function appendToSessionFile(session, obj) {
|
|
109
|
-
if (!session.cliSessionId)
|
|
116
|
+
if (!session.cliSessionId) {
|
|
117
|
+
session.cliSessionId = crypto.randomUUID();
|
|
118
|
+
saveSessionFile(session);
|
|
119
|
+
}
|
|
110
120
|
session.lastActivity = Date.now();
|
|
111
121
|
try {
|
|
112
122
|
var afPath = sessionFilePath(session.cliSessionId);
|
|
@@ -123,6 +133,13 @@ function createSessionManager(opts) {
|
|
|
123
133
|
var files;
|
|
124
134
|
try { files = fs.readdirSync(sessionsDir); } catch { return; }
|
|
125
135
|
|
|
136
|
+
// Clean up stale temp files from interrupted atomic writes
|
|
137
|
+
for (var ti = 0; ti < files.length; ti++) {
|
|
138
|
+
if (files[ti].indexOf(".tmp.") !== -1) {
|
|
139
|
+
try { fs.unlinkSync(path.join(sessionsDir, files[ti])); } catch (e) {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
var loaded = [];
|
|
127
144
|
for (var i = 0; i < files.length; i++) {
|
|
128
145
|
if (!files[i].endsWith(".jsonl")) continue;
|
|
@@ -226,10 +243,8 @@ function createSessionManager(opts) {
|
|
|
226
243
|
return [...sessions.values()].filter(function (s) {
|
|
227
244
|
if (s.hidden) return false;
|
|
228
245
|
if (!multiUser) {
|
|
229
|
-
// Single-user mode: only show sessions without ownerId
|
|
230
246
|
return !s.ownerId;
|
|
231
247
|
}
|
|
232
|
-
// Multi-user mode: include all sessions (per-user filtering done by canAccessSession)
|
|
233
248
|
return true;
|
|
234
249
|
});
|
|
235
250
|
}
|
|
@@ -355,7 +370,7 @@ function createSessionManager(opts) {
|
|
|
355
370
|
}
|
|
356
371
|
}
|
|
357
372
|
|
|
358
|
-
_send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens });
|
|
373
|
+
_send({ type: "history_done", lastUsage: lastUsage, lastModelUsage: lastModelUsage, lastCost: lastCost, lastStreamInputTokens: lastStreamInputTokens, contextUsage: session.lastContextUsage || null });
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
function switchSession(localId, targetWs, transform) {
|
|
@@ -481,7 +496,23 @@ function createSessionManager(opts) {
|
|
|
481
496
|
sessions.delete(localId);
|
|
482
497
|
}
|
|
483
498
|
|
|
499
|
+
function doSendToSession(session, obj) {
|
|
500
|
+
// Send to active clients without recording to history/disk (ephemeral data)
|
|
501
|
+
if (sendEach) {
|
|
502
|
+
var data = JSON.stringify(obj);
|
|
503
|
+
sendEach(function (ws) {
|
|
504
|
+
if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
|
|
505
|
+
ws.send(data);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
} else if (session.localId === activeSessionId) {
|
|
509
|
+
send(obj);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
484
513
|
function doSendAndRecord(session, obj) {
|
|
514
|
+
// Stamp every recorded message so history replay preserves original times
|
|
515
|
+
if (!obj._ts) obj._ts = Date.now();
|
|
485
516
|
session.history.push(obj);
|
|
486
517
|
appendToSessionFile(session, obj);
|
|
487
518
|
if (sendEach) {
|
|
@@ -726,6 +757,7 @@ function createSessionManager(opts) {
|
|
|
726
757
|
saveSessionFile: saveSessionFile,
|
|
727
758
|
appendToSessionFile: appendToSessionFile,
|
|
728
759
|
sendAndRecord: doSendAndRecord,
|
|
760
|
+
sendToSession: doSendToSession,
|
|
729
761
|
findTurnBoundary: findTurnBoundary,
|
|
730
762
|
replayHistory: replayHistory,
|
|
731
763
|
searchSessions: searchSessions,
|