clay-server 5.2.0-beta.4 → 5.2.0-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/account-auth-socket.js +1 -0
- package/lib/builtin-mates.js +17 -14
- package/lib/canonical-clay-record-authority.js +15 -0
- package/lib/canonical-clay-scheduling-authority.js +19 -0
- package/lib/claude-hook-installer.js +28 -0
- package/lib/clay-mate-chat-receipts.js +26 -0
- package/lib/clay-mate-chat.js +87 -0
- package/lib/clay-mate-knowledge-input.js +26 -0
- package/lib/clay-mate-knowledge.js +95 -0
- package/lib/clay-mate-lifecycle.js +95 -0
- package/lib/clay-product-control-input.js +33 -0
- package/lib/clay-product-control-tools.js +51 -0
- package/lib/clay-product-control.js +237 -0
- package/lib/clay-project-lifecycle.js +154 -0
- package/lib/clay-project-record-input.js +44 -0
- package/lib/clay-project-records.js +83 -0
- package/lib/clay-scheduling-input.js +21 -0
- package/lib/clay-scheduling-receipts.js +26 -0
- package/lib/clay-scheduling.js +119 -0
- package/lib/clay-screen-controls.js +96 -0
- package/lib/clay-workspace-controls.js +83 -0
- package/lib/clay-workspace-input.js +17 -0
- package/lib/clay-workspace-receipts.js +26 -0
- package/lib/daemon-project-access.js +11 -0
- package/lib/daemon-project-lifecycle-callbacks.js +15 -0
- package/lib/daemon-project-lifecycle.js +262 -0
- package/lib/daemon-projects.js +1 -1
- package/lib/daemon.js +73 -221
- package/lib/issues-service.js +29 -15
- package/lib/knowledge-db-import.js +5 -4
- package/lib/knowledge-db-service.js +9 -6
- package/lib/knowledge-document-index.js +2 -1
- package/lib/knowledge-product-operations.js +46 -0
- package/lib/mate-creation-mcp-server.js +2 -0
- package/lib/mate-knowledge-canonical-access.js +10 -0
- package/lib/mate-knowledge-service.js +7 -4
- package/lib/mate-lifecycle-service.js +94 -0
- package/lib/mate-message-delivery.js +43 -0
- package/lib/mate-ready-creation.js +9 -1
- package/lib/mate-schedule-input.js +29 -0
- package/lib/mate-schedule-lifecycle.js +151 -0
- package/lib/mate-scheduling.js +103 -0
- package/lib/mates.js +4 -3
- package/lib/notes-checklist.js +50 -0
- package/lib/notes.js +49 -3
- package/lib/os-users.js +20 -5
- package/lib/project-admin-overview.js +51 -0
- package/lib/project-ask-user.js +4 -2
- package/lib/project-delegated-follow-up.js +2 -1
- package/lib/project-directory-claim.js +65 -0
- package/lib/project-file-operations.js +61 -0
- package/lib/project-filesystem.js +13 -87
- package/lib/project-full-access.js +23 -2
- package/lib/project-knowledge.js +32 -18
- package/lib/project-lifecycle-operations.js +98 -0
- package/lib/project-lifecycle-process-witness.js +32 -0
- package/lib/project-lifecycle-runtime.js +50 -0
- package/lib/project-linux-access.js +22 -0
- package/lib/project-logs-canonical-clay.js +27 -0
- package/lib/project-logs-clay-query.js +16 -0
- package/lib/project-logs-schema.js +4 -2
- package/lib/project-logs-service.js +8 -7
- package/lib/project-logs.js +1 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-mate-chat-control.js +55 -0
- package/lib/project-mate-creation-proposal.js +18 -3
- package/lib/project-models.js +14 -31
- package/lib/project-notifications.js +74 -16
- package/lib/project-ownership.js +77 -0
- package/lib/project-pair-result-delivery.js +14 -3
- package/lib/project-pending-message-queue.js +1 -0
- package/lib/project-product-control.js +87 -0
- package/lib/project-product-session-queue.js +46 -0
- package/lib/project-push-access.js +28 -0
- package/lib/project-scheduled-message-queue.js +30 -0
- package/lib/project-scheduled-messages.js +63 -64
- package/lib/project-scheduled-task-control.js +83 -0
- package/lib/project-scheduled-tasks.js +13 -2
- package/lib/project-scheduling-access.js +10 -0
- package/lib/project-session-actions.js +65 -0
- package/lib/project-session-delete.js +8 -1
- package/lib/project-session-github.js +47 -12
- package/lib/project-session-history-actions.js +165 -0
- package/lib/project-session-runtime-actions.js +63 -0
- package/lib/project-session-vendor.js +54 -0
- package/lib/project-sessions.js +64 -241
- package/lib/project-user-message.js +30 -1
- package/lib/project-workspace-operations.js +126 -0
- package/lib/project-workspace-query.js +9 -2
- package/lib/project.js +95 -3
- package/lib/public/css/clay-tool-activity.css +51 -0
- package/lib/public/css/home-mate-creation.css +5 -0
- package/lib/public/css/overlays.css +4 -2
- package/lib/public/css/project-admin-overview.css +13 -0
- package/lib/public/css/session-folders.css +11 -0
- package/lib/public/css/session-github.css +3 -0
- package/lib/public/css/sticky-notes.css +61 -1
- package/lib/public/index.html +1 -1
- package/lib/public/modules/admin-settings-projects.js +6 -2
- package/lib/public/modules/app-connection.js +8 -1
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +18 -3
- package/lib/public/modules/app-projects.js +5 -0
- package/lib/public/modules/clay-owner-navigation.js +27 -0
- package/lib/public/modules/clay-tool-activity.js +140 -0
- package/lib/public/modules/clay-tool-presentation.js +338 -0
- package/lib/public/modules/command-palette.js +1 -1
- package/lib/public/modules/default-ai.js +11 -3
- package/lib/public/modules/home-debate-planning.js +5 -5
- package/lib/public/modules/home-mate-chat.js +8 -6
- package/lib/public/modules/home-mate-creation.js +73 -1
- package/lib/public/modules/personal-settings-dialog.js +7 -1
- package/lib/public/modules/project-access-recovery.js +17 -0
- package/lib/public/modules/project-admin-overview.js +21 -0
- package/lib/public/modules/session-folder-layout.js +1 -1
- package/lib/public/modules/session-folder-view-menu.js +9 -0
- package/lib/public/modules/session-github.js +65 -8
- package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
- package/lib/public/modules/sticky-note-markdown.js +72 -6
- package/lib/public/modules/sticky-notes-actions.js +30 -0
- package/lib/public/modules/sticky-notes-browser.js +9 -2
- package/lib/public/modules/sticky-notes-card.js +23 -7
- package/lib/public/modules/sticky-notes-checklist.js +71 -0
- package/lib/public/modules/sticky-notes-editor.js +108 -44
- package/lib/public/modules/sticky-notes-shared.js +50 -2
- package/lib/public/modules/sticky-notes-state.js +66 -0
- package/lib/public/modules/sticky-notes.js +34 -51
- package/lib/public/modules/thinking-lifecycle.js +33 -3
- package/lib/public/modules/thinking-view.js +23 -1
- package/lib/public/modules/tools.js +34 -6
- package/lib/public/modules/user-settings-appearance.js +10 -3
- package/lib/public/modules/user-settings-editors.js +1 -1
- package/lib/public/modules/user-settings-layout.js +11 -2
- package/lib/public/modules/user-settings-preferences.js +6 -2
- package/lib/public/modules/user-settings-profile.js +12 -2
- package/lib/public/modules/user-settings.js +3 -2
- package/lib/public/style.css +2 -0
- package/lib/scheduled-task-records.js +3 -1
- package/lib/scheduler.js +2 -0
- package/lib/sdk-bridge.js +220 -21
- package/lib/sdk-message-processor.js +2 -0
- package/lib/sdk-response-recovery.js +155 -0
- package/lib/server-admin.js +63 -16
- package/lib/server-dm.js +1 -1
- package/lib/server-global-ws.js +10 -20
- package/lib/server-home-chat-events.js +7 -3
- package/lib/server-home-chat.js +4 -16
- package/lib/server-home-mate-creation.js +1 -0
- package/lib/server-home-mcp-consent.js +3 -3
- package/lib/server-mates.js +25 -67
- package/lib/server-owner-ui.js +77 -0
- package/lib/server-path-access.js +38 -0
- package/lib/server-project-removal.js +22 -0
- package/lib/server.js +99 -29
- package/lib/session-config-transition.js +45 -0
- package/lib/session-folders.js +7 -2
- package/lib/session-github.js +9 -2
- package/lib/session-visibility.js +5 -2
- package/lib/sessions.js +10 -0
- package/lib/terminal-manager.js +52 -0
- package/lib/users-permissions.js +2 -8
- package/lib/users.js +4 -0
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +8 -2
- package/lib/yoke/adapters/claude.js +47 -8
- package/lib/yoke/adapters/codex.js +52 -111
- package/lib/yoke/claude-worker-startup.js +28 -0
- package/lib/yoke/codex-idle-recovery.js +81 -1
- package/lib/yoke/codex-recovery-admission.js +164 -0
- package/lib/yoke/session-tool-transport.js +3 -0
- package/package.json +1 -1
package/lib/sdk-bridge.js
CHANGED
|
@@ -323,6 +323,7 @@ function createSDKBridge(opts) {
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
function stopIdleReaper() {
|
|
326
|
+
responseRecovery.dispose();
|
|
326
327
|
if (_idleReaperTimer) {
|
|
327
328
|
clearInterval(_idleReaperTimer);
|
|
328
329
|
_idleReaperTimer = null;
|
|
@@ -389,6 +390,17 @@ function createSDKBridge(opts) {
|
|
|
389
390
|
var sendAndRecord = msgProcessor.sendAndRecord;
|
|
390
391
|
var sendToSession = msgProcessor.sendToSession;
|
|
391
392
|
|
|
393
|
+
var responseRecovery = require("./sdk-response-recovery").attachResponseRecovery(Object.assign({
|
|
394
|
+
sessions: sm.sessions,
|
|
395
|
+
changed: onProcessingChanged,
|
|
396
|
+
record: sendAndRecord,
|
|
397
|
+
send: sendToSession,
|
|
398
|
+
broadcast: function() { if (sm.broadcastSessionList) sm.broadcastSessionList(); },
|
|
399
|
+
restart: function(session, payload, state) {
|
|
400
|
+
return startQuery(session, payload.text, payload.images, payload.linuxUser, payload.beforePush, null, state);
|
|
401
|
+
},
|
|
402
|
+
}, opts.responseRecovery || {}));
|
|
403
|
+
|
|
392
404
|
// --- MCP elicitation ---
|
|
393
405
|
|
|
394
406
|
function handleElicitation(session, request, opts) {
|
|
@@ -417,6 +429,7 @@ function createSDKBridge(opts) {
|
|
|
417
429
|
|
|
418
430
|
if (pushModule) {
|
|
419
431
|
pushModule.sendPush({
|
|
432
|
+
sessionId: session.localId,
|
|
420
433
|
type: "elicitation",
|
|
421
434
|
slug: slug,
|
|
422
435
|
title: (request.serverName || "MCP Server") + " needs input",
|
|
@@ -467,6 +480,7 @@ function createSDKBridge(opts) {
|
|
|
467
480
|
|
|
468
481
|
if (pushModule) {
|
|
469
482
|
pushModule.sendPush({
|
|
483
|
+
sessionId: session.localId,
|
|
470
484
|
type: "user_dialog",
|
|
471
485
|
slug: slug,
|
|
472
486
|
title: "Claude needs a decision",
|
|
@@ -745,6 +759,34 @@ function createSDKBridge(opts) {
|
|
|
745
759
|
// Workspace inspection and proposal posting are session-bound. Keep this
|
|
746
760
|
// list exact so a future execution/mutation tool cannot inherit approval.
|
|
747
761
|
var safeWorkspaceTools = {
|
|
762
|
+
"mcp__clay-workspace__read_owner_screen": true,
|
|
763
|
+
"mcp__clay-workspace__inspect_owner_screen_change": true,
|
|
764
|
+
"mcp__clay-workspace__control_owner_screen": true,
|
|
765
|
+
"mcp__clay-workspace__discover_product_controls": true,
|
|
766
|
+
"mcp__clay-workspace__list_owned_mates": true,
|
|
767
|
+
"mcp__clay-workspace__begin_schedule_control_interview": true,
|
|
768
|
+
"mcp__clay-workspace__read_schedule_controls": true,
|
|
769
|
+
"mcp__clay-workspace__inspect_schedule_change": true,
|
|
770
|
+
"mcp__clay-workspace__control_schedules": true,
|
|
771
|
+
"mcp__clay-workspace__read_mate_chat_control": true,
|
|
772
|
+
"mcp__clay-workspace__inspect_mate_chat_change": true,
|
|
773
|
+
"mcp__clay-workspace__control_mate_chat": true,
|
|
774
|
+
"mcp__clay-workspace__read_project_workspace": true,
|
|
775
|
+
"mcp__clay-workspace__read_workspace_receipt": true,
|
|
776
|
+
"mcp__clay-workspace__inspect_workspace_change": true,
|
|
777
|
+
"mcp__clay-workspace__control_project_workspace": true,
|
|
778
|
+
"mcp__clay-workspace__read_project_records": true,
|
|
779
|
+
"mcp__clay-workspace__inspect_project_record_change": true,
|
|
780
|
+
"mcp__clay-workspace__control_project_records": true,
|
|
781
|
+
"mcp__clay-workspace__read_mate_knowledge_control": true,
|
|
782
|
+
"mcp__clay-workspace__inspect_mate_knowledge_change": true,
|
|
783
|
+
"mcp__clay-workspace__control_mate_knowledge": true,
|
|
784
|
+
"mcp__clay-workspace__inspect_mate_control": true,
|
|
785
|
+
"mcp__clay-workspace__control_mate_lifecycle": true,
|
|
786
|
+
"mcp__clay-workspace__inspect_project_control": true,
|
|
787
|
+
"mcp__clay-workspace__control_project_lifecycle": true,
|
|
788
|
+
"mcp__clay-workspace__inspect_session_control": true,
|
|
789
|
+
"mcp__clay-workspace__control_project_session": true,
|
|
748
790
|
"mcp__clay-workspace__list_projects": true,
|
|
749
791
|
"mcp__clay-workspace__list_project_sessions": true,
|
|
750
792
|
"mcp__clay-workspace__search_project_history": true,
|
|
@@ -902,6 +944,7 @@ function createSDKBridge(opts) {
|
|
|
902
944
|
|
|
903
945
|
if (pushModule) {
|
|
904
946
|
pushModule.sendPush({
|
|
947
|
+
sessionId: session.localId,
|
|
905
948
|
type: "permission_request",
|
|
906
949
|
slug: slug,
|
|
907
950
|
requestId: requestId,
|
|
@@ -1006,6 +1049,7 @@ function createSDKBridge(opts) {
|
|
|
1006
1049
|
function recordRuntimeEffectivePermissionMode(session, queryInstance, queryGeneration, message) {
|
|
1007
1050
|
if (!message || (message.yokeType !== "init" && message.yokeType !== "status") || !message.permissionMode) return false;
|
|
1008
1051
|
if (!ownsQueryRuntime(session, queryInstance, queryGeneration)) return false;
|
|
1052
|
+
session._permissionObservationRevision = Number(session._permissionObservationRevision || 0) + 1;
|
|
1009
1053
|
session.effectivePermissionMode = message.permissionMode;
|
|
1010
1054
|
session.permissionModeFallbackReason = null;
|
|
1011
1055
|
sm.saveSessionFile(session);
|
|
@@ -1047,7 +1091,7 @@ function createSDKBridge(opts) {
|
|
|
1047
1091
|
try {
|
|
1048
1092
|
for await (var msg of myQueryInstance) {
|
|
1049
1093
|
// An old iterator may drain after Stop or after a replacement query starts.
|
|
1050
|
-
if (!ownsQueryRuntime(session, myQueryInstance, myQueryGeneration)) continue;
|
|
1094
|
+
if (!ownsQueryRuntime(session, myQueryInstance, myQueryGeneration) || responseRecovery.stopping(session)) continue;
|
|
1051
1095
|
if ((session.taskStopRequested || (myAbortController && myAbortController.signal.aborted))
|
|
1052
1096
|
&& msg && msg.yokeType !== "error" && msg.yokeType !== "interrupted") continue;
|
|
1053
1097
|
if (msg && msg.yokeType !== "text_delta" && msg.yokeType !== "thinking_delta" && msg.yokeType !== "tool_input_delta") {
|
|
@@ -1091,6 +1135,7 @@ function createSDKBridge(opts) {
|
|
|
1091
1135
|
if (continuationIdentity && !ownsQueryRuntime(session, myQueryInstance, myQueryGeneration)) {
|
|
1092
1136
|
continue;
|
|
1093
1137
|
}
|
|
1138
|
+
responseRecovery.event(session, msg);
|
|
1094
1139
|
processSDKMessage(session, msg);
|
|
1095
1140
|
if (continuationIdentity) {
|
|
1096
1141
|
settleContinuationStartup(session, myQueryInstance, myQueryGeneration, true, null);
|
|
@@ -1120,7 +1165,7 @@ function createSDKBridge(opts) {
|
|
|
1120
1165
|
// Stream ended normally after a task stop — no "result" message was sent,
|
|
1121
1166
|
// so the session is still marked as processing. Send interrupted feedback.
|
|
1122
1167
|
console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
|
|
1123
|
-
var stillOwnsRuntime = session.queryInstance === myQueryInstance;
|
|
1168
|
+
var stillOwnsRuntime = session.queryInstance === myQueryInstance && !responseRecovery.stopping(session);
|
|
1124
1169
|
settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor query ended before initialization.");
|
|
1125
1170
|
if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
|
|
1126
1171
|
session._lastTurnInterrupted = true;
|
|
@@ -1158,7 +1203,7 @@ function createSDKBridge(opts) {
|
|
|
1158
1203
|
}
|
|
1159
1204
|
} catch (err) {
|
|
1160
1205
|
settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, err.message || "The successor query failed before initialization.");
|
|
1161
|
-
if (session.isProcessing && session.queryInstance === myQueryInstance) {
|
|
1206
|
+
if (session.isProcessing && session.queryInstance === myQueryInstance && !responseRecovery.stopping(session)) {
|
|
1162
1207
|
session.isProcessing = false;
|
|
1163
1208
|
session._awaitingTurnResult = false;
|
|
1164
1209
|
session._queuedTurnCount = 0;
|
|
@@ -1272,7 +1317,8 @@ function createSDKBridge(opts) {
|
|
|
1272
1317
|
}
|
|
1273
1318
|
if (session.messageQueue === myMessageQueue) session.messageQueue = null;
|
|
1274
1319
|
if (session.abortController === myAbortController) session.abortController = null;
|
|
1275
|
-
if (ownsRuntime) {
|
|
1320
|
+
if (ownsRuntime && !responseRecovery.stopping(session)) {
|
|
1321
|
+
responseRecovery.cancel(session);
|
|
1276
1322
|
session.taskStopRequested = false;
|
|
1277
1323
|
session.pendingPermissions = {};
|
|
1278
1324
|
// Preserve only restored legacy MCP entries. Canonical Yoke requests
|
|
@@ -1321,7 +1367,7 @@ function createSDKBridge(opts) {
|
|
|
1321
1367
|
}
|
|
1322
1368
|
}
|
|
1323
1369
|
}
|
|
1324
|
-
if (!session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
|
|
1370
|
+
if (!responseRecovery.stopping(session) && !session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
|
|
1325
1371
|
}
|
|
1326
1372
|
}
|
|
1327
1373
|
|
|
@@ -1442,22 +1488,70 @@ function createSDKBridge(opts) {
|
|
|
1442
1488
|
|
|
1443
1489
|
// Wrapper: marks the boot window so pushMessage can buffer instead of
|
|
1444
1490
|
// dropping messages that arrive while createQuery is still awaiting.
|
|
1445
|
-
async function startQuery(session, text, images, linuxUser, beforePush, onAccepted) {
|
|
1491
|
+
async function startQuery(session, text, images, linuxUser, beforePush, onAccepted, retryState) {
|
|
1492
|
+
if (opts.isLifecycleExecutionPaused && opts.isLifecycleExecutionPaused()) {
|
|
1493
|
+
sendAndRecord(session, { type: "error", text: "Project execution is paused for lifecycle recovery. Reconcile the saved operation, or send a new owner message after restoring the registration." });
|
|
1494
|
+
session.isProcessing = false; onProcessingChanged(); sendAndRecord(session, { type: "done", code: 1 }); return false;
|
|
1495
|
+
}
|
|
1496
|
+
if (session._responseRecoveryStopPending && !retryState) {
|
|
1497
|
+
session.isProcessing = false;
|
|
1498
|
+
onProcessingChanged();
|
|
1499
|
+
sendAndRecord(session, { type: "error", text: "The previous Claude attempt has not confirmed stopped. Check the connection before sending this message again." });
|
|
1500
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
if (responseRecovery.stopping(session) && !retryState) return false;
|
|
1446
1504
|
delete session._lastTurnInterrupted;
|
|
1505
|
+
var claude = (session.vendor || adapter.vendor) === "claude";
|
|
1506
|
+
var payload = { text: text, images: images, linuxUser: linuxUser, beforePush: beforePush };
|
|
1507
|
+
var recoveryState = claude ? responseRecovery.begin(session, payload, retryState) : null;
|
|
1508
|
+
if (recoveryState) recoveryState.handle = null;
|
|
1509
|
+
var guardedPush = function() {
|
|
1510
|
+
return !session.destroying && !session.taskStopRequested
|
|
1511
|
+
&& (!recoveryState || responseRecovery.valid(session, recoveryState) && !recoveryState.stopping)
|
|
1512
|
+
&& (typeof beforePush !== "function" || beforePush() === true);
|
|
1513
|
+
};
|
|
1514
|
+
guardedPush.exactProductDelivery = !!(beforePush && beforePush.exactProductDelivery);
|
|
1447
1515
|
session._queryGeneration = (session._queryGeneration || 0) + 1;
|
|
1448
1516
|
session._queryStarting = true;
|
|
1517
|
+
var starting = startQueryInner(session, text, images, linuxUser, guardedPush, onAccepted, recoveryState);
|
|
1518
|
+
if (recoveryState) recoveryState.starting = starting;
|
|
1449
1519
|
try {
|
|
1450
|
-
|
|
1520
|
+
var accepted = await starting;
|
|
1521
|
+
if (accepted === false && (!recoveryState || !recoveryState.stopping)) {
|
|
1522
|
+
responseRecovery.cancel(session);
|
|
1523
|
+
session.isProcessing = false;
|
|
1524
|
+
onProcessingChanged();
|
|
1525
|
+
}
|
|
1526
|
+
return accepted;
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
// Includes synchronous input queue failures, before stream consumption.
|
|
1529
|
+
var failedHandle = session.queryInstance;
|
|
1530
|
+
if (failedHandle) {
|
|
1531
|
+
try { if (typeof failedHandle.abort === "function") failedHandle.abort(); } catch (e) {}
|
|
1532
|
+
try { if (typeof failedHandle.close === "function") failedHandle.close(); } catch (e) {}
|
|
1533
|
+
}
|
|
1534
|
+
session.queryInstance = null;
|
|
1535
|
+
session.abortController = null;
|
|
1536
|
+
session.messageQueue = null;
|
|
1537
|
+
session.isProcessing = false;
|
|
1538
|
+
session._awaitingTurnResult = false;
|
|
1539
|
+
session._queuedTurnCount = 0;
|
|
1540
|
+
taskLifecycle.clearAll(session);
|
|
1541
|
+
if (!recoveryState || !recoveryState.stopping) {
|
|
1542
|
+
responseRecovery.cancel(session);
|
|
1543
|
+
sendAndRecord(session, { type: "error", text: "The agent connection failed while delivering your message. Please retry or check the connection." });
|
|
1544
|
+
sendAndRecord(session, { type: "done", code: 1 });
|
|
1545
|
+
onProcessingChanged();
|
|
1546
|
+
sm.broadcastSessionList();
|
|
1547
|
+
}
|
|
1548
|
+
return false;
|
|
1451
1549
|
} finally {
|
|
1452
1550
|
session._queryStarting = false;
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
session.pendingPush = [];
|
|
1458
|
-
}
|
|
1459
|
-
if (!session.queryInstance && !session.isProcessing) session.taskStopRequested = false;
|
|
1460
|
-
if (!session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
|
|
1551
|
+
delete session._codexRecoveryWaiting;
|
|
1552
|
+
if (!session.queryInstance && session.pendingPush && session.pendingPush.length) session.pendingPush = [];
|
|
1553
|
+
if (!session.queryInstance && !session.isProcessing && !session._pendingMessageDrainPaused && !responseRecovery.stopping(session)) session.taskStopRequested = false;
|
|
1554
|
+
if (!responseRecovery.stopping(session) && !session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
|
|
1461
1555
|
}
|
|
1462
1556
|
}
|
|
1463
1557
|
|
|
@@ -1501,7 +1595,7 @@ function createSDKBridge(opts) {
|
|
|
1501
1595
|
};
|
|
1502
1596
|
}
|
|
1503
1597
|
|
|
1504
|
-
async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted) {
|
|
1598
|
+
async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted, recoveryState) {
|
|
1505
1599
|
var sessionRuntimeEnv = getRuntimeEnv(session);
|
|
1506
1600
|
var resumedAtQueryStart = !!session.cliSessionId;
|
|
1507
1601
|
async function ensureVendorReady(vendor) {
|
|
@@ -1822,13 +1916,15 @@ function createSDKBridge(opts) {
|
|
|
1822
1916
|
if (sm && sm.sessions && typeof sm.sessions.get === "function" && sm.sessions.get(session.localId) !== session) return false;
|
|
1823
1917
|
return canUseSessionTools(session) === true;
|
|
1824
1918
|
}
|
|
1919
|
+
var hasMateScheduleTool = sessionToolDefs.some(function (tool) { return yoke.sessionTools.MATE_SCHEDULE_TOOLS.indexOf(tool.name) !== -1; });
|
|
1920
|
+
var recoverCodexMateSchedules = sessionAdapter.vendor === "codex" && !!session.cliSessionId && hasMateScheduleTool && queryContextAvailable();
|
|
1825
1921
|
var hasScheduleTool = false;
|
|
1826
1922
|
var hasScheduleInterviewTool = false;
|
|
1827
1923
|
var scheduleInterviewActive = null;
|
|
1828
1924
|
var scheduleQuestionLimit = null;
|
|
1829
1925
|
for (var sessionToolIndex = 0; sessionToolIndex < sessionToolDefs.length; sessionToolIndex++) {
|
|
1830
1926
|
if (yoke.sessionTools.SCHEDULE_TOOLS.indexOf(sessionToolDefs[sessionToolIndex].name) !== -1) hasScheduleTool = true;
|
|
1831
|
-
if (sessionToolDefs[sessionToolIndex].name
|
|
1927
|
+
if (["begin_scheduled_task_interview", "begin_mate_schedule_interview", "begin_schedule_control_interview"].indexOf(sessionToolDefs[sessionToolIndex].name) >= 0) {
|
|
1832
1928
|
hasScheduleInterviewTool = true;
|
|
1833
1929
|
scheduleInterviewActive = sessionToolDefs[sessionToolIndex].scheduleInterviewActive || null;
|
|
1834
1930
|
scheduleQuestionLimit = sessionToolDefs[sessionToolIndex].structuredQuestionLimit || null;
|
|
@@ -1860,9 +1956,10 @@ function createSDKBridge(opts) {
|
|
|
1860
1956
|
return "Ask exactly one question at a time, wait for the answer, then ask the next question.";
|
|
1861
1957
|
}
|
|
1862
1958
|
var sessionUserInputHandler = function(request, respond) {
|
|
1863
|
-
if (!structuredInputContextAvailable()) {
|
|
1959
|
+
if (queryRetired() || !structuredInputContextAvailable()) {
|
|
1864
1960
|
return respond.cancel("This structured-input request belongs to an unavailable or older query.");
|
|
1865
1961
|
}
|
|
1962
|
+
responseRecovery.progress(session);
|
|
1866
1963
|
var questionError = scheduleQuestionError(request);
|
|
1867
1964
|
if (questionError) return respond.cancel(questionError);
|
|
1868
1965
|
if (request.presentation === "elicitation" && request.diagnostics && request.diagnostics.elicitation) {
|
|
@@ -1895,6 +1992,11 @@ function createSDKBridge(opts) {
|
|
|
1895
1992
|
}
|
|
1896
1993
|
}
|
|
1897
1994
|
}
|
|
1995
|
+
if (sessionAdapter.vendor === "claude" && hasMateScheduleTool && typeof sessionAdapter.createToolServer === "function") {
|
|
1996
|
+
localMcpServers = Object.assign({}, localMcpServers || {});
|
|
1997
|
+
localMcpServers["clay-mate-schedules"] = sessionAdapter.createToolServer({ name: "clay-mate-schedules", version: "1.0.0",
|
|
1998
|
+
tools: sessionToolDefs.filter(function (tool) { return yoke.sessionTools.MATE_SCHEDULE_TOOLS.indexOf(tool.name) !== -1; }) });
|
|
1999
|
+
}
|
|
1898
2000
|
if (selectedUserInputMode === "fallback") {
|
|
1899
2001
|
var userInputServer = yoke.userInput.createFallbackServer(sessionAdapter, sessionUserInputHandler, { force: true, maxQuestions: scheduleQuestionLimit });
|
|
1900
2002
|
if (userInputServer) {
|
|
@@ -1951,6 +2053,11 @@ function createSDKBridge(opts) {
|
|
|
1951
2053
|
}
|
|
1952
2054
|
|
|
1953
2055
|
var modelSwitchGeneration = session._queryGeneration;
|
|
2056
|
+
function queryRetired() {
|
|
2057
|
+
if (sessionAdapter.vendor !== "claude") return false;
|
|
2058
|
+
return Number(session._sdkQueryGeneration || 0) !== queryGeneration || session.taskStopRequested
|
|
2059
|
+
|| session.destroying || responseRecovery.stopping(session) || (handle && session.queryInstance !== handle);
|
|
2060
|
+
}
|
|
1954
2061
|
var queryOpts = {
|
|
1955
2062
|
onModelSwitch: function(change) {
|
|
1956
2063
|
if (session._queryGeneration !== modelSwitchGeneration || session.taskStopRequested) return;
|
|
@@ -1992,7 +2099,18 @@ function createSDKBridge(opts) {
|
|
|
1992
2099
|
dynamicTools: sessionToolDefs,
|
|
1993
2100
|
// Codex persists dynamic tools with a thread. A stale authorized Driver
|
|
1994
2101
|
// gets this exact-query bridge; its handlers still reauthorize live.
|
|
1995
|
-
sessionMcpServer: (recoverCodexIssueTools || recoverCodexQuestionTools || recoverCodexBrowserTools || recoverCodexLinearTools || recoverCodexYouTools || recoverCodexExternalTools) ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
|
|
2102
|
+
sessionMcpServer: (recoverCodexMateSchedules || recoverCodexIssueTools || recoverCodexQuestionTools || recoverCodexBrowserTools || recoverCodexLinearTools || recoverCodexYouTools || recoverCodexExternalTools) ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
|
|
2103
|
+
onRecoveryAdmission: function(waiting) {
|
|
2104
|
+
if (!queryContextAvailable()) return;
|
|
2105
|
+
if (waiting) session._codexRecoveryWaiting = queryGeneration;
|
|
2106
|
+
else delete session._codexRecoveryWaiting;
|
|
2107
|
+
},
|
|
2108
|
+
onRecoveryWaiting: function(waiting) {
|
|
2109
|
+
if (!queryContextAvailable()) return;
|
|
2110
|
+
sendAndRecord(session, { type: 'informational', level: 'info', content: waiting
|
|
2111
|
+
? 'Codex recovery is waiting for this conversation’s runtime to become safely available. Your message is retained; Stop cancels the wait.'
|
|
2112
|
+
: 'Codex recovery is ready. Continuing your retained message.' });
|
|
2113
|
+
},
|
|
1996
2114
|
callDynamicTool: function (toolName, args) {
|
|
1997
2115
|
var toolDef = findSessionToolDef(toolName);
|
|
1998
2116
|
if (toolDef && typeof toolDef.handler === "function") {
|
|
@@ -2003,6 +2121,8 @@ function createSDKBridge(opts) {
|
|
|
2003
2121
|
resumeSessionId: session.cliSessionId || undefined,
|
|
2004
2122
|
abortController: linuxUser ? undefined : session.abortController,
|
|
2005
2123
|
canUseTool: function(toolName, input, toolOpts) {
|
|
2124
|
+
if (queryRetired()) return { behavior: "deny", message: "This attempt has stopped." };
|
|
2125
|
+
responseRecovery.progress(session);
|
|
2006
2126
|
var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
|
|
2007
2127
|
if (nativeQuestionError) return { behavior: "deny", message: nativeQuestionError };
|
|
2008
2128
|
var permissionName = toolName;
|
|
@@ -2011,6 +2131,8 @@ function createSDKBridge(opts) {
|
|
|
2011
2131
|
return handleCanUseTool(session, permissionName, input, toolOpts);
|
|
2012
2132
|
},
|
|
2013
2133
|
preToolUsePolicy: function(toolName, input) {
|
|
2134
|
+
if (queryRetired()) return Promise.resolve({ behavior: "deny", message: "This attempt has stopped." });
|
|
2135
|
+
responseRecovery.progress(session);
|
|
2014
2136
|
var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
|
|
2015
2137
|
if (nativeQuestionError) return Promise.resolve({ behavior: "deny", message: nativeQuestionError });
|
|
2016
2138
|
var permissionName = toolName;
|
|
@@ -2024,12 +2146,18 @@ function createSDKBridge(opts) {
|
|
|
2024
2146
|
validateUserInputRequest: scheduleQuestionError,
|
|
2025
2147
|
userInputMode: selectedUserInputMode,
|
|
2026
2148
|
onElicitation: function(request, elicitOpts) {
|
|
2149
|
+
if (queryRetired()) return Promise.resolve({ action: "cancel" });
|
|
2150
|
+
responseRecovery.progress(session);
|
|
2027
2151
|
return handleElicitation(session, request, elicitOpts);
|
|
2028
2152
|
},
|
|
2029
2153
|
onUserDialog: function(dialogRequest, dialogOpts) {
|
|
2154
|
+
if (queryRetired()) return Promise.resolve({ behavior: "cancelled" });
|
|
2155
|
+
responseRecovery.progress(session);
|
|
2030
2156
|
return handleUserDialog(session, dialogRequest, dialogOpts);
|
|
2031
2157
|
},
|
|
2032
2158
|
callMcpTool: function(serverName, toolName, args) {
|
|
2159
|
+
if (queryRetired()) return Promise.reject(new Error("This attempt has stopped."));
|
|
2160
|
+
responseRecovery.progress(session);
|
|
2033
2161
|
return callMcpToolHandler(mergedMcpServers, serverName, toolName, args);
|
|
2034
2162
|
},
|
|
2035
2163
|
adapterOptions: {
|
|
@@ -2061,10 +2189,28 @@ function createSDKBridge(opts) {
|
|
|
2061
2189
|
},
|
|
2062
2190
|
};
|
|
2063
2191
|
|
|
2192
|
+
if (recoveryState) {
|
|
2193
|
+
if (!recoveryState.context) recoveryState.context = { appendSystemPrompt: queryOpts.appendSystemPrompt, model: queryOpts.model, effort: queryOpts.effort };
|
|
2194
|
+
Object.assign(queryOpts, recoveryState.context);
|
|
2195
|
+
}
|
|
2064
2196
|
var handle;
|
|
2197
|
+
var startupHandle = session.queryInstance;
|
|
2065
2198
|
console.log("[sdk-bridge] calling adapter.createQuery... vendor=" + sessionAdapter.vendor);
|
|
2066
2199
|
try {
|
|
2067
2200
|
handle = await sessionAdapter.createQuery(queryOpts);
|
|
2201
|
+
// A guarded delivery must still own the exact startup after provider
|
|
2202
|
+
// creation. Never replace a newer handle or publish to a deleted target.
|
|
2203
|
+
if (beforePush && beforePush.exactProductDelivery) {
|
|
2204
|
+
var startupCurrent = false;
|
|
2205
|
+
try { startupCurrent = sm.sessions.get(session.localId) === session && session.queryInstance === startupHandle && Number(session._sdkQueryGeneration || 0) === queryGeneration && beforePush() === true; } catch (guardError) {}
|
|
2206
|
+
if (!startupCurrent) {
|
|
2207
|
+
try { if (handle.abort) handle.abort(); } catch (abortError) {}
|
|
2208
|
+
try { if (handle.close) handle.close(); } catch (closeError) {}
|
|
2209
|
+
if (session.queryInstance === startupHandle) session.isProcessing = false;
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if (recoveryState) recoveryState.handle = handle;
|
|
2068
2214
|
console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
|
|
2069
2215
|
// SDK accepted the explicit title. Provisional labels remain eligible
|
|
2070
2216
|
// for semantic generation after the first Home exchange (or the normal
|
|
@@ -2084,6 +2230,7 @@ function createSDKBridge(opts) {
|
|
|
2084
2230
|
console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
|
|
2085
2231
|
console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
|
|
2086
2232
|
console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
|
|
2233
|
+
if (beforePush && beforePush.exactProductDelivery && (sm.sessions.get(session.localId) !== session || session.queryInstance !== startupHandle || Number(session._sdkQueryGeneration || 0) !== queryGeneration)) return false;
|
|
2087
2234
|
session.isProcessing = false;
|
|
2088
2235
|
session._awaitingTurnResult = false;
|
|
2089
2236
|
session._queuedTurnCount = 0;
|
|
@@ -2138,6 +2285,7 @@ function createSDKBridge(opts) {
|
|
|
2138
2285
|
}
|
|
2139
2286
|
|
|
2140
2287
|
if (typeof beforePush === "function" && beforePush() !== true) {
|
|
2288
|
+
try { if (typeof handle.abort === "function") handle.abort(); } catch (e) {}
|
|
2141
2289
|
try { handle.close(); } catch (e) {}
|
|
2142
2290
|
if (session.queryInstance === handle) session.queryInstance = null;
|
|
2143
2291
|
session.messageQueue = null;
|
|
@@ -2147,6 +2295,13 @@ function createSDKBridge(opts) {
|
|
|
2147
2295
|
session._queuedTurnCount = 0;
|
|
2148
2296
|
taskLifecycle.clearAll(session);
|
|
2149
2297
|
onProcessingChanged();
|
|
2298
|
+
if (recoveryState && !responseRecovery.stopping(session)) {
|
|
2299
|
+
sendAndRecord(session, { type: "thinking_stop" });
|
|
2300
|
+
sendAndRecord(session, { type: session.taskStopRequested ? "info" : "error", text: session.taskStopRequested
|
|
2301
|
+
? "Stopped before sending your message."
|
|
2302
|
+
: "This request is no longer eligible for delivery. Please review it and send it again." });
|
|
2303
|
+
sendAndRecord(session, { type: "done", code: session.taskStopRequested ? 0 : 1 });
|
|
2304
|
+
}
|
|
2150
2305
|
sm.broadcastSessionList();
|
|
2151
2306
|
return false;
|
|
2152
2307
|
}
|
|
@@ -2235,6 +2390,19 @@ function createSDKBridge(opts) {
|
|
|
2235
2390
|
// dropped messages in both gap windows, which is what made a session show
|
|
2236
2391
|
// the processing indicator without ever answering until a second send.
|
|
2237
2392
|
function pushMessage(session, text, images, deliveryMeta) {
|
|
2393
|
+
if (session._responseRecoveryStopPending && !responseRecovery.stopping(session)) return false;
|
|
2394
|
+
function trackDelivery() {
|
|
2395
|
+
if ((session.vendor || adapter.vendor) === "claude") responseRecovery.enqueue(session, {
|
|
2396
|
+
text: text, images: images, linuxUser: session.lastLinuxUser || null,
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
if (responseRecovery.stopping(session)) {
|
|
2400
|
+
// Keep admission serialized while the old execution is terminating.
|
|
2401
|
+
session.pendingPush = session.pendingPush || [];
|
|
2402
|
+
session.pendingPush.push(Object.assign({ text: text, images: images || null }, deliveryMeta || {}));
|
|
2403
|
+
trackDelivery();
|
|
2404
|
+
return true;
|
|
2405
|
+
}
|
|
2238
2406
|
session.lastActivityAt = Date.now();
|
|
2239
2407
|
if (session._runtimeRefreshRequested && !session._queryStarting) {
|
|
2240
2408
|
session._runtimeRefreshRequested = false;
|
|
@@ -2261,6 +2429,7 @@ function createSDKBridge(opts) {
|
|
|
2261
2429
|
console.error("[sdk-bridge] QueryHandle rejected message for session " + session.localId + ":", e.message || e);
|
|
2262
2430
|
}
|
|
2263
2431
|
if (delivered) {
|
|
2432
|
+
trackDelivery();
|
|
2264
2433
|
delete session._lastTurnInterrupted;
|
|
2265
2434
|
if (session._awaitingTurnResult) {
|
|
2266
2435
|
session._queuedTurnCount = (session._queuedTurnCount || 0) + 1;
|
|
@@ -2285,6 +2454,7 @@ function createSDKBridge(opts) {
|
|
|
2285
2454
|
if (session._queryStarting) {
|
|
2286
2455
|
session.pendingPush = session.pendingPush || [];
|
|
2287
2456
|
session.pendingPush.push(Object.assign({ text: text, images: images || null }, deliveryMeta || {}));
|
|
2457
|
+
trackDelivery();
|
|
2288
2458
|
return true;
|
|
2289
2459
|
}
|
|
2290
2460
|
return false;
|
|
@@ -2495,7 +2665,20 @@ function createSDKBridge(opts) {
|
|
|
2495
2665
|
sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
|
|
2496
2666
|
}
|
|
2497
2667
|
|
|
2498
|
-
async function setModel(session, model) {
|
|
2668
|
+
async function setModel(session, model, options) {
|
|
2669
|
+
if (options && typeof options.revalidate === "function") {
|
|
2670
|
+
var vendor = session.vendor || sm.defaultVendor || "claude";
|
|
2671
|
+
return require("./session-config-transition").applySessionConfig({
|
|
2672
|
+
sm: sm, session: session, revalidate: options.revalidate, method: "setModel", nativeValue: model, changes: { model: model },
|
|
2673
|
+
publish: function () {
|
|
2674
|
+
sm.defaultModelByVendor = sm.defaultModelByVendor || {};
|
|
2675
|
+
sm.defaultModelByVendor[vendor] = model;
|
|
2676
|
+
sm.broadcastSessionList();
|
|
2677
|
+
sendModelInfoForVendor(session, vendor, model);
|
|
2678
|
+
sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: vendor, model: model, effort: session.effort || sm.currentEffort || "medium", mode: session.permissionMode || sm.currentPermissionMode || "default", betas: sm.currentBetas || [] });
|
|
2679
|
+
},
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2499
2682
|
// Normalize to string id in case a { value, displayName } object slips in
|
|
2500
2683
|
if (model && typeof model !== "string") {
|
|
2501
2684
|
model = modelEntryValue(model);
|
|
@@ -2531,7 +2714,22 @@ function createSDKBridge(opts) {
|
|
|
2531
2714
|
}
|
|
2532
2715
|
}
|
|
2533
2716
|
|
|
2534
|
-
async function setEffort(session, effort) {
|
|
2717
|
+
async function setEffort(session, effort, options) {
|
|
2718
|
+
if (options && typeof options.revalidate === "function") {
|
|
2719
|
+
var vendor = session.vendor || sm.defaultVendor || "claude";
|
|
2720
|
+
var value = yoke.clampEffort(vendor, effort);
|
|
2721
|
+
if (!value || value !== effort) throw new Error("Unsupported reasoning effort.");
|
|
2722
|
+
return require("./session-config-transition").applySessionConfig({
|
|
2723
|
+
sm: sm, session: session, revalidate: options.revalidate, method: "setEffort", nativeValue: value, changes: { effort: value },
|
|
2724
|
+
publish: function () {
|
|
2725
|
+
sm.currentEffortByVendor = sm.currentEffortByVendor || {};
|
|
2726
|
+
sm.currentEffortByVendor[vendor] = value;
|
|
2727
|
+
sm.currentEffort = value;
|
|
2728
|
+
sm.broadcastSessionList();
|
|
2729
|
+
sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: vendor, effort: value, model: session.model || "", mode: session.permissionMode || sm.currentPermissionMode || "default", betas: sm.currentBetas || [] });
|
|
2730
|
+
},
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2535
2733
|
// Remember the pick per vendor (clamped) so vendor switches don't leak
|
|
2536
2734
|
// unsupported levels across sessions; sm.currentEffort stays as the
|
|
2537
2735
|
// display value for the session the user just configured.
|
|
@@ -2800,6 +2998,7 @@ function createSDKBridge(opts) {
|
|
|
2800
2998
|
startQuery: startQuery,
|
|
2801
2999
|
startQueryWithAcceptance: startQueryWithAcceptance,
|
|
2802
3000
|
pushMessage: pushMessage,
|
|
3001
|
+
cancelResponseRecovery: responseRecovery.cancel,
|
|
2803
3002
|
refreshSessionRuntime: refreshSessionRuntime,
|
|
2804
3003
|
refreshEnvironmentRuntime: refreshEnvironmentRuntime,
|
|
2805
3004
|
setModel: setModel,
|
|
@@ -430,6 +430,7 @@ function attachMessageProcessor(ctx) {
|
|
|
430
430
|
if (pushModule && block.name === "AskUserQuestion" && input.questions) {
|
|
431
431
|
var q = input.questions[0];
|
|
432
432
|
pushModule.sendPush({
|
|
433
|
+
sessionId: session.localId,
|
|
433
434
|
type: "ask_user",
|
|
434
435
|
slug: slug,
|
|
435
436
|
title: (mateDisplayName || "Claude") + " has a question",
|
|
@@ -715,6 +716,7 @@ function attachMessageProcessor(ctx) {
|
|
|
715
716
|
|
|
716
717
|
if (pushModule && !session._delegatedBy && !_suppressResponseNotification) {
|
|
717
718
|
pushModule.sendPush({
|
|
719
|
+
sessionId: session.localId,
|
|
718
720
|
type: "done",
|
|
719
721
|
slug: slug,
|
|
720
722
|
title: _doneTitle,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Claude delivery watchdog. Only an unanswered foreground turn is replayable.
|
|
2
|
+
function attachResponseRecovery(ctx) {
|
|
3
|
+
var states = new Map();
|
|
4
|
+
var schedule = ctx.setTimeout || setTimeout;
|
|
5
|
+
var unschedule = ctx.clearTimeout || clearTimeout;
|
|
6
|
+
var waitMs = ctx.waitMs || 180000;
|
|
7
|
+
var stopMs = ctx.stopMs || 10000;
|
|
8
|
+
|
|
9
|
+
function live(session, state) {
|
|
10
|
+
return states.get(session) === state && !session.destroying && !session.taskStopRequested
|
|
11
|
+
&& state.ownerId === session.ownerId && state.vendor === session.vendor
|
|
12
|
+
&& (!ctx.sessions || ctx.sessions.get(session.localId) === session);
|
|
13
|
+
}
|
|
14
|
+
function clearTimer(state) {
|
|
15
|
+
if (state.timer) unschedule(state.timer);
|
|
16
|
+
state.timer = null;
|
|
17
|
+
}
|
|
18
|
+
function cancel(session, expected) {
|
|
19
|
+
var state = states.get(session);
|
|
20
|
+
if (expected && state !== expected) return;
|
|
21
|
+
if (state) clearTimer(state);
|
|
22
|
+
states.delete(session);
|
|
23
|
+
}
|
|
24
|
+
function protectedWait(session) {
|
|
25
|
+
return [session.pendingPermissions, session.pendingAskUser, session.pendingElicitations,
|
|
26
|
+
session.pendingUserDialogs, session.activeTaskToolIds].some(function(map) {
|
|
27
|
+
return map && Object.keys(map).length > 0;
|
|
28
|
+
}) || (session.activeBackgroundTasks && session.activeBackgroundTasks.length > 0);
|
|
29
|
+
}
|
|
30
|
+
function arm(session, state) {
|
|
31
|
+
clearTimer(state);
|
|
32
|
+
if (!state.turns.length || state.turns[0].progress || state.stopping) return;
|
|
33
|
+
state.timer = schedule(function() { expire(session, state); }, waitMs);
|
|
34
|
+
if (state.timer && state.timer.unref) state.timer.unref();
|
|
35
|
+
}
|
|
36
|
+
function snapshot(payload) {
|
|
37
|
+
return Object.assign({}, payload, { images: payload.images ? JSON.parse(JSON.stringify(payload.images)) : payload.images });
|
|
38
|
+
}
|
|
39
|
+
function begin(session, payload, retryState) {
|
|
40
|
+
if (retryState) return retryState;
|
|
41
|
+
cancel(session);
|
|
42
|
+
var state = { turns: [snapshot(payload)], timer: null, stopping: false, retried: false, ownerId: session.ownerId, vendor: session.vendor };
|
|
43
|
+
states.set(session, state);
|
|
44
|
+
arm(session, state);
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
function enqueue(session, payload) {
|
|
48
|
+
var state = states.get(session);
|
|
49
|
+
if (!state) return begin(session, payload);
|
|
50
|
+
state.turns.push(snapshot(payload));
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
function progress(session) {
|
|
54
|
+
var state = states.get(session);
|
|
55
|
+
if (!state || state.stopping || !state.turns.length) return;
|
|
56
|
+
state.turns[0].progress = true;
|
|
57
|
+
clearTimer(state);
|
|
58
|
+
}
|
|
59
|
+
function event(session, msg) {
|
|
60
|
+
var state = states.get(session);
|
|
61
|
+
if (!state || state.stopping || !msg) return;
|
|
62
|
+
var type = msg.yokeType;
|
|
63
|
+
if (type === 'error' || type === 'interrupted') { cancel(session); return; }
|
|
64
|
+
if (type === 'result') {
|
|
65
|
+
if (msg.subtype === 'success' && !msg.is_error && msg.numTurns === 0
|
|
66
|
+
&& msg.origin && msg.origin.kind === 'task-notification'
|
|
67
|
+
&& !msg.localCommand && msg.resultText === '') return;
|
|
68
|
+
var remaining = require('./sdk-turn-queue').pendingSends(session, msg);
|
|
69
|
+
state.turns = remaining ? state.turns.slice(-remaining) : [];
|
|
70
|
+
state.retried = false;
|
|
71
|
+
arm(session, state);
|
|
72
|
+
if (!state.turns.length) cancel(session);
|
|
73
|
+
} else if ((type === 'text_delta' || type === 'thinking_delta') && (msg.text || msg.delta)
|
|
74
|
+
|| type === 'tool_start' || type === 'tool_input_delta' || type === 'tool_progress'
|
|
75
|
+
|| type === 'task_started' || type === 'task_progress' || type === 'subagent_message'
|
|
76
|
+
|| type === 'message' && msg.messageRole === 'assistant' && msg.content && msg.content.length) {
|
|
77
|
+
progress(session);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function finish(session, state, text) {
|
|
81
|
+
if (!live(session, state)) { cancel(session, state); return; }
|
|
82
|
+
cancel(session);
|
|
83
|
+
// Fence late output when a transport cannot confirm termination.
|
|
84
|
+
session._sdkQueryGeneration = Number(session._sdkQueryGeneration || 0) + 1;
|
|
85
|
+
session.isProcessing = false;
|
|
86
|
+
session._awaitingTurnResult = false;
|
|
87
|
+
session._queuedTurnCount = 0;
|
|
88
|
+
ctx.changed();
|
|
89
|
+
ctx.record(session, { type: 'thinking_stop' });
|
|
90
|
+
ctx.record(session, { type: 'error', text: text });
|
|
91
|
+
ctx.record(session, { type: 'done', code: 1 });
|
|
92
|
+
ctx.broadcast();
|
|
93
|
+
}
|
|
94
|
+
async function expire(session, state) {
|
|
95
|
+
clearTimer(state);
|
|
96
|
+
if (!live(session, state)) { cancel(session, state); return; }
|
|
97
|
+
// Never stop a known tool or a human decision because prose is absent.
|
|
98
|
+
if (protectedWait(session)) { progress(session); return; }
|
|
99
|
+
state.stopping = true;
|
|
100
|
+
ctx.record(session, { type: 'info', text: 'Claude has not responded for three minutes. Stopping this attempt safely before retrying.' });
|
|
101
|
+
var timeout;
|
|
102
|
+
var stopped = false;
|
|
103
|
+
try {
|
|
104
|
+
var stopAttempt = (async function() {
|
|
105
|
+
var handle = session.queryInstance || state.handle;
|
|
106
|
+
if (!handle && session.abortController) session.abortController.abort();
|
|
107
|
+
if (!handle && state.starting) await state.starting;
|
|
108
|
+
handle = handle || session.queryInstance || state.handle;
|
|
109
|
+
if (handle) {
|
|
110
|
+
if (typeof handle.stopAndWait !== 'function') return false;
|
|
111
|
+
await handle.stopAndWait();
|
|
112
|
+
if (session.streamPromise) await session.streamPromise;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
})();
|
|
116
|
+
session._responseRecoveryStopPending = stopAttempt;
|
|
117
|
+
stopAttempt.then(function(confirmed) {
|
|
118
|
+
if (confirmed && session._responseRecoveryStopPending === stopAttempt) session._responseRecoveryStopPending = null;
|
|
119
|
+
}, function() {});
|
|
120
|
+
stopped = await Promise.race([
|
|
121
|
+
stopAttempt,
|
|
122
|
+
new Promise(function(resolve) { timeout = schedule(function() { resolve(false); }, stopMs); }),
|
|
123
|
+
]);
|
|
124
|
+
} catch (e) { stopped = false; }
|
|
125
|
+
if (timeout) unschedule(timeout);
|
|
126
|
+
if (!live(session, state)) { cancel(session, state); return; }
|
|
127
|
+
if (!stopped) {
|
|
128
|
+
finish(session, state, 'Claude could not be confirmed stopped. Automatic retry was cancelled to avoid running your request twice. Check your Claude connection before trying again.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (state.retried || state.turns.length !== 1) {
|
|
132
|
+
finish(session, state, state.retried
|
|
133
|
+
? 'Claude still has not responded after one automatic retry. Please send your message again or check your Claude connection.'
|
|
134
|
+
: 'Claude did not respond. Several messages were queued, so automatic replay was cancelled to avoid duplicating work. Please send the unanswered messages again.');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
state.retried = true;
|
|
138
|
+
state.stopping = false;
|
|
139
|
+
session.isProcessing = true;
|
|
140
|
+
ctx.changed();
|
|
141
|
+
ctx.record(session, { type: 'info', text: 'The previous attempt has stopped. Retrying your original message once.' });
|
|
142
|
+
ctx.send(session, { type: 'status', status: 'processing' });
|
|
143
|
+
arm(session, state);
|
|
144
|
+
var payload = state.turns[0];
|
|
145
|
+
await ctx.restart(session, payload, state);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
begin: begin, enqueue: enqueue, event: event, progress: progress, cancel: cancel,
|
|
149
|
+
current: function(session) { return states.get(session); },
|
|
150
|
+
valid: live,
|
|
151
|
+
stopping: function(session) { var state = states.get(session); return !!(state && state.stopping); },
|
|
152
|
+
dispose: function() { states.forEach(function(state) { clearTimer(state); }); states.clear(); },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
module.exports = { attachResponseRecovery: attachResponseRecovery };
|