clay-server 4.7.1-beta.3 → 4.7.1-beta.5
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-connection.js +1 -1
- package/lib/project-sessions.js +1 -1
- package/lib/public/app.js +1 -0
- package/lib/public/modules/app-connection.js +2 -0
- package/lib/public/modules/app-message-router.js +5 -0
- package/lib/public/modules/app-messages.js +14 -10
- package/lib/public/modules/chat-render-runtime.js +2 -2
- package/lib/public/modules/history-replay-batch.js +113 -0
- package/lib/yoke/adapters/codex.js +55 -8
- package/lib/yoke/codex-idle-recovery.js +52 -0
- package/package.json +1 -1
|
@@ -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);
|
package/lib/project-sessions.js
CHANGED
|
@@ -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
|
-
//
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
}
|
|
@@ -14,6 +14,7 @@ var { resolveOsUserInfo } = require("../../os-users");
|
|
|
14
14
|
var backgroundTasks = require("../codex-background-tasks");
|
|
15
15
|
var codexUserInput = require("../codex-user-input");
|
|
16
16
|
var createTurnStop = require("../codex-turn-stop").createTurnStop;
|
|
17
|
+
var idleRecovery = require("../codex-idle-recovery");
|
|
17
18
|
|
|
18
19
|
// --- Reasoning defaults ---
|
|
19
20
|
// Reasoning summaries are the only readable thinking channel Codex offers:
|
|
@@ -895,6 +896,9 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
895
896
|
var messageWaiting = null;
|
|
896
897
|
var messageQueueEnded = false;
|
|
897
898
|
var handlerEntry = null;
|
|
899
|
+
var inputRevision = 0;
|
|
900
|
+
var resolveLoopFinished;
|
|
901
|
+
var loopFinished = new Promise(function(resolve) { resolveLoopFinished = resolve; });
|
|
898
902
|
|
|
899
903
|
function pushMessageToQueue(msg) {
|
|
900
904
|
if (messageQueueEnded) return false;
|
|
@@ -1314,6 +1318,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1314
1318
|
|
|
1315
1319
|
state.done = true;
|
|
1316
1320
|
endIterator();
|
|
1321
|
+
resolveLoopFinished();
|
|
1317
1322
|
}
|
|
1318
1323
|
|
|
1319
1324
|
var handle = {
|
|
@@ -1333,6 +1338,7 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1333
1338
|
|
|
1334
1339
|
pushMessage: function(text, images) {
|
|
1335
1340
|
if (iteratorDone || state.done || messageQueueEnded) return false;
|
|
1341
|
+
inputRevision++;
|
|
1336
1342
|
var input;
|
|
1337
1343
|
if (images && images.length > 0) {
|
|
1338
1344
|
input = [];
|
|
@@ -1354,6 +1360,16 @@ function createCodexQueryHandle(appServer, queryOpts) {
|
|
|
1354
1360
|
}
|
|
1355
1361
|
},
|
|
1356
1362
|
|
|
1363
|
+
getIdleRecoverySnapshot: function() {
|
|
1364
|
+
if (!state.threadId || !messageWaiting || messageQueue.length || messageQueueEnded || iteratorDone || isCancelled()) return null;
|
|
1365
|
+
return { threadId: state.threadId, revision: inputRevision };
|
|
1366
|
+
},
|
|
1367
|
+
|
|
1368
|
+
retireIdleForRecovery: function() {
|
|
1369
|
+
handle.endInput();
|
|
1370
|
+
return loopFinished;
|
|
1371
|
+
},
|
|
1372
|
+
|
|
1357
1373
|
setModel: function(model) {
|
|
1358
1374
|
state.model = model || DEFAULT_CODEX_MODEL;
|
|
1359
1375
|
return Promise.resolve();
|
|
@@ -1477,6 +1493,7 @@ function createCodexAdapter(opts) {
|
|
|
1477
1493
|
var _initPromise = null;
|
|
1478
1494
|
var _initializing = false;
|
|
1479
1495
|
var _shutdownPromise = null;
|
|
1496
|
+
var _shutdownEpoch = 0;
|
|
1480
1497
|
var _refCount = 0;
|
|
1481
1498
|
var _lastActiveAt = Date.now();
|
|
1482
1499
|
var _shuttingDown = false;
|
|
@@ -1560,10 +1577,6 @@ function createCodexAdapter(opts) {
|
|
|
1560
1577
|
updateLastActiveAt();
|
|
1561
1578
|
}
|
|
1562
1579
|
|
|
1563
|
-
function waitForRecoveryHandoff() {
|
|
1564
|
-
return _recoveryHandoff || Promise.resolve();
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
1580
|
function retainRecoveryRuntime(runtime) {
|
|
1568
1581
|
_recoveryRuntimes.push(runtime);
|
|
1569
1582
|
}
|
|
@@ -1612,6 +1625,11 @@ function createCodexAdapter(opts) {
|
|
|
1612
1625
|
}
|
|
1613
1626
|
|
|
1614
1627
|
async function createIsolatedRecoveryQuery(queryOpts) {
|
|
1628
|
+
var shutdownEpoch = _shutdownEpoch;
|
|
1629
|
+
function checkCurrent() {
|
|
1630
|
+
idleRecovery.checkAbort(queryOpts);
|
|
1631
|
+
if (_shuttingDown || shutdownEpoch !== _shutdownEpoch) throw createShutdownError();
|
|
1632
|
+
}
|
|
1615
1633
|
var releaseHandoff;
|
|
1616
1634
|
var priorHandoff = _recoveryHandoff;
|
|
1617
1635
|
var myHandoff = new Promise(function(resolve) { releaseHandoff = resolve; });
|
|
@@ -1619,6 +1637,11 @@ function createCodexAdapter(opts) {
|
|
|
1619
1637
|
await (priorHandoff || Promise.resolve());
|
|
1620
1638
|
try {
|
|
1621
1639
|
await _recoveryCleanup;
|
|
1640
|
+
checkCurrent();
|
|
1641
|
+
if (!_initializing && _refCount > 0) {
|
|
1642
|
+
await idleRecovery.retireIdleQueries(_activeQueries.slice(), _appServer, checkCurrent);
|
|
1643
|
+
}
|
|
1644
|
+
checkCurrent();
|
|
1622
1645
|
if (_refCount > 0 || _initializing) {
|
|
1623
1646
|
throw new Error("Codex resumed-tool recovery is waiting for an active shared runtime; retry after its current queries finish.");
|
|
1624
1647
|
}
|
|
@@ -1626,6 +1649,7 @@ function createCodexAdapter(opts) {
|
|
|
1626
1649
|
var stopped = await adapter.shutdownIfIdle(0);
|
|
1627
1650
|
if (!stopped) throw new Error("Codex resumed-tool recovery could not safely release the shared runtime; retry after its current queries finish.");
|
|
1628
1651
|
}
|
|
1652
|
+
checkCurrent();
|
|
1629
1653
|
var recoveryRuntime = createCodexAdapter(Object.assign({}, _defaultInitOpts, {
|
|
1630
1654
|
recoveryRuntime: true,
|
|
1631
1655
|
recoverySessionMcpServer: queryOpts.sessionMcpServer,
|
|
@@ -1792,6 +1816,13 @@ function createCodexAdapter(opts) {
|
|
|
1792
1816
|
if (_shuttingDown) {
|
|
1793
1817
|
return Promise.reject(createShutdownError());
|
|
1794
1818
|
}
|
|
1819
|
+
if (_recoveryHandoff) {
|
|
1820
|
+
var initEpoch = _shutdownEpoch;
|
|
1821
|
+
return _recoveryHandoff.then(function() {
|
|
1822
|
+
if (initEpoch !== _shutdownEpoch) throw createShutdownError();
|
|
1823
|
+
return adapter.init(initOpts);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1795
1826
|
|
|
1796
1827
|
var effectiveInitOpts = Object.assign({}, _defaultInitOpts, initOpts || {});
|
|
1797
1828
|
|
|
@@ -2004,10 +2035,25 @@ function createCodexAdapter(opts) {
|
|
|
2004
2035
|
if (queryOpts && queryOpts.sessionMcpServer && queryOpts.resumeSessionId && !queryOpts.isolatedRecovery) {
|
|
2005
2036
|
return createIsolatedRecoveryQuery(queryOpts);
|
|
2006
2037
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2038
|
+
var queryEpoch = _shutdownEpoch;
|
|
2039
|
+
// Recheck after every await: recovery may replace the runtime while an
|
|
2040
|
+
// ordinary start is waiting for initialization or an earlier handoff.
|
|
2041
|
+
while (true) {
|
|
2042
|
+
idleRecovery.checkAbort(queryOpts || {});
|
|
2043
|
+
if (_shuttingDown || queryEpoch !== _shutdownEpoch) throw createShutdownError();
|
|
2044
|
+
if (_recoveryHandoff) {
|
|
2045
|
+
await _recoveryHandoff;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (_initializing && _initPromise) {
|
|
2049
|
+
await _initPromise;
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
if (!_appServer || !_appServer.started) {
|
|
2053
|
+
await adapter.init(queryOpts || {});
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
break;
|
|
2011
2057
|
}
|
|
2012
2058
|
|
|
2013
2059
|
if (_shuttingDown) {
|
|
@@ -2186,6 +2232,7 @@ function createCodexAdapter(opts) {
|
|
|
2186
2232
|
|
|
2187
2233
|
// Shutdown the app-server process
|
|
2188
2234
|
shutdown: function() {
|
|
2235
|
+
_shutdownEpoch++;
|
|
2189
2236
|
return Promise.all([
|
|
2190
2237
|
beginShutdown(true),
|
|
2191
2238
|
shutdownUserRuntimes("shutdown"),
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Retire retained inputs only after every query is verified idle. The adapter
|
|
2
|
+
// holds admission closed while this runs; existing handles may still receive
|
|
3
|
+
// messages, so their input revisions must be checked again after each probe.
|
|
4
|
+
function checkAbort(queryOpts) {
|
|
5
|
+
var signal = queryOpts.abortController && queryOpts.abortController.signal;
|
|
6
|
+
if (signal && signal.aborted) {
|
|
7
|
+
var error = new Error("Query cancelled");
|
|
8
|
+
error.name = "AbortError";
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function terminalsAreIdle(result) {
|
|
14
|
+
if (!result || result.nextCursor) return false;
|
|
15
|
+
var terminals = Array.isArray(result) ? result
|
|
16
|
+
: result.data || result.terminals || result.backgroundTerminals || result.items;
|
|
17
|
+
if (!Array.isArray(terminals)) return false;
|
|
18
|
+
return terminals.every(function(terminal) {
|
|
19
|
+
var status = String(terminal && (terminal.status || terminal.state) || "").toLowerCase();
|
|
20
|
+
return /^(exited|terminated|completed|failed)$/.test(status);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function retireIdleQueries(entries, server, checkCurrent) {
|
|
25
|
+
checkCurrent();
|
|
26
|
+
var candidates = [];
|
|
27
|
+
for (var i = 0; i < entries.length; i++) {
|
|
28
|
+
var handle = entries[i].handle;
|
|
29
|
+
var snapshot = handle && handle.getIdleRecoverySnapshot && handle.getIdleRecoverySnapshot();
|
|
30
|
+
if (!snapshot) return false;
|
|
31
|
+
candidates.push({ handle: handle, snapshot: snapshot });
|
|
32
|
+
}
|
|
33
|
+
var safe = await Promise.all(candidates.map(function(candidate) {
|
|
34
|
+
return server.send("thread/backgroundTerminals/list", {
|
|
35
|
+
threadId: candidate.snapshot.threadId,
|
|
36
|
+
}, 5000).then(terminalsAreIdle, function() { return false; });
|
|
37
|
+
}));
|
|
38
|
+
checkCurrent();
|
|
39
|
+
for (var ci = 0; ci < candidates.length; ci++) {
|
|
40
|
+
var current = candidates[ci].handle.getIdleRecoverySnapshot();
|
|
41
|
+
if (!safe[ci] || !current || current.revision !== candidates[ci].snapshot.revision) return false;
|
|
42
|
+
}
|
|
43
|
+
// No awaits between validation and closing every input. A new send either
|
|
44
|
+
// already invalidated its snapshot or sees a closed input and resumes anew.
|
|
45
|
+
var finished = candidates.map(function(candidate) {
|
|
46
|
+
return candidate.handle.retireIdleForRecovery();
|
|
47
|
+
});
|
|
48
|
+
await Promise.all(finished);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { retireIdleQueries: retireIdleQueries, checkAbort: checkAbort };
|
package/package.json
CHANGED