clay-server 4.7.1-beta.3 → 4.7.1-beta.4

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.
@@ -260,7 +260,7 @@ function attachConnection(ctx) {
260
260
  }
261
261
  var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
262
262
  var activePermissionState = permissionModes.clientState(active, sm.currentPermissionMode, sm.defaultVendor);
263
- 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 });
263
+ 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, isProcessing: !!active.isProcessing, 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 });
264
264
  if (ctx.autonomousRun) sendTo(ws, { type: "autonomous_run_state", sessionId: active.localId, run: ctx.autonomousRun.project(active.autonomousRun) });
265
265
  // Send per-session context sources
266
266
  var sessionSources = loadContextSources(slug, active.localId);
@@ -1760,7 +1760,7 @@ function attachSessions(ctx) {
1760
1760
  // Send feedback as next user message if there's text
1761
1761
  if (feedback) {
1762
1762
  setTimeout(function () {
1763
- var userMsg = { type: "user_message", text: feedback };
1763
+ var userMsg = { type: "user_message", text: feedback, planFeedback: true };
1764
1764
  session.history.push(userMsg);
1765
1765
  sm.appendToSessionFile(session, userMsg);
1766
1766
  sendToSession(session.localId, userMsg);
package/lib/public/app.js CHANGED
@@ -284,6 +284,7 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
284
284
  loadingMore: false,
285
285
  historyTotal: 0,
286
286
  replayingHistory: false,
287
+ historyReplayBatch: null,
287
288
  projectName: projectName,
288
289
  cwd: "",
289
290
  currentSlug: currentSlug,
@@ -24,6 +24,7 @@ import { clearPermissionModePending } from './permission-control.js';
24
24
  import { clearMcpPermissionModePending } from './mcp-ui.js';
25
25
  import { createWebSocketLifecycle } from './websocket-lifecycle.js';
26
26
  import { createWebSocketWatchdog } from './websocket-watchdog.js';
27
+ import { resetHistoryReplayBatch } from './history-replay-batch.js';
27
28
 
28
29
  var connectOverlay = null;
29
30
  var hasConnectedOnce = false;
@@ -324,6 +325,7 @@ function onConnected() {
324
325
 
325
326
  export function connect() {
326
327
  if (lifecycle.isOffline()) return;
328
+ resetHistoryReplayBatch();
327
329
  var attemptEpoch = lifecycle.beginAttempt();
328
330
  attemptStartedAt = Date.now();
329
331
  var ws = getWs();
@@ -14,6 +14,7 @@ import { handleClaySessionTarget } from './command-palette.js';
14
14
  import { handleDefaultAiMessage } from './default-ai.js';
15
15
  import { handleCursorSharingMessage } from './cursor-sharing-preference.js';
16
16
  import { handlePendingMessageQueueMessage } from './pending-message-queue.js';
17
+ import { receiveHistoryReplay } from './history-replay-batch.js';
17
18
 
18
19
  export function handleHomeProtocolMessage(msg) {
19
20
  if (handleIssueReference(msg)) return true;
@@ -82,6 +83,10 @@ export function handleHomeProtocolMessage(msg) {
82
83
  }
83
84
 
84
85
  export function processMessage(msg) {
86
+ receiveHistoryReplay(msg, dispatchMessage);
87
+ }
88
+
89
+ function dispatchMessage(msg) {
85
90
  if (handlePendingMessageQueueMessage(msg)) return;
86
91
  if (handleDefaultAiMessage(msg)) return;
87
92
  if (handleCursorSharingMessage(msg)) return;
@@ -67,7 +67,7 @@ import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerati
67
67
  import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
68
68
 
69
69
  // --- App module imports ---
70
- import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
70
+ import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, resetStreamBuffer, appendDelta, flushStreamBuffer, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
71
71
  import { setActivity, startUrgentBlink, stopUrgentBlink, blinkSessionDot, updateCrossProjectBlink } from './app-favicon.js';
72
72
  import { setStatus } from './app-connection.js';
73
73
  import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
@@ -251,16 +251,14 @@ export function processMessage(msg) {
251
251
  }
252
252
  updateContextPanel();
253
253
  updateUsagePanel();
254
- // Render + finalize any incomplete turn from the replayed history
255
- var _hs = store.snap();
256
- if (_hs.currentMsgEl && _hs.currentFullText) {
257
- var replayContentEl = _hs.currentMsgEl.querySelector(".md-content");
258
- if (replayContentEl) {
259
- replayContentEl.innerHTML = renderMarkdown(_hs.currentFullText);
260
- }
254
+ // Keep the active answer and tools attached so live deltas continue
255
+ // the restored turn instead of starting a second answer bubble.
256
+ if (store.get('sessionIsProcessing')) {
257
+ flushStreamBuffer();
258
+ } else {
259
+ markAllToolsDone();
260
+ finalizeAssistantBlock();
261
261
  }
262
- markAllToolsDone();
263
- finalizeAssistantBlock();
264
262
  stopUrgentBlink();
265
263
  // Clean up debate UI if debate is not active after replay
266
264
  if (!isDebateActive()) {
@@ -975,6 +973,12 @@ export function processMessage(msg) {
975
973
  if (msg.sessionId != null && String(msg.sessionId) !== String(store.get('activeSessionId'))) break;
976
974
  if (msg.clientMessageId && messagesEl.querySelector('[data-client-message-id="' + msg.clientMessageId + '"]')) break;
977
975
  if (!store.get('replayingHistory')) beginMarkdownTurn();
976
+ // A recorded user turn closes the previous one. Tools left open by an
977
+ // interrupted earlier turn must not keep spinning in a restored
978
+ // running session, where history_done keeps current tools active.
979
+ // Question answers, plan feedback and Driver delegations are injected
980
+ // mid-turn, so tools before them may still be running.
981
+ else if (!msg.askUserAnswer && !msg.planFeedback && !msg.delegated) markAllToolsDone();
978
982
  resetThinkingGroup();
979
983
  if (msg.planContent) {
980
984
  setPlanContent(msg.planContent);
@@ -168,7 +168,7 @@ export function keepActiveThinkingAtTail() {
168
168
  }
169
169
 
170
170
  export function scrollToBottom() {
171
- if (prependAnchor) return;
171
+ if (prependAnchor || store.get('replayingHistory')) return;
172
172
  var messagesEl = getMessagesEl();
173
173
  if (!messagesEl) return;
174
174
  var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
@@ -192,7 +192,7 @@ export function scrollToBottom() {
192
192
  }
193
193
 
194
194
  export function forceScrollToBottom() {
195
- if (prependAnchor) return;
195
+ if (prependAnchor || store.get('replayingHistory')) return;
196
196
  armStickyBottom(750);
197
197
  }
198
198
 
@@ -0,0 +1,113 @@
1
+ import { store } from './store.js';
2
+
3
+ // Recovery bounds for a replay whose history_done never arrives. Either limit
4
+ // restores everything received so far and lets later messages through, so a
5
+ // lost terminator cannot capture live output indefinitely.
6
+ export var HISTORY_REPLAY_TIMEOUT_MS = 20000;
7
+ export var HISTORY_REPLAY_MAX_EVENTS = 200000;
8
+ export var HISTORY_REPLAY_MAX_TEXT = 16 * 1024 * 1024;
9
+ var MAX_LOGGED_FAILURES = 5;
10
+
11
+ function cancelTimer(batch) {
12
+ if (batch && batch.timer) {
13
+ clearTimeout(batch.timer);
14
+ batch.timer = null;
15
+ }
16
+ }
17
+
18
+ export function resetHistoryReplayBatch() {
19
+ cancelTimer(store.get('historyReplayBatch'));
20
+ store.set({ historyReplayBatch: null, replayingHistory: false });
21
+ }
22
+
23
+ function canCombine(previous, message) {
24
+ if (!previous || previous.type !== message.type || typeof previous.text !== 'string' || typeof message.text !== 'string') return false;
25
+ if (message.type !== 'delta' && message.type !== 'thinking_delta') return false;
26
+ // Keep identity/metadata boundaries intact. Timestamps on individual tokens
27
+ // can differ; the first token still supplies the message's display time.
28
+ var keys = Object.keys(previous).concat(Object.keys(message));
29
+ return keys.every(function (key) {
30
+ return key === 'text' || key === '_ts' || previous[key] === message[key];
31
+ });
32
+ }
33
+
34
+ function finishText(batch) {
35
+ if (!batch.textParts) return;
36
+ batch.items[batch.items.length - 1].text = batch.textParts.join('');
37
+ batch.textParts = null;
38
+ }
39
+
40
+ // Diagnostics name the entry type and error class only. Error messages and
41
+ // stacks can quote transcript content, so they are never logged.
42
+ function describeFailure(msg, err) {
43
+ var type = msg && typeof msg.type === 'string' && /^[A-Za-z0-9_]{1,64}$/.test(msg.type) ? msg.type : 'unknown';
44
+ var name = err && typeof err.name === 'string' && /^[A-Za-z]{1,64}$/.test(err.name) ? err.name : 'Error';
45
+ return type + ' entry failed with ' + name;
46
+ }
47
+
48
+ function deliver(batch, done, dispatch) {
49
+ cancelTimer(batch);
50
+ finishText(batch);
51
+ if (store.get('historyReplayBatch') === batch) store.set({ historyReplayBatch: null });
52
+ var entries = batch.items.concat([done]);
53
+ var failures = 0;
54
+ try {
55
+ // Each entry is isolated like a separate socket message: one bad record
56
+ // must not discard the rest of the transcript or history_done cleanup.
57
+ for (var i = 0; i < entries.length; i++) {
58
+ try {
59
+ dispatch(entries[i]);
60
+ } catch (err) {
61
+ failures++;
62
+ if (failures <= MAX_LOGGED_FAILURES) {
63
+ console.error('[history-replay] Restored history ' + (i + 1) + '/' + entries.length + ': ' + describeFailure(entries[i], err));
64
+ }
65
+ }
66
+ }
67
+ if (failures > MAX_LOGGED_FAILURES) {
68
+ console.error('[history-replay] ' + (failures - MAX_LOGGED_FAILURES) + ' more restored history failures were not logged.');
69
+ }
70
+ } finally {
71
+ store.set({ replayingHistory: false });
72
+ }
73
+ }
74
+
75
+ function recover(batch, reason, dispatch) {
76
+ if (store.get('historyReplayBatch') !== batch) return;
77
+ console.warn('[history-replay] history_done missing after ' + batch.received + ' events (' + reason + '); restoring received history.');
78
+ deliver(batch, { type: 'history_done', replayRecovered: true }, dispatch);
79
+ }
80
+
81
+ // WebSocket history arrives across many browser tasks. Collect it before
82
+ // calling the normal handlers so the browser cannot paint intermediate tools,
83
+ // Thinking segments or old answers between those tasks.
84
+ export function receiveHistoryReplay(msg, dispatch) {
85
+ if (msg.type === 'session_switched' || msg.type === 'project_info') resetHistoryReplayBatch();
86
+ if (msg.type === 'history_meta') {
87
+ cancelTimer(store.get('historyReplayBatch'));
88
+ var started = { items: [msg], textParts: null, received: 1, text: 0, timer: null };
89
+ started.timer = setTimeout(function () { recover(started, 'timeout', dispatch); }, HISTORY_REPLAY_TIMEOUT_MS);
90
+ store.set({ historyReplayBatch: started });
91
+ return;
92
+ }
93
+ var batch = store.get('historyReplayBatch');
94
+ if (!batch) {
95
+ dispatch(msg);
96
+ return;
97
+ }
98
+ if (msg.type === 'history_done') {
99
+ deliver(batch, msg, dispatch);
100
+ return;
101
+ }
102
+ batch.received++;
103
+ if (typeof msg.text === 'string') batch.text += msg.text.length;
104
+ var previous = batch.items[batch.items.length - 1];
105
+ if (canCombine(previous, msg)) {
106
+ if (!batch.textParts) batch.textParts = [previous.text];
107
+ batch.textParts.push(msg.text);
108
+ } else {
109
+ finishText(batch);
110
+ batch.items.push(Object.assign({}, msg));
111
+ }
112
+ if (batch.received >= HISTORY_REPLAY_MAX_EVENTS || batch.text >= HISTORY_REPLAY_MAX_TEXT) recover(batch, 'size', dispatch);
113
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.7.1-beta.3",
3
+ "version": "4.7.1-beta.4",
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"