clay-server 4.7.0-beta.5 → 4.7.0
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-session-spawn.js +1 -0
- package/lib/project-sessions.js +16 -13
- package/lib/public/css/input.css +19 -11
- package/lib/public/index.html +1 -3
- package/lib/public/modules/app-messages.js +7 -0
- package/lib/public/modules/generation-stop.js +27 -6
- package/lib/public/modules/tool-result-lifecycle.js +3 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +46 -6
- package/lib/sdk-message-processor.js +83 -18
- package/lib/sdk-task-lifecycle.js +52 -0
- package/lib/sessions.js +1 -1
- package/lib/yoke/adapters/claude-worker.js +3 -0
- package/lib/yoke/adapters/claude.js +25 -2
- package/lib/yoke/adapters/codex.js +31 -5
- package/package.json +3 -3
package/lib/project-sessions.js
CHANGED
|
@@ -1489,14 +1489,26 @@ function attachSessions(ctx) {
|
|
|
1489
1489
|
}
|
|
1490
1490
|
}
|
|
1491
1491
|
|
|
1492
|
-
//
|
|
1493
|
-
|
|
1492
|
+
// Codex's thread/revert cuts at a turn id, not a count. Read the
|
|
1493
|
+
// target entry's turnId BEFORE trimming local history below.
|
|
1494
|
+
var hasTurnsToRollBack = false;
|
|
1495
|
+
var targetTurnId = null;
|
|
1494
1496
|
if (targetIdx >= 0) {
|
|
1497
|
+
targetTurnId = session.messageUUIDs[targetIdx].turnId || null;
|
|
1495
1498
|
for (var ri = targetIdx; ri < session.messageUUIDs.length; ri++) {
|
|
1496
|
-
if (session.messageUUIDs[ri].type === "user")
|
|
1499
|
+
if (session.messageUUIDs[ri].type === "user") { hasTurnsToRollBack = true; break; }
|
|
1497
1500
|
}
|
|
1498
1501
|
}
|
|
1499
1502
|
|
|
1503
|
+
// Notify adapter of conversation rollback (e.g. Codex thread/revert)
|
|
1504
|
+
// BEFORE trimming local history below. Deliberately not caught
|
|
1505
|
+
// here: a failed or impossible (missing turnId) provider-side
|
|
1506
|
+
// rollback must abort the whole rewind via the outer catch, never
|
|
1507
|
+
// leave local history trimmed ahead of the real provider thread.
|
|
1508
|
+
if (hasTurnsToRollBack) {
|
|
1509
|
+
await sdk.rollbackConversation(session, targetTurnId);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1500
1512
|
if (targetIdx >= 0) {
|
|
1501
1513
|
var trimTo = session.messageUUIDs[targetIdx].historyIndex;
|
|
1502
1514
|
for (var k = trimTo - 1; k >= 0; k--) {
|
|
@@ -1513,15 +1525,6 @@ function attachSessions(ctx) {
|
|
|
1513
1525
|
}
|
|
1514
1526
|
}
|
|
1515
1527
|
|
|
1516
|
-
// Notify adapter of conversation rollback (e.g. Codex thread/rollback)
|
|
1517
|
-
if (turnsToRollBack > 0) {
|
|
1518
|
-
try {
|
|
1519
|
-
await sdk.rollbackConversation(session, turnsToRollBack);
|
|
1520
|
-
} catch (rbErr) {
|
|
1521
|
-
console.error("[project-sessions] conversation rollback failed:", rbErr.message || rbErr);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
1528
|
var kept = session.messageUUIDs;
|
|
1526
1529
|
session.lastRewindUuid = kept.length > 0 ? kept[kept.length - 1].uuid : null;
|
|
1527
1530
|
}
|
|
@@ -1588,7 +1591,7 @@ function attachSessions(ctx) {
|
|
|
1588
1591
|
forked.messageUUIDs = [];
|
|
1589
1592
|
for (var hi = 0; hi < forkHistory.length; hi++) {
|
|
1590
1593
|
if (forkHistory[hi].type === "message_uuid") {
|
|
1591
|
-
forked.messageUUIDs.push({ uuid: forkHistory[hi].uuid, type: forkHistory[hi].messageType, historyIndex: hi });
|
|
1594
|
+
forked.messageUUIDs.push({ uuid: forkHistory[hi].uuid, type: forkHistory[hi].messageType, historyIndex: hi, turnId: forkHistory[hi].turnId || null });
|
|
1592
1595
|
}
|
|
1593
1596
|
}
|
|
1594
1597
|
sm.saveSessionFile(forked);
|
package/lib/public/css/input.css
CHANGED
|
@@ -1718,21 +1718,17 @@ body.tui-composer-active #config-chip-wrap {
|
|
|
1718
1718
|
color: inherit;
|
|
1719
1719
|
}
|
|
1720
1720
|
|
|
1721
|
-
/* Stop remains available when Send is occupied by a draft or
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
justify-content: flex-end;
|
|
1727
|
-
}
|
|
1728
|
-
#generation-stop-row.hidden { display: none; }
|
|
1729
|
-
body:not(.worker-pane-locked) #input-area:has(#send-btn.stop:not(:disabled)) #generation-stop-row { display: none; }
|
|
1721
|
+
/* Stop remains available when Send is occupied by a draft or a locked
|
|
1722
|
+
configured Worker pane. One button, two homes, moved (never duplicated) by
|
|
1723
|
+
generation-stop.js: beside Send in the ordinary composer action row, or a
|
|
1724
|
+
direct child of #input-area -- outside the composer that worker-pane-lock.js
|
|
1725
|
+
hides with #input-wrapper { display: none } -- when the pane is locked. */
|
|
1730
1726
|
#generation-stop {
|
|
1731
1727
|
display: inline-flex;
|
|
1732
1728
|
align-items: center;
|
|
1733
1729
|
gap: 6px;
|
|
1734
|
-
|
|
1735
|
-
padding:
|
|
1730
|
+
height: 36px;
|
|
1731
|
+
padding: 0 12px;
|
|
1736
1732
|
border: 1px solid var(--border);
|
|
1737
1733
|
border-radius: 18px;
|
|
1738
1734
|
background: var(--sidebar-bg);
|
|
@@ -1740,7 +1736,19 @@ body:not(.worker-pane-locked) #input-area:has(#send-btn.stop:not(:disabled)) #ge
|
|
|
1740
1736
|
font: inherit;
|
|
1741
1737
|
font-size: 12px;
|
|
1742
1738
|
cursor: pointer;
|
|
1739
|
+
flex-shrink: 0;
|
|
1743
1740
|
}
|
|
1741
|
+
#generation-stop.hidden { display: none; }
|
|
1742
|
+
body:not(.worker-pane-locked) #input-bottom-right:has(#send-btn.stop:not(:disabled)) #generation-stop { display: none; }
|
|
1744
1743
|
#generation-stop:hover:not(:disabled) { color: var(--text); background: var(--code-bg); }
|
|
1745
1744
|
#generation-stop:disabled { opacity: 0.5; cursor: default; }
|
|
1746
1745
|
#generation-stop .lucide { width: 12px; height: 12px; }
|
|
1746
|
+
|
|
1747
|
+
/* Locked placement: a direct child of #input-area, centered under the
|
|
1748
|
+
"Controlled by Driver" status line rather than inline in a hidden row. */
|
|
1749
|
+
#input-area > #generation-stop {
|
|
1750
|
+
display: flex;
|
|
1751
|
+
width: fit-content;
|
|
1752
|
+
margin: 8px auto 0;
|
|
1753
|
+
}
|
|
1754
|
+
#input-area > #generation-stop.hidden { display: none; }
|
package/lib/public/index.html
CHANGED
|
@@ -630,14 +630,12 @@
|
|
|
630
630
|
</div>
|
|
631
631
|
</div>
|
|
632
632
|
<button id="stt-btn" type="button" aria-label="Voice input" title="Voice input"><i data-lucide="mic"></i></button>
|
|
633
|
+
<button id="generation-stop" type="button" class="hidden" aria-label="Stop generating"><i data-lucide="square"></i><span>Stop</span></button>
|
|
633
634
|
<button id="send-btn" disabled aria-label="Send"><i data-lucide="arrow-up"></i></button>
|
|
634
635
|
</div>
|
|
635
636
|
</div>
|
|
636
637
|
</div>
|
|
637
638
|
</div>
|
|
638
|
-
<div id="generation-stop-row" class="hidden">
|
|
639
|
-
<button id="generation-stop" type="button" aria-label="Stop generating"><i data-lucide="square"></i><span>Stop</span></button>
|
|
640
|
-
</div>
|
|
641
639
|
</div>
|
|
642
640
|
</div>
|
|
643
641
|
<!-- Mobile input-more bottom sheet: attach / image + context sources -->
|
|
@@ -35,6 +35,7 @@ import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js
|
|
|
35
35
|
import { renderDriverContinuation, updateDriverContinuation, renderDriverContinuationContext } from './driver-continuation.js';
|
|
36
36
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent, handleAcceptedOrdinaryMessage, handleScheduleMessageResult } from './input.js';
|
|
37
37
|
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
38
|
+
import { isInterimToolResult } from './tool-result-lifecycle.js';
|
|
38
39
|
import { finishThinkingTurn, resumeThinkingAfterReplay } from './thinking-lifecycle.js';
|
|
39
40
|
import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './notifications.js';
|
|
40
41
|
import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
|
|
@@ -1194,6 +1195,12 @@ export function processMessage(msg) {
|
|
|
1194
1195
|
case "tool_result": {
|
|
1195
1196
|
var tr = getTools()[msg.id];
|
|
1196
1197
|
if (tr && tr.hidden) break; // skip hidden plan tools
|
|
1198
|
+
// A detached SDK placeholder means this exact tool call is still
|
|
1199
|
+
// running. Leave its card and label untouched; history replay and
|
|
1200
|
+
// split panes use this same router, so a later real result can
|
|
1201
|
+
// complete the original card everywhere without a false
|
|
1202
|
+
// "(no output)" terminal state.
|
|
1203
|
+
if (isInterimToolResult(msg)) break;
|
|
1197
1204
|
if (msg.is_error && tr && tr.name === "ImageGen") clearImageGenerationProgress(msg.id);
|
|
1198
1205
|
// Always call updateToolResult for Edit (to show diff from input), or when content exists
|
|
1199
1206
|
if (msg.content != null || msg.images || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
|
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { isDriverOperatedView } from './worker-pane-lock.js';
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
5
|
+
// Fallback Stop control for when Send cannot double as Stop: a draft sitting
|
|
6
|
+
// in the composer (Send stays Send so the draft can still go out), or a
|
|
7
|
+
// locked configured Worker pane (Send and the rest of the composer are
|
|
8
|
+
// hidden entirely). One button, two homes, moved rather than duplicated:
|
|
9
|
+
// beside Send in the ordinary composer action row, or a direct child of
|
|
10
|
+
// #input-area -- outside the composer that worker-pane-lock.js hides with
|
|
11
|
+
// #input-wrapper { display: none } -- when the pane is locked. Either way
|
|
12
|
+
// there is never more than one visible Stop, and a locked pane's hidden
|
|
13
|
+
// composer never takes the human's cancellation away with it.
|
|
6
14
|
export function initGenerationStop() {
|
|
7
|
-
var row = document.getElementById('generation-stop-row');
|
|
8
15
|
var button = document.getElementById('generation-stop');
|
|
9
|
-
if (!
|
|
16
|
+
if (!button) return;
|
|
17
|
+
var inputArea = document.getElementById('input-area');
|
|
18
|
+
var actionRow = document.getElementById('input-bottom-right');
|
|
19
|
+
var sendBtn = document.getElementById('send-btn');
|
|
20
|
+
|
|
21
|
+
function place() {
|
|
22
|
+
if (isDriverOperatedView()) {
|
|
23
|
+
if (inputArea && button.parentNode !== inputArea) inputArea.appendChild(button);
|
|
24
|
+
} else if (actionRow && button.parentNode !== actionRow) {
|
|
25
|
+
actionRow.insertBefore(button, sendBtn || null);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
10
28
|
function sync() {
|
|
11
|
-
|
|
29
|
+
button.classList.toggle('hidden', !store.get('processing'));
|
|
12
30
|
button.disabled = !store.get('connected');
|
|
13
31
|
button.title = store.get('connected') ? 'Stop generating' : 'Reconnect to stop generating';
|
|
32
|
+
place();
|
|
14
33
|
}
|
|
15
34
|
button.addEventListener('click', function () {
|
|
16
35
|
var ws = getWs();
|
|
@@ -18,7 +37,9 @@ export function initGenerationStop() {
|
|
|
18
37
|
ws.send(JSON.stringify({ type: 'stop' }));
|
|
19
38
|
});
|
|
20
39
|
store.subscribe(function (state, previous) {
|
|
21
|
-
if (state.processing !== previous.processing || state.connected !== previous.connected
|
|
40
|
+
if (state.processing !== previous.processing || state.connected !== previous.connected
|
|
41
|
+
|| state.splitGroups !== previous.splitGroups || state.activeSessionId !== previous.activeSessionId
|
|
42
|
+
|| state.paneSessionId !== previous.paneSessionId) sync();
|
|
22
43
|
});
|
|
23
44
|
sync();
|
|
24
45
|
}
|
package/lib/public/style.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
@import url("css/messages.css?v=20260828-delegated-muted1");
|
|
14
14
|
@import url("css/generated-images.css?v=20260829-save-details1");
|
|
15
15
|
@import url("css/rewind.css");
|
|
16
|
-
@import url("css/input.css?v=
|
|
16
|
+
@import url("css/input.css?v=20261002-stop-inline1");
|
|
17
17
|
@import url("css/pending-message-queue.css?v=20260914-queue1");
|
|
18
18
|
@import url("css/filebrowser.css?v=20260829-terminal-canvas1");
|
|
19
19
|
@import url("css/git-panel.css");
|
package/lib/sdk-bridge.js
CHANGED
|
@@ -12,6 +12,7 @@ var skillDiscovery = require("./yoke/skill-discovery");
|
|
|
12
12
|
var { isSafeBashSegment } = require("./safe-bash-commands");
|
|
13
13
|
var { createMessageQueue } = require("./sdk-message-queue");
|
|
14
14
|
var { attachMessageProcessor } = require("./sdk-message-processor");
|
|
15
|
+
var taskLifecycle = require("./sdk-task-lifecycle");
|
|
15
16
|
var homeDebateToolPolicy = require("./home-debate-tool-policy");
|
|
16
17
|
var yoke = require("./yoke");
|
|
17
18
|
var sessionTitlePolicy = require("./session-title-policy");
|
|
@@ -851,6 +852,14 @@ function createSDKBridge(opts) {
|
|
|
851
852
|
}
|
|
852
853
|
|
|
853
854
|
// Regular tool permission request: send to client and wait
|
|
855
|
+
// mcpServer (name/source) is SDK-reported metadata about which MCP
|
|
856
|
+
// server actually served the tool call. It comes from the trusted CLI
|
|
857
|
+
// subprocess boundary in opts, never from the model-controlled `input`,
|
|
858
|
+
// so it is safe to surface informationally; it is not used to grant or
|
|
859
|
+
// widen any permission decision made above.
|
|
860
|
+
var mcpServerMeta = (opts.mcpServer && typeof opts.mcpServer === "object")
|
|
861
|
+
? { name: opts.mcpServer.name || null, source: opts.mcpServer.source || null }
|
|
862
|
+
: null;
|
|
854
863
|
return new Promise(function(resolve) {
|
|
855
864
|
var requestId = crypto.randomUUID();
|
|
856
865
|
sm.permissionRequestIndex[requestId] = session.localId;
|
|
@@ -863,6 +872,7 @@ function createSDKBridge(opts) {
|
|
|
863
872
|
decisionReason: opts.decisionReason || "",
|
|
864
873
|
defaultToNo: opts.defaultToNo === true,
|
|
865
874
|
suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
|
|
875
|
+
mcpServer: mcpServerMeta,
|
|
866
876
|
};
|
|
867
877
|
|
|
868
878
|
var permMsg = {
|
|
@@ -874,6 +884,7 @@ function createSDKBridge(opts) {
|
|
|
874
884
|
decisionReason: opts.decisionReason || "",
|
|
875
885
|
defaultToNo: opts.defaultToNo === true,
|
|
876
886
|
suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
|
|
887
|
+
mcpServer: mcpServerMeta,
|
|
877
888
|
vendor: session.vendor || (adapter && adapter.vendor) || "claude",
|
|
878
889
|
};
|
|
879
890
|
sendAndRecord(session, permMsg);
|
|
@@ -1103,6 +1114,7 @@ function createSDKBridge(opts) {
|
|
|
1103
1114
|
session.isProcessing = false;
|
|
1104
1115
|
session._awaitingTurnResult = false;
|
|
1105
1116
|
session._queuedTurnCount = 0;
|
|
1117
|
+
taskLifecycle.clearForeground(session);
|
|
1106
1118
|
onProcessingChanged();
|
|
1107
1119
|
send({ type: "status", processing: false });
|
|
1108
1120
|
sendAndRecord(session, { type: "thinking_stop" });
|
|
@@ -1119,6 +1131,7 @@ function createSDKBridge(opts) {
|
|
|
1119
1131
|
session.isProcessing = false;
|
|
1120
1132
|
session._awaitingTurnResult = false;
|
|
1121
1133
|
session._queuedTurnCount = 0;
|
|
1134
|
+
taskLifecycle.clearAll(session);
|
|
1122
1135
|
onProcessingChanged();
|
|
1123
1136
|
if (!session._lastAdapterError) {
|
|
1124
1137
|
sendAndRecord(session, {
|
|
@@ -1138,6 +1151,7 @@ function createSDKBridge(opts) {
|
|
|
1138
1151
|
session._queuedTurnCount = 0;
|
|
1139
1152
|
onProcessingChanged();
|
|
1140
1153
|
if (err.name === "AbortError" || (myAbortController && myAbortController.signal.aborted) || session.taskStopRequested) {
|
|
1154
|
+
taskLifecycle.clearForeground(session);
|
|
1141
1155
|
session._lastTurnInterrupted = true;
|
|
1142
1156
|
if (!session.destroying) {
|
|
1143
1157
|
sendAndRecord(session, { type: "thinking_stop" });
|
|
@@ -1148,9 +1162,11 @@ function createSDKBridge(opts) {
|
|
|
1148
1162
|
sendAndRecord(session, { type: "done", code: 0 });
|
|
1149
1163
|
}
|
|
1150
1164
|
} else if (session.destroying) {
|
|
1165
|
+
taskLifecycle.clearAll(session);
|
|
1151
1166
|
// Suppress error messages during shutdown
|
|
1152
1167
|
console.log("[sdk-bridge] Suppressing stream error during shutdown for session " + session.localId);
|
|
1153
1168
|
} else {
|
|
1169
|
+
taskLifecycle.clearAll(session);
|
|
1154
1170
|
var errDetail = err.message || String(err);
|
|
1155
1171
|
if (err.stderr) errDetail += "\nstderr: " + err.stderr;
|
|
1156
1172
|
if (err.exitCode != null) errDetail += " (exitCode: " + err.exitCode + ")";
|
|
@@ -1370,10 +1386,19 @@ function createSDKBridge(opts) {
|
|
|
1370
1386
|
}
|
|
1371
1387
|
}
|
|
1372
1388
|
|
|
1373
|
-
async function rollbackConversation(session,
|
|
1389
|
+
async function rollbackConversation(session, turnId) {
|
|
1374
1390
|
var sessionAdapter = getAdapterForSession(session);
|
|
1375
1391
|
if (sessionAdapter && typeof sessionAdapter.rollbackThread === "function") {
|
|
1376
|
-
|
|
1392
|
+
// Codex's thread/revert requires its own turnId (beforeTurnId), not a
|
|
1393
|
+
// turn count. A session whose target turn predates turnId capture (or
|
|
1394
|
+
// whose persisted record lost it) has nothing valid to send: throwing
|
|
1395
|
+
// here -- rather than guessing or silently skipping -- lets the caller
|
|
1396
|
+
// abort the whole rewind instead of leaving local history trimmed
|
|
1397
|
+
// ahead of the real, un-rolled-back provider thread.
|
|
1398
|
+
if (!turnId) {
|
|
1399
|
+
throw new Error("No turn id recorded for this turn; cannot roll back the Codex thread.");
|
|
1400
|
+
}
|
|
1401
|
+
await sessionAdapter.rollbackThread(session.cliSessionId, turnId, {
|
|
1377
1402
|
linuxUser: getSessionLinuxUser(session) || undefined,
|
|
1378
1403
|
});
|
|
1379
1404
|
}
|
|
@@ -1620,7 +1645,7 @@ function createSDKBridge(opts) {
|
|
|
1620
1645
|
|
|
1621
1646
|
session.blocks = {};
|
|
1622
1647
|
session.sentToolResults = {};
|
|
1623
|
-
session
|
|
1648
|
+
taskLifecycle.clearAll(session);
|
|
1624
1649
|
session.pendingElicitations = {};
|
|
1625
1650
|
session.streamedText = false;
|
|
1626
1651
|
session.responsePreview = "";
|
|
@@ -1685,9 +1710,16 @@ function createSDKBridge(opts) {
|
|
|
1685
1710
|
else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
|
|
1686
1711
|
else effectiveDefault = globalMode;
|
|
1687
1712
|
var modeToApply = session._loopPermissionMode || effectiveDefault;
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1713
|
+
// Always set permissionMode explicitly, including "default". SDK
|
|
1714
|
+
// 0.3.286+ leaves an omitted permissionMode to Claude Code itself,
|
|
1715
|
+
// which can apply a settings defaultMode or start in auto mode on
|
|
1716
|
+
// third-party providers / with telemetry off -- silently widening
|
|
1717
|
+
// past Clay's Ask policy. Sending "default" keeps the SDK's own
|
|
1718
|
+
// decision point at the same "default" behavior Clay has always run
|
|
1719
|
+
// under: a tool the SDK's own allowed/denied rules or allowedTools
|
|
1720
|
+
// list already settles is not re-asked, but anything left undecided
|
|
1721
|
+
// still reaches Clay's canUseTool callback, as before.
|
|
1722
|
+
claudeOpts.permissionMode = modeToApply || "default";
|
|
1691
1723
|
}
|
|
1692
1724
|
// Clear one-shot acceptEditsAfterStart regardless of which branch ran above,
|
|
1693
1725
|
// so the flag does not linger into subsequent turns.
|
|
@@ -2022,6 +2054,7 @@ function createSDKBridge(opts) {
|
|
|
2022
2054
|
session.isProcessing = false;
|
|
2023
2055
|
session._awaitingTurnResult = false;
|
|
2024
2056
|
session._queuedTurnCount = 0;
|
|
2057
|
+
taskLifecycle.clearAll(session);
|
|
2025
2058
|
onProcessingChanged();
|
|
2026
2059
|
session.queryInstance = null;
|
|
2027
2060
|
session.messageQueue = null;
|
|
@@ -2079,6 +2112,7 @@ function createSDKBridge(opts) {
|
|
|
2079
2112
|
session.isProcessing = false;
|
|
2080
2113
|
session._awaitingTurnResult = false;
|
|
2081
2114
|
session._queuedTurnCount = 0;
|
|
2115
|
+
taskLifecycle.clearAll(session);
|
|
2082
2116
|
onProcessingChanged();
|
|
2083
2117
|
sm.broadcastSessionList();
|
|
2084
2118
|
return false;
|
|
@@ -2096,6 +2130,7 @@ function createSDKBridge(opts) {
|
|
|
2096
2130
|
session.isProcessing = false;
|
|
2097
2131
|
session._awaitingTurnResult = false;
|
|
2098
2132
|
session._queuedTurnCount = 0;
|
|
2133
|
+
taskLifecycle.clearAll(session);
|
|
2099
2134
|
onProcessingChanged();
|
|
2100
2135
|
sendAndRecord(session, { type: "error", text: "The agent connection closed before it received your message. Please send it again." });
|
|
2101
2136
|
sendAndRecord(session, { type: "done", code: 1 });
|
|
@@ -2499,6 +2534,7 @@ function createSDKBridge(opts) {
|
|
|
2499
2534
|
var session = sm.getActiveSession();
|
|
2500
2535
|
if (!session) return;
|
|
2501
2536
|
session.taskStopRequested = true;
|
|
2537
|
+
taskLifecycle.finish(session, null, taskId);
|
|
2502
2538
|
if (!session.queryInstance) return;
|
|
2503
2539
|
try {
|
|
2504
2540
|
// Route through QueryHandle (works for both in-process and worker paths)
|
|
@@ -2582,6 +2618,10 @@ function createSDKBridge(opts) {
|
|
|
2582
2618
|
CLAUDE: {
|
|
2583
2619
|
settingSources: ["user"],
|
|
2584
2620
|
includePartialMessages: true,
|
|
2621
|
+
// Explicit, not omitted: SDK 0.3.286+ leaves an omitted
|
|
2622
|
+
// permissionMode to Claude Code's own default/auto behavior,
|
|
2623
|
+
// which could bypass the deny-by-default canUseTool above.
|
|
2624
|
+
permissionMode: "default",
|
|
2585
2625
|
},
|
|
2586
2626
|
},
|
|
2587
2627
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var usersModule = require("./users");
|
|
2
2
|
var turnQueue = require("./sdk-turn-queue");
|
|
3
|
+
var taskLifecycle = require("./sdk-task-lifecycle");
|
|
3
4
|
var contextUsage = require("./context-usage");
|
|
4
5
|
var yoke = require("./yoke");
|
|
5
6
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
@@ -193,8 +194,11 @@ function attachMessageProcessor(ctx) {
|
|
|
193
194
|
// Capture message UUIDs for rewind support
|
|
194
195
|
if (parsed.uuid) {
|
|
195
196
|
if (parsed.messageType === "user" && !parsed.parentToolUseId) {
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
// turnId (Codex's own turn identifier, distinct from this uuid) is
|
|
198
|
+
// stored so a later rewind can send Codex's thread/revert
|
|
199
|
+
// beforeTurnId instead of a turn count. Undefined for Claude.
|
|
200
|
+
session.messageUUIDs.push({ uuid: parsed.uuid, type: "user", historyIndex: session.history.length, turnId: parsed.turnId || null });
|
|
201
|
+
sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "user", turnId: parsed.turnId || null });
|
|
198
202
|
} else if (parsed.messageType === "assistant") {
|
|
199
203
|
session.messageUUIDs.push({ uuid: parsed.uuid, type: "assistant", historyIndex: session.history.length });
|
|
200
204
|
sendAndRecord(session, { type: "message_uuid", uuid: parsed.uuid, messageType: "assistant" });
|
|
@@ -252,6 +256,14 @@ function attachMessageProcessor(ctx) {
|
|
|
252
256
|
if (parsed.fastModeState) {
|
|
253
257
|
sendAndRecord(session, { type: "fast_mode_state", state: parsed.fastModeState });
|
|
254
258
|
}
|
|
259
|
+
if (Array.isArray(parsed.pluginErrors) && parsed.pluginErrors.length > 0) {
|
|
260
|
+
var pluginPaths = parsed.pluginErrors.map(function (e) { return e && e.path; }).filter(Boolean);
|
|
261
|
+
sendAndRecord(session, {
|
|
262
|
+
type: "informational",
|
|
263
|
+
level: "warning",
|
|
264
|
+
content: "Plugin failed to load: " + (pluginPaths.join(", ") || "unknown path"),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
255
267
|
}
|
|
256
268
|
|
|
257
269
|
if (parsed.yokeType === "turn_start") {
|
|
@@ -474,13 +486,29 @@ function attachMessageProcessor(ctx) {
|
|
|
474
486
|
for (var i = 0; i < content.length; i++) {
|
|
475
487
|
var block = content[i];
|
|
476
488
|
if (block.type === "tool_result" && !session.sentToolResults[block.tool_use_id]) {
|
|
477
|
-
//
|
|
478
|
-
|
|
489
|
+
// SDK: a WebFetch/WebSearch call that stepped aside for a
|
|
490
|
+
// priority message carries { detachedToolCall: true } here "in
|
|
491
|
+
// place of its Output: the call is still running, and its result
|
|
492
|
+
// reaches the model in a later turn" under the same tool_use_id.
|
|
493
|
+
// Do not mark it sent, or that later real tool_result for the
|
|
494
|
+
// same id would be silently dropped by the dedup check above;
|
|
495
|
+
// do not clear its active-task bookkeeping either, since it has
|
|
496
|
+
// not actually finished.
|
|
497
|
+
var isDetached = !!(parsed.toolUseResult
|
|
498
|
+
&& parsed.toolUseResult.detachedToolCall === true
|
|
499
|
+
&& parsed.detachedToolId === block.tool_use_id);
|
|
500
|
+
|
|
501
|
+
// Clear both lifecycle indexes when the real terminal result
|
|
502
|
+
// arrives. A later duplicate task_notification will then be
|
|
503
|
+
// recognized as already settled.
|
|
504
|
+
var finishedTask = isDetached
|
|
505
|
+
? { matched: false }
|
|
506
|
+
: taskLifecycle.finish(session, block.tool_use_id, null);
|
|
507
|
+
if (finishedTask.matched) {
|
|
479
508
|
sendAndRecord(session, {
|
|
480
509
|
type: "subagent_done",
|
|
481
510
|
parentToolId: block.tool_use_id,
|
|
482
511
|
});
|
|
483
|
-
delete session.activeTaskToolIds[block.tool_use_id];
|
|
484
512
|
}
|
|
485
513
|
var resultText = "";
|
|
486
514
|
var resultImages = [];
|
|
@@ -501,13 +529,14 @@ function attachMessageProcessor(ctx) {
|
|
|
501
529
|
}
|
|
502
530
|
}
|
|
503
531
|
}
|
|
504
|
-
session.sentToolResults[block.tool_use_id] = true;
|
|
532
|
+
if (!isDetached) session.sentToolResults[block.tool_use_id] = true;
|
|
505
533
|
var toolResultMsg = {
|
|
506
534
|
type: "tool_result",
|
|
507
535
|
id: block.tool_use_id,
|
|
508
536
|
content: resultText,
|
|
509
537
|
is_error: block.is_error || false,
|
|
510
538
|
};
|
|
539
|
+
if (isDetached) toolResultMsg.detached = true;
|
|
511
540
|
if (resultImages.length > 0) toolResultMsg.images = resultImages;
|
|
512
541
|
sendAndRecord(session, toolResultMsg);
|
|
513
542
|
}
|
|
@@ -515,6 +544,22 @@ function attachMessageProcessor(ctx) {
|
|
|
515
544
|
}
|
|
516
545
|
|
|
517
546
|
} else if (parsed.yokeType === "result") {
|
|
547
|
+
// SDK 0.3.274 batches queued background-task completion wakes into one
|
|
548
|
+
// model call. Every wake still emits a result, but all results before
|
|
549
|
+
// the final wake are successful task-notification-origin placeholders
|
|
550
|
+
// with no model turns and no result text. They are not user-turn
|
|
551
|
+
// completion boundaries: queued_turn_count cannot identify them because
|
|
552
|
+
// that field counts user sends only. Keep permissions, elicitations,
|
|
553
|
+
// processing state, completion callbacks and notifications intact until
|
|
554
|
+
// the real result arrives.
|
|
555
|
+
var isIntermediateBackgroundResult = parsed.subtype === "success"
|
|
556
|
+
&& parsed.is_error !== true
|
|
557
|
+
&& parsed.numTurns === 0
|
|
558
|
+
&& parsed.origin && parsed.origin.kind === "task-notification"
|
|
559
|
+
&& !parsed.localCommand
|
|
560
|
+
&& parsed.resultText === "";
|
|
561
|
+
if (isIntermediateBackgroundResult) return;
|
|
562
|
+
|
|
518
563
|
var overloadAbortRequested = !!session._overloadAbortRequested;
|
|
519
564
|
session._overloadAbortRequested = false;
|
|
520
565
|
session.blocks = {};
|
|
@@ -539,8 +584,16 @@ function attachMessageProcessor(ctx) {
|
|
|
539
584
|
sendAndRecord(session, { type: "ask_user_answered", toolId: lid });
|
|
540
585
|
}
|
|
541
586
|
session.pendingAskUser = keptAskUser;
|
|
542
|
-
|
|
543
|
-
|
|
587
|
+
// activeTaskToolIds / taskIdMap are deliberately NOT reset here. A
|
|
588
|
+
// backgrounded Task/agent/MCP call (SDK 0.3.286: a priority "now"
|
|
589
|
+
// message can move a running tool to the background and let the turn's
|
|
590
|
+
// own result fire without waiting for it) can still be genuinely
|
|
591
|
+
// running when this result arrives. Clearing these maps here would
|
|
592
|
+
// orphan that task:
|
|
593
|
+
// its real tool_result / task_notification would arrive later with no
|
|
594
|
+
// matching entry, and the client would never get subagent_done. Both
|
|
595
|
+
// maps are self-cleaning at the point each tracked task actually
|
|
596
|
+
// finishes (see the tool_result and task_notification handlers below).
|
|
544
597
|
// Only clear rateLimitResetsAt on genuine success (non-zero cost).
|
|
545
598
|
// When rate-limited, the SDK sends result with zero cost right after
|
|
546
599
|
// rate_limit_event; clearing here would prevent auto-continue scheduling.
|
|
@@ -549,6 +602,23 @@ function attachMessageProcessor(ctx) {
|
|
|
549
602
|
}
|
|
550
603
|
console.log("[sdk-bridge] result handler: session " + session.localId + " cost=" + parsed.cost + " rateLimitResetsAt=" + session.rateLimitResetsAt);
|
|
551
604
|
|
|
605
|
+
// A known, named startup failure (e.g. auth, provider_not_allowed) the
|
|
606
|
+
// CLI reports on the error result it writes before exiting -- distinct
|
|
607
|
+
// from an ordinary in-turn execution error, and not gated on a specific
|
|
608
|
+
// subtype since it marks the run never got past startup at all.
|
|
609
|
+
if (parsed.startupFailureReason) {
|
|
610
|
+
console.error("[sdk-bridge] Startup failure for session " + session.localId + ": " + parsed.startupFailureReason);
|
|
611
|
+
session.isProcessing = false;
|
|
612
|
+
session._awaitingTurnResult = false;
|
|
613
|
+
session._queuedTurnCount = 0;
|
|
614
|
+
taskLifecycle.clearAll(session);
|
|
615
|
+
onProcessingChanged();
|
|
616
|
+
sendAndRecord(session, { type: "error", text: "Claude failed to start: " + parsed.startupFailureReason });
|
|
617
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
618
|
+
sm.broadcastSessionList();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
552
622
|
// Handle SDK execution errors: show the error to the user instead of
|
|
553
623
|
// silently swallowing it. These have subtype "error_during_execution".
|
|
554
624
|
if (parsed.subtype === "error_during_execution") {
|
|
@@ -561,6 +631,7 @@ function attachMessageProcessor(ctx) {
|
|
|
561
631
|
session.isProcessing = false;
|
|
562
632
|
session._awaitingTurnResult = false;
|
|
563
633
|
session._queuedTurnCount = 0;
|
|
634
|
+
taskLifecycle.clearAll(session);
|
|
564
635
|
onProcessingChanged();
|
|
565
636
|
if (!overloadAbortRequested) {
|
|
566
637
|
sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
|
|
@@ -768,8 +839,9 @@ function attachMessageProcessor(ctx) {
|
|
|
768
839
|
}
|
|
769
840
|
|
|
770
841
|
} else if (parsed.yokeType === "task_notification") {
|
|
771
|
-
var
|
|
772
|
-
|
|
842
|
+
var finishedTask = taskLifecycle.finish(session, parsed.parentToolId, parsed.taskId);
|
|
843
|
+
var parentId = finishedTask.parentToolId;
|
|
844
|
+
if (parentId && finishedTask.matched) {
|
|
773
845
|
sendAndRecord(session, {
|
|
774
846
|
type: "subagent_done",
|
|
775
847
|
parentToolId: parentId,
|
|
@@ -778,14 +850,6 @@ function attachMessageProcessor(ctx) {
|
|
|
778
850
|
usage: parsed.usage || null,
|
|
779
851
|
});
|
|
780
852
|
}
|
|
781
|
-
if (session.taskIdMap) {
|
|
782
|
-
for (var k in session.taskIdMap) {
|
|
783
|
-
if (session.taskIdMap[k] === parsed.taskId) {
|
|
784
|
-
delete session.taskIdMap[k];
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
853
|
|
|
790
854
|
} else if (parsed.yokeType === "rate_limit") {
|
|
791
855
|
var info = parsed.rateLimitInfo;
|
|
@@ -965,6 +1029,7 @@ function attachMessageProcessor(ctx) {
|
|
|
965
1029
|
session.isProcessing = false;
|
|
966
1030
|
session._awaitingTurnResult = false;
|
|
967
1031
|
session._queuedTurnCount = 0;
|
|
1032
|
+
taskLifecycle.clearAll(session);
|
|
968
1033
|
onProcessingChanged();
|
|
969
1034
|
emitAuthRequired(session);
|
|
970
1035
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function clearAll(session) {
|
|
2
|
+
session.activeTaskToolIds = {};
|
|
3
|
+
session.taskIdMap = {};
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function clearForeground(session) {
|
|
7
|
+
var backgroundIds = {};
|
|
8
|
+
var backgroundTasks = Array.isArray(session.activeBackgroundTasks) ? session.activeBackgroundTasks : [];
|
|
9
|
+
for (var i = 0; i < backgroundTasks.length; i++) {
|
|
10
|
+
if (backgroundTasks[i] && backgroundTasks[i].task_id) backgroundIds[backgroundTasks[i].task_id] = true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
var keptTaskIds = {};
|
|
14
|
+
var keptToolIds = {};
|
|
15
|
+
var taskIdMap = session.taskIdMap || {};
|
|
16
|
+
for (var parentToolId in taskIdMap) {
|
|
17
|
+
var taskId = taskIdMap[parentToolId];
|
|
18
|
+
if (!backgroundIds[taskId]) continue;
|
|
19
|
+
keptTaskIds[parentToolId] = taskId;
|
|
20
|
+
if (session.activeTaskToolIds && session.activeTaskToolIds[parentToolId]) keptToolIds[parentToolId] = true;
|
|
21
|
+
}
|
|
22
|
+
session.activeTaskToolIds = keptToolIds;
|
|
23
|
+
session.taskIdMap = keptTaskIds;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function finish(session, parentToolId, taskId) {
|
|
27
|
+
var parentId = parentToolId || null;
|
|
28
|
+
var matched = false;
|
|
29
|
+
|
|
30
|
+
if (parentId && session.activeTaskToolIds && session.activeTaskToolIds[parentId]) {
|
|
31
|
+
matched = true;
|
|
32
|
+
delete session.activeTaskToolIds[parentId];
|
|
33
|
+
}
|
|
34
|
+
if (parentId && session.taskIdMap && session.taskIdMap[parentId]) {
|
|
35
|
+
matched = true;
|
|
36
|
+
delete session.taskIdMap[parentId];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (taskId && session.taskIdMap) {
|
|
40
|
+
for (var key in session.taskIdMap) {
|
|
41
|
+
if (session.taskIdMap[key] !== taskId) continue;
|
|
42
|
+
matched = true;
|
|
43
|
+
delete session.taskIdMap[key];
|
|
44
|
+
if (session.activeTaskToolIds) delete session.activeTaskToolIds[key];
|
|
45
|
+
if (!parentId) parentId = key;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { matched: matched, parentToolId: parentId };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { clearAll: clearAll, clearForeground: clearForeground, finish: finish };
|
package/lib/sessions.js
CHANGED
|
@@ -323,7 +323,7 @@ function createSessionManager(opts) {
|
|
|
323
323
|
var messageUUIDs = [];
|
|
324
324
|
for (var k = 0; k < loaded[i].history.length; k++) {
|
|
325
325
|
if (loaded[i].history[k].type === "message_uuid") {
|
|
326
|
-
messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k });
|
|
326
|
+
messageUUIDs.push({ uuid: loaded[i].history[k].uuid, type: loaded[i].history[k].messageType, historyIndex: k, turnId: loaded[i].history[k].turnId || null });
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
var hasTitleProvenance = Object.prototype.hasOwnProperty.call(m, "titleProvisional")
|
|
@@ -337,6 +337,9 @@ function canUseTool(toolName, input, opts) {
|
|
|
337
337
|
decisionReason: opts.decisionReason || "",
|
|
338
338
|
defaultToNo: opts.defaultToNo === true,
|
|
339
339
|
suppressAlwaysAllowRule: opts.suppressAlwaysAllowRule === true,
|
|
340
|
+
mcpServer: (opts.mcpServer && typeof opts.mcpServer === "object")
|
|
341
|
+
? { name: opts.mcpServer.name || null, source: opts.mcpServer.source || null }
|
|
342
|
+
: null,
|
|
340
343
|
});
|
|
341
344
|
return new Promise(function(resolve) {
|
|
342
345
|
pendingPermissions[requestId] = resolve;
|
|
@@ -257,6 +257,7 @@ function flattenEvent(raw) {
|
|
|
257
257
|
if (raw.resume_reason) base.resumeReason = raw.resume_reason;
|
|
258
258
|
if (Number.isInteger(raw.result_index)) base.resultIndex = raw.result_index;
|
|
259
259
|
if (raw.local_command) base.localCommand = raw.local_command;
|
|
260
|
+
if (raw.origin && typeof raw.origin === "object") base.origin = raw.origin;
|
|
260
261
|
if (raw.session_id) base.sessionId = raw.session_id;
|
|
261
262
|
if (raw.uuid) {
|
|
262
263
|
base.uuid = raw.uuid;
|
|
@@ -342,6 +343,10 @@ function flattenEvent(raw) {
|
|
|
342
343
|
base.slashCommands = raw.slash_commands;
|
|
343
344
|
base.fastModeState = raw.fast_mode_state || null;
|
|
344
345
|
base.permissionMode = raw.permissionMode || raw.permission_mode || null;
|
|
346
|
+
// SDK 0.3.283: entries for a --plugin-dir that did not load, each with
|
|
347
|
+
// a `path`. Forwarded so the user can see a plugin silently failed
|
|
348
|
+
// instead of finding its commands/skills simply missing.
|
|
349
|
+
base.pluginErrors = Array.isArray(raw.plugin_errors) ? raw.plugin_errors : null;
|
|
345
350
|
return base;
|
|
346
351
|
}
|
|
347
352
|
if (raw.subtype === "status") {
|
|
@@ -392,7 +397,7 @@ function flattenEvent(raw) {
|
|
|
392
397
|
}
|
|
393
398
|
if (raw.subtype === "task_notification") {
|
|
394
399
|
base.yokeType = "task_notification";
|
|
395
|
-
base.parentToolId = raw.parent_tool_use_id;
|
|
400
|
+
base.parentToolId = raw.tool_use_id || raw.parent_tool_use_id;
|
|
396
401
|
base.taskId = raw.task_id;
|
|
397
402
|
base.status = raw.status || "completed";
|
|
398
403
|
base.summary = raw.summary || "";
|
|
@@ -475,6 +480,8 @@ function flattenEvent(raw) {
|
|
|
475
480
|
if (raw.type === "result") {
|
|
476
481
|
base.yokeType = "result";
|
|
477
482
|
base.is_error = raw.is_error === true;
|
|
483
|
+
base.numTurns = Number.isInteger(raw.num_turns) ? raw.num_turns : null;
|
|
484
|
+
base.resultText = typeof raw.result === "string" ? raw.result : null;
|
|
478
485
|
if (Number.isInteger(raw.queued_turn_count) && raw.queued_turn_count >= 0) base.queuedTurnCount = raw.queued_turn_count;
|
|
479
486
|
base.cost = raw.total_cost_usd;
|
|
480
487
|
base.duration = raw.duration_ms;
|
|
@@ -483,6 +490,10 @@ function flattenEvent(raw) {
|
|
|
483
490
|
base.subtype = raw.subtype;
|
|
484
491
|
base.errors = raw.errors;
|
|
485
492
|
base.terminalReason = raw.terminal_reason;
|
|
493
|
+
// SDK 0.3.280: a known, named reason a stream-json run failed to start
|
|
494
|
+
// (e.g. auth, provider_not_allowed) carried on the error result the CLI
|
|
495
|
+
// writes before exiting, distinct from an ordinary in-turn execution error.
|
|
496
|
+
base.startupFailureReason = raw.startup_failure_reason || null;
|
|
486
497
|
base.fastModeState = raw.fast_mode_state || null;
|
|
487
498
|
return base;
|
|
488
499
|
}
|
|
@@ -499,6 +510,17 @@ function flattenEvent(raw) {
|
|
|
499
510
|
base.yokeType = "message";
|
|
500
511
|
base.messageRole = raw.type;
|
|
501
512
|
base.content = raw.message ? raw.message.content : null;
|
|
513
|
+
// Structured sibling metadata for the single tool_result this message
|
|
514
|
+
// carries (SDK type docs: "the tool's full Output object, not the string
|
|
515
|
+
// content sent to the model" -- never the text Clay actually renders).
|
|
516
|
+
// Its only use here is detecting `{ detachedToolCall: true }`: a call
|
|
517
|
+
// that stepped aside for a priority message, whose real result "reaches
|
|
518
|
+
// the model in a later turn" under the same tool_use_id.
|
|
519
|
+
base.toolUseResult = raw.tool_use_result || null;
|
|
520
|
+
if (raw.tool_use_result && raw.tool_use_result.detachedToolCall === true && Array.isArray(base.content)) {
|
|
521
|
+
var detachedResults = base.content.filter(function(block) { return block && block.type === "tool_result"; });
|
|
522
|
+
if (detachedResults.length === 1) base.detachedToolId = detachedResults[0].tool_use_id || null;
|
|
523
|
+
}
|
|
502
524
|
return base;
|
|
503
525
|
}
|
|
504
526
|
|
|
@@ -519,7 +541,7 @@ function flattenEvent(raw) {
|
|
|
519
541
|
// --- task_notification ---
|
|
520
542
|
if (raw.type === "task_notification") {
|
|
521
543
|
base.yokeType = "task_notification";
|
|
522
|
-
base.parentToolId = raw.parent_tool_use_id;
|
|
544
|
+
base.parentToolId = raw.tool_use_id || raw.parent_tool_use_id;
|
|
523
545
|
base.taskId = raw.task_id;
|
|
524
546
|
base.status = raw.status || "completed";
|
|
525
547
|
base.summary = raw.summary || "";
|
|
@@ -1134,6 +1156,7 @@ function createWorkerQueryHandle(worker, canUseTool, onElicitation, callMcpTool,
|
|
|
1134
1156
|
decisionReason: msg.decisionReason,
|
|
1135
1157
|
defaultToNo: msg.defaultToNo === true,
|
|
1136
1158
|
suppressAlwaysAllowRule: msg.suppressAlwaysAllowRule === true,
|
|
1159
|
+
mcpServer: msg.mcpServer || null,
|
|
1137
1160
|
signal: interactionAbort.signal,
|
|
1138
1161
|
}).then(function(result) {
|
|
1139
1162
|
if (interactionAbort.signal.aborted) result = { behavior: "deny", message: "Query cancelled" };
|
|
@@ -190,7 +190,10 @@ function flattenEvent(notification, state) {
|
|
|
190
190
|
state.lastOutputTokens = null;
|
|
191
191
|
state.currentContextUsage = null;
|
|
192
192
|
var userUuid = generateUuid();
|
|
193
|
-
|
|
193
|
+
// turnId (Codex's own id, distinct from Clay's local uuid) is required
|
|
194
|
+
// by thread/revert's beforeTurnId; without it this turn can never be
|
|
195
|
+
// the target of a later rollback.
|
|
196
|
+
events.push({ yokeType: "turn_start", uuid: userUuid, messageType: "user", turnId: state.currentTurnId });
|
|
194
197
|
return events;
|
|
195
198
|
}
|
|
196
199
|
|
|
@@ -2145,14 +2148,37 @@ function createCodexAdapter(opts) {
|
|
|
2145
2148
|
return { sessionId: newThreadId };
|
|
2146
2149
|
});
|
|
2147
2150
|
},
|
|
2148
|
-
|
|
2151
|
+
// Codex 0.160+ replaced the count-based `thread/rollback` (threadId,
|
|
2152
|
+
// numTurns) with `thread/revert` (threadId, beforeTurnId): the turn
|
|
2153
|
+
// excluded from the replacement history, together with every later turn.
|
|
2154
|
+
rollbackThread: async function(threadId, beforeTurnId, opts) {
|
|
2149
2155
|
var requestedLinuxUser = opts && opts.linuxUser;
|
|
2150
2156
|
if (!_runtimeLinuxUser && requestedLinuxUser) {
|
|
2151
|
-
return getUserRuntime(requestedLinuxUser).rollbackThread(threadId,
|
|
2157
|
+
return getUserRuntime(requestedLinuxUser).rollbackThread(threadId, beforeTurnId, withoutLinuxUser(opts));
|
|
2152
2158
|
}
|
|
2153
2159
|
if (_requiresLinuxUser) return Promise.reject(new Error("Codex requires a mapped Linux user while OS-user isolation is enabled"));
|
|
2154
|
-
|
|
2155
|
-
|
|
2160
|
+
// A missing/stopped app-server means the revert was never sent, so the
|
|
2161
|
+
// Codex thread was never touched. Resolving null here (as this used to)
|
|
2162
|
+
// reads as success to the caller, which then trims Clay's own local
|
|
2163
|
+
// history to match a rollback that never happened. Lazily (re)start the
|
|
2164
|
+
// app-server first, as createQuery does, and only fail if that does not
|
|
2165
|
+
// bring it up -- never silently report success without sending anything.
|
|
2166
|
+
if (!_appServer || !_appServer.started) await adapter.init(opts || {});
|
|
2167
|
+
if (!_appServer || !_appServer.started) {
|
|
2168
|
+
throw new Error("Codex app-server is not running; the thread was not rolled back.");
|
|
2169
|
+
}
|
|
2170
|
+
var revertResult = await _appServer.send("thread/revert", { threadId: threadId, beforeTurnId: beforeTurnId }, 30000);
|
|
2171
|
+
// Confirm the server actually reports the exact thread it was asked to
|
|
2172
|
+
// cut, rather than trusting any truthy response: ThreadRevertResponse's
|
|
2173
|
+
// `thread` field is required and always carries that thread's own `id`
|
|
2174
|
+
// (per the app-server's generated protocol schema). A response for a
|
|
2175
|
+
// different thread id -- e.g. misrouted after a resume/fork -- or a
|
|
2176
|
+
// malformed/empty response is treated the same as a failure, never as
|
|
2177
|
+
// a silent success.
|
|
2178
|
+
if (!revertResult || !revertResult.thread || revertResult.thread.id !== threadId) {
|
|
2179
|
+
throw new Error("Codex did not confirm the thread revert.");
|
|
2180
|
+
}
|
|
2181
|
+
return revertResult;
|
|
2156
2182
|
},
|
|
2157
2183
|
|
|
2158
2184
|
// Shutdown the app-server process
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clay-server",
|
|
3
|
-
"version": "4.7.0
|
|
3
|
+
"version": "4.7.0",
|
|
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"
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"homepage": "https://github.com/chadbyte/clay#readme",
|
|
48
48
|
"author": "Chad",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
50
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.287",
|
|
51
51
|
"@lydell/node-pty": "^1.2.0-beta.3",
|
|
52
|
-
"@openai/codex": "^0.
|
|
52
|
+
"@openai/codex": "^0.160.0",
|
|
53
53
|
"@seald-io/nedb": "^4.1.2",
|
|
54
54
|
"imapflow": "^1.7.8",
|
|
55
55
|
"nodemailer": "^9.1.1",
|