clay-server 2.26.0-beta.2 → 2.26.0-beta.3

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/project.js CHANGED
@@ -2204,6 +2204,8 @@ function createProjectContext(opts) {
2204
2204
  if (msg.type === "rewind_preview") {
2205
2205
  var session = getSessionForWs(ws);
2206
2206
  if (!session || !session.cliSessionId || !msg.uuid) return;
2207
+ // Reject preview requests while a rewind is executing
2208
+ if (session._rewindInProgress) return;
2207
2209
 
2208
2210
  (async function () {
2209
2211
  var result;
@@ -2233,6 +2235,12 @@ function createProjectContext(opts) {
2233
2235
  if (msg.type === "rewind_execute") {
2234
2236
  var session = getSessionForWs(ws);
2235
2237
  if (!session || !session.cliSessionId || !msg.uuid) return;
2238
+ // Guard against concurrent rewind executions
2239
+ if (session._rewindInProgress) {
2240
+ sendTo(ws, { type: "rewind_error", text: "Rewind already in progress." });
2241
+ return;
2242
+ }
2243
+ session._rewindInProgress = true;
2236
2244
  var mode = msg.mode || "both";
2237
2245
 
2238
2246
  (async function () {
@@ -2293,6 +2301,7 @@ function createProjectContext(opts) {
2293
2301
  } catch (err) {
2294
2302
  sendTo(ws, { type: "rewind_error", text: "Rewind failed: " + err.message });
2295
2303
  } finally {
2304
+ session._rewindInProgress = false;
2296
2305
  if (result && result.isTemp) result.cleanup();
2297
2306
  }
2298
2307
  })();
package/lib/public/app.js CHANGED
@@ -6,7 +6,7 @@ import { initSidebar, renderSessionList, handleSearchResults, updateSessionPrese
6
6
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
7
7
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
8
8
  import { initMateMemory, renderMemoryList, hideMemory } from './modules/mate-memory.js';
9
- import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
9
+ import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton, onRewindComplete, onRewindError } from './modules/rewind.js';
10
10
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
11
11
  import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage, hasSendableContent, setScheduleBtnDisabled, setScheduleDelayMs, clearScheduleDelay } from './modules/input.js';
12
12
  import { initQrCode, triggerShare } from './modules/qrcode.js';
@@ -14,7 +14,7 @@ import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFs
14
14
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
15
15
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
16
16
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
17
- import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
17
+ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
18
18
  import { initServerSettings, updateSettingsStats, updateSettingsModels, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './modules/server-settings.js';
19
19
  import { initProjectSettings, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, isProjectSettingsOpen, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './modules/project-settings.js';
20
20
  import { initSkills, handleSkillInstalled, handleSkillUninstalled } from './modules/skills.js';
@@ -2468,7 +2468,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2468
2468
  }
2469
2469
 
2470
2470
  function accumulateUsage(cost, usage) {
2471
- if (cost != null) sessionUsage.cost += cost;
2471
+ // cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
2472
+ // Assign directly instead of summing to avoid overcounting.
2473
+ if (cost != null) sessionUsage.cost = cost;
2472
2474
  if (usage) {
2473
2475
  sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
2474
2476
  sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
@@ -2666,7 +2668,8 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2666
2668
  }
2667
2669
 
2668
2670
  function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens) {
2669
- if (cost != null) contextData.cost += cost;
2671
+ // cost is the SDK's total_cost_usd — a cumulative running total, not a delta.
2672
+ if (cost != null) contextData.cost = cost;
2670
2673
  // Use latest turn values (not cumulative) since each turn's input_tokens
2671
2674
  // already includes the full conversation context up to that point
2672
2675
  if (usage) {
@@ -3718,6 +3721,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
3718
3721
  setStatus("connected");
3719
3722
  if (!loopActive) enableMainInput();
3720
3723
  resetUsage();
3724
+ resetTurnMetaCost();
3721
3725
  resetContext();
3722
3726
  // Clear header indicators
3723
3727
  clearRateLimitIndicator();
@@ -4711,6 +4715,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4711
4715
  break;
4712
4716
 
4713
4717
  case "rewind_complete":
4718
+ onRewindComplete();
4714
4719
  setRewindMode(false);
4715
4720
  var rewindText = "Rewound to earlier point. Files have been restored.";
4716
4721
  if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
@@ -4719,6 +4724,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4719
4724
  break;
4720
4725
 
4721
4726
  case "rewind_error":
4727
+ onRewindError();
4722
4728
  clearPendingRewindUuid();
4723
4729
  addSystemMessage(msg.text || "Rewind failed.", true);
4724
4730
  break;
@@ -4980,6 +4986,22 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4980
4986
  // Update mate sidebar if currently viewing this mate
4981
4987
  if (dmMode && dmTargetUser && dmTargetUser.isMate && dmTargetUser.id === msg.mate.id) {
4982
4988
  updateMateSidebarProfile(msg.mate);
4989
+ // Sync dmTargetUser so subsequent renders use fresh data
4990
+ var mp2 = msg.mate.profile || {};
4991
+ dmTargetUser.displayName = mp2.displayName || msg.mate.name || dmTargetUser.displayName;
4992
+ dmTargetUser.avatarStyle = mp2.avatarStyle || dmTargetUser.avatarStyle;
4993
+ dmTargetUser.avatarSeed = mp2.avatarSeed || dmTargetUser.avatarSeed;
4994
+ dmTargetUser.avatarColor = mp2.avatarColor || dmTargetUser.avatarColor;
4995
+ dmTargetUser.avatarCustom = mp2.avatarCustom || "";
4996
+ dmTargetUser.profile = mp2;
4997
+ // Refresh body dataset so new chat bubbles use the updated avatar
4998
+ document.body.dataset.mateAvatarUrl = mateAvatarUrl(dmTargetUser, 36);
4999
+ document.body.dataset.mateName = mp2.displayName || msg.mate.name || "";
5000
+ // Update existing chat bubble avatars
5001
+ var mateAvis = document.querySelectorAll(".dm-bubble-avatar-mate");
5002
+ for (var mbi = 0; mbi < mateAvis.length; mbi++) {
5003
+ mateAvis[mbi].src = document.body.dataset.mateAvatarUrl;
5004
+ }
4983
5005
  }
4984
5006
  // Update DM header if currently chatting with this mate
4985
5007
  if (dmMode && dmTargetUser && dmTargetUser.id === msg.mate.id) {
@@ -4,6 +4,8 @@ import { iconHtml, refreshIcons } from './icons.js';
4
4
  var ctx;
5
5
  var rewindMode = false;
6
6
  var pendingRewindUuid = null;
7
+ var rewindPreviewInFlight = false;
8
+ var rewindExecuting = false;
7
9
  var rewindBannerEl = null;
8
10
  var rewindScrollHandler = null;
9
11
  var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn, rewindModeOptions;
@@ -50,6 +52,17 @@ export function clearPendingRewindUuid() {
50
52
  pendingRewindUuid = null;
51
53
  }
52
54
 
55
+ export function onRewindComplete() {
56
+ rewindExecuting = false;
57
+ rewindPreviewInFlight = false;
58
+ pendingRewindUuid = null;
59
+ }
60
+
61
+ export function onRewindError() {
62
+ rewindExecuting = false;
63
+ rewindPreviewInFlight = false;
64
+ }
65
+
53
66
  function initiateRewind(uuid) {
54
67
  if (ctx.processing) {
55
68
  ctx.addSystemMessage("Cannot rewind while processing. Stop the current operation first.", true);
@@ -59,7 +72,16 @@ function initiateRewind(uuid) {
59
72
  ctx.addSystemMessage("No rewind point found for this turn.", true);
60
73
  return;
61
74
  }
75
+ if (rewindPreviewInFlight) {
76
+ // Debounce: ignore clicks while a preview is already in flight
77
+ return;
78
+ }
79
+ if (rewindExecuting) {
80
+ ctx.addSystemMessage("Rewind already in progress.", true);
81
+ return;
82
+ }
62
83
  pendingRewindUuid = uuid;
84
+ rewindPreviewInFlight = true;
63
85
  if (ctx.ws && ctx.connected) {
64
86
  ctx.ws.send(JSON.stringify({ type: "rewind_preview", uuid: uuid }));
65
87
  }
@@ -98,6 +120,17 @@ function updateSummaryForMode() {
98
120
  }
99
121
 
100
122
  export function showRewindModal(data) {
123
+ rewindPreviewInFlight = false;
124
+
125
+ // Ignore stale preview results that don't match current pending UUID
126
+ if (data.uuid && pendingRewindUuid && data.uuid !== pendingRewindUuid) {
127
+ return;
128
+ }
129
+ // Ignore if a rewind is already executing
130
+ if (rewindExecuting) {
131
+ return;
132
+ }
133
+
101
134
  var p = data.preview || data;
102
135
  var filePaths = p.filesChanged || p.filePaths || p.files || [];
103
136
  var fileCount = filePaths.length;
@@ -171,6 +204,7 @@ export function showRewindModal(data) {
171
204
  export function hideRewindModal() {
172
205
  rewindModal.classList.add("hidden");
173
206
  pendingRewindUuid = null;
207
+ rewindPreviewInFlight = false;
174
208
  }
175
209
 
176
210
  export function renderDiffPre(text) {
@@ -333,7 +367,9 @@ export function initRewind(_ctx) {
333
367
  });
334
368
 
335
369
  rewindConfirmBtn.addEventListener("click", function() {
370
+ if (rewindExecuting) return;
336
371
  if (pendingRewindUuid && ctx.ws && ctx.connected) {
372
+ rewindExecuting = true;
337
373
  var mode = getSelectedMode();
338
374
  ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid, mode: mode }));
339
375
  }
@@ -2063,13 +2063,29 @@ export function markSubagentDone(parentToolId, status, summary, usage) {
2063
2063
  if (usage) updateSubagentProgress(parentToolId, usage, null);
2064
2064
  }
2065
2065
 
2066
+ var _lastCumulativeCost = 0;
2067
+
2068
+ export function resetTurnMetaCost() {
2069
+ _lastCumulativeCost = 0;
2070
+ }
2071
+
2066
2072
  export function addTurnMeta(cost, duration) {
2067
2073
  closeToolGroup();
2068
2074
  var div = document.createElement("div");
2069
2075
  div.className = "turn-meta";
2070
2076
  div.dataset.turn = ctx.turnCounter;
2071
2077
  var parts = [];
2072
- if (cost != null) parts.push("$" + cost.toFixed(4));
2078
+ if (cost != null) {
2079
+ // cost is cumulative total_cost_usd from the SDK.
2080
+ // When the SDK session restarts, total_cost_usd resets to 0 so cost
2081
+ // can drop below _lastCumulativeCost. In that case the entire cost
2082
+ // value IS the delta for this turn (fresh SDK session).
2083
+ var delta = cost - _lastCumulativeCost;
2084
+ if (delta < 0) delta = cost;
2085
+ _lastCumulativeCost = cost;
2086
+ var deltaStr = delta > 0 ? "+$" + delta.toFixed(4) : "$0.0000";
2087
+ parts.push(deltaStr + " \u2192 $" + cost.toFixed(4));
2088
+ }
2073
2089
  if (duration != null) parts.push((duration / 1000).toFixed(1) + "s");
2074
2090
  if (parts.length) {
2075
2091
  div.textContent = parts.join(" \u00b7 ");
@@ -2114,6 +2130,7 @@ export function saveToolState() {
2114
2130
  currentToolGroup: currentToolGroup,
2115
2131
  toolGroupCounter: toolGroupCounter,
2116
2132
  toolGroups: toolGroups,
2133
+ lastCumulativeCost: _lastCumulativeCost,
2117
2134
  };
2118
2135
  }
2119
2136
 
@@ -2126,6 +2143,7 @@ export function restoreToolState(saved) {
2126
2143
  currentToolGroup = saved.currentToolGroup;
2127
2144
  toolGroupCounter = saved.toolGroupCounter;
2128
2145
  toolGroups = saved.toolGroups;
2146
+ _lastCumulativeCost = saved.lastCumulativeCost || 0;
2129
2147
  if (todoWidgetEl) {
2130
2148
  setupTodoObserver();
2131
2149
  }
@@ -2146,6 +2164,9 @@ export function resetToolState() {
2146
2164
  currentToolGroup = null;
2147
2165
  toolGroupCounter = 0;
2148
2166
  toolGroups = {};
2167
+ // NOTE: do NOT reset _lastCumulativeCost here — it must persist across
2168
+ // turns so addTurnMeta can compute per-turn deltas. It is only cleared
2169
+ // on new conversation via resetTurnMetaCost().
2149
2170
  var stickyEl = document.getElementById("todo-sticky");
2150
2171
  if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
2151
2172
  }
package/lib/sdk-bridge.js CHANGED
@@ -1138,6 +1138,8 @@ function createSDKBridge(opts) {
1138
1138
  if (session.lastRewindUuid) {
1139
1139
  queryOptions.resumeSessionAt = session.lastRewindUuid;
1140
1140
  delete session.lastRewindUuid;
1141
+ // Persist the deletion so server restarts don't re-use a stale UUID
1142
+ sm.saveSessionFile(session);
1141
1143
  }
1142
1144
  }
1143
1145
 
@@ -1687,8 +1689,13 @@ function createSDKBridge(opts) {
1687
1689
  }
1688
1690
 
1689
1691
  async function processQueryStream(session) {
1692
+ // Capture references at start so we only clean up OUR resources in finally,
1693
+ // not resources from a newer query that may have been created after an abort.
1694
+ var myQueryInstance = session.queryInstance;
1695
+ var myMessageQueue = session.messageQueue;
1696
+ var myAbortController = session.abortController;
1690
1697
  try {
1691
- for await (var msg of session.queryInstance) {
1698
+ for await (var msg of myQueryInstance) {
1692
1699
  processSDKMessage(session, msg);
1693
1700
  }
1694
1701
  // Stream ended normally after a task stop — no "result" message was sent,
@@ -1704,7 +1711,7 @@ function createSDKBridge(opts) {
1704
1711
  if (session.isProcessing) {
1705
1712
  session.isProcessing = false;
1706
1713
  onProcessingChanged();
1707
- if (err.name === "AbortError" || (session.abortController && session.abortController.signal.aborted) || session.taskStopRequested) {
1714
+ if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
1708
1715
  if (!session.destroying) {
1709
1716
  sendAndRecord(session, { type: "info", text: "Interrupted \u00b7 What should Claude do instead?" });
1710
1717
  sendAndRecord(session, { type: "done", code: 0 });
@@ -1777,16 +1784,19 @@ function createSDKBridge(opts) {
1777
1784
  } finally {
1778
1785
  // Close the SDK query to terminate the underlying claude child process.
1779
1786
  // Without this, the process stays alive indefinitely (single-user mode).
1780
- if (session.queryInstance) {
1787
+ // Only clean up if the session still references OUR resources.
1788
+ // A rewind + new startQuery may have already replaced these with
1789
+ // a newer query — clobbering them would kill the new query.
1790
+ if (session.queryInstance === myQueryInstance) {
1781
1791
  try {
1782
1792
  if (typeof session.queryInstance.close === "function") {
1783
1793
  session.queryInstance.close();
1784
1794
  }
1785
1795
  } catch (e) {}
1796
+ session.queryInstance = null;
1786
1797
  }
1787
- session.queryInstance = null;
1788
- session.messageQueue = null;
1789
- session.abortController = null;
1798
+ if (session.messageQueue === myMessageQueue) session.messageQueue = null;
1799
+ if (session.abortController === myAbortController) session.abortController = null;
1790
1800
  session.taskStopRequested = false;
1791
1801
  session.pendingPermissions = {};
1792
1802
  session.pendingAskUser = {};
@@ -1952,6 +1962,8 @@ function createSDKBridge(opts) {
1952
1962
  if (session.lastRewindUuid) {
1953
1963
  queryOptions.resumeSessionAt = session.lastRewindUuid;
1954
1964
  delete session.lastRewindUuid;
1965
+ // Persist the deletion so server restarts don't re-use a stale UUID
1966
+ sm.saveSessionFile(session);
1955
1967
  }
1956
1968
  }
1957
1969
 
package/lib/sessions.js CHANGED
@@ -96,10 +96,14 @@ function createSessionManager(opts) {
96
96
  lines.push(JSON.stringify(session.history[i]));
97
97
  }
98
98
  var sfPath = sessionFilePath(session.cliSessionId);
99
- fs.writeFileSync(sfPath, lines.join("\n") + "\n");
99
+ // Atomic write: write to temp file then rename, so a crash mid-write
100
+ // cannot leave a truncated/corrupted session file.
101
+ var tmpPath = sfPath + ".tmp." + process.pid;
102
+ fs.writeFileSync(tmpPath, lines.join("\n") + "\n");
100
103
  if (process.platform !== "win32") {
101
- try { fs.chmodSync(sfPath, 0o600); } catch (chmodErr) {}
104
+ try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
102
105
  }
106
+ fs.renameSync(tmpPath, sfPath);
103
107
  } catch(e) {
104
108
  console.error("[session] Failed to save session file:", e.message);
105
109
  }
@@ -123,6 +127,13 @@ function createSessionManager(opts) {
123
127
  var files;
124
128
  try { files = fs.readdirSync(sessionsDir); } catch { return; }
125
129
 
130
+ // Clean up stale temp files from interrupted atomic writes
131
+ for (var ti = 0; ti < files.length; ti++) {
132
+ if (files[ti].indexOf(".tmp.") !== -1) {
133
+ try { fs.unlinkSync(path.join(sessionsDir, files[ti])); } catch (e) {}
134
+ }
135
+ }
136
+
126
137
  var loaded = [];
127
138
  for (var i = 0; i < files.length; i++) {
128
139
  if (!files[i].endsWith(".jsonl")) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.26.0-beta.2",
3
+ "version": "2.26.0-beta.3",
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",