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 +9 -0
- package/lib/public/app.js +26 -4
- package/lib/public/modules/rewind.js +36 -0
- package/lib/public/modules/tools.js +22 -1
- package/lib/sdk-bridge.js +18 -6
- package/lib/sessions.js +13 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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" || (
|
|
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
|
|
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.
|
|
1788
|
-
session.
|
|
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
|
-
|
|
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(
|
|
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;
|