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.
Files changed (171) hide show
  1. package/lib/account-auth-socket.js +1 -0
  2. package/lib/builtin-mates.js +17 -14
  3. package/lib/canonical-clay-record-authority.js +15 -0
  4. package/lib/canonical-clay-scheduling-authority.js +19 -0
  5. package/lib/claude-hook-installer.js +28 -0
  6. package/lib/clay-mate-chat-receipts.js +26 -0
  7. package/lib/clay-mate-chat.js +87 -0
  8. package/lib/clay-mate-knowledge-input.js +26 -0
  9. package/lib/clay-mate-knowledge.js +95 -0
  10. package/lib/clay-mate-lifecycle.js +95 -0
  11. package/lib/clay-product-control-input.js +33 -0
  12. package/lib/clay-product-control-tools.js +51 -0
  13. package/lib/clay-product-control.js +237 -0
  14. package/lib/clay-project-lifecycle.js +154 -0
  15. package/lib/clay-project-record-input.js +44 -0
  16. package/lib/clay-project-records.js +83 -0
  17. package/lib/clay-scheduling-input.js +21 -0
  18. package/lib/clay-scheduling-receipts.js +26 -0
  19. package/lib/clay-scheduling.js +119 -0
  20. package/lib/clay-screen-controls.js +96 -0
  21. package/lib/clay-workspace-controls.js +83 -0
  22. package/lib/clay-workspace-input.js +17 -0
  23. package/lib/clay-workspace-receipts.js +26 -0
  24. package/lib/daemon-project-access.js +11 -0
  25. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  26. package/lib/daemon-project-lifecycle.js +262 -0
  27. package/lib/daemon-projects.js +1 -1
  28. package/lib/daemon.js +73 -221
  29. package/lib/issues-service.js +29 -15
  30. package/lib/knowledge-db-import.js +5 -4
  31. package/lib/knowledge-db-service.js +9 -6
  32. package/lib/knowledge-document-index.js +2 -1
  33. package/lib/knowledge-product-operations.js +46 -0
  34. package/lib/mate-creation-mcp-server.js +2 -0
  35. package/lib/mate-knowledge-canonical-access.js +10 -0
  36. package/lib/mate-knowledge-service.js +7 -4
  37. package/lib/mate-lifecycle-service.js +94 -0
  38. package/lib/mate-message-delivery.js +43 -0
  39. package/lib/mate-ready-creation.js +9 -1
  40. package/lib/mate-schedule-input.js +29 -0
  41. package/lib/mate-schedule-lifecycle.js +151 -0
  42. package/lib/mate-scheduling.js +103 -0
  43. package/lib/mates.js +4 -3
  44. package/lib/notes-checklist.js +50 -0
  45. package/lib/notes.js +49 -3
  46. package/lib/os-users.js +20 -5
  47. package/lib/project-admin-overview.js +51 -0
  48. package/lib/project-ask-user.js +4 -2
  49. package/lib/project-delegated-follow-up.js +2 -1
  50. package/lib/project-directory-claim.js +65 -0
  51. package/lib/project-file-operations.js +61 -0
  52. package/lib/project-filesystem.js +13 -87
  53. package/lib/project-full-access.js +23 -2
  54. package/lib/project-knowledge.js +32 -18
  55. package/lib/project-lifecycle-operations.js +98 -0
  56. package/lib/project-lifecycle-process-witness.js +32 -0
  57. package/lib/project-lifecycle-runtime.js +50 -0
  58. package/lib/project-linux-access.js +22 -0
  59. package/lib/project-logs-canonical-clay.js +27 -0
  60. package/lib/project-logs-clay-query.js +16 -0
  61. package/lib/project-logs-schema.js +4 -2
  62. package/lib/project-logs-service.js +8 -7
  63. package/lib/project-logs.js +1 -0
  64. package/lib/project-loop.js +1 -0
  65. package/lib/project-mate-chat-control.js +55 -0
  66. package/lib/project-mate-creation-proposal.js +18 -3
  67. package/lib/project-models.js +14 -31
  68. package/lib/project-notifications.js +74 -16
  69. package/lib/project-ownership.js +77 -0
  70. package/lib/project-pair-result-delivery.js +14 -3
  71. package/lib/project-pending-message-queue.js +1 -0
  72. package/lib/project-product-control.js +87 -0
  73. package/lib/project-product-session-queue.js +46 -0
  74. package/lib/project-push-access.js +28 -0
  75. package/lib/project-scheduled-message-queue.js +30 -0
  76. package/lib/project-scheduled-messages.js +63 -64
  77. package/lib/project-scheduled-task-control.js +83 -0
  78. package/lib/project-scheduled-tasks.js +13 -2
  79. package/lib/project-scheduling-access.js +10 -0
  80. package/lib/project-session-actions.js +65 -0
  81. package/lib/project-session-delete.js +8 -1
  82. package/lib/project-session-github.js +47 -12
  83. package/lib/project-session-history-actions.js +165 -0
  84. package/lib/project-session-runtime-actions.js +63 -0
  85. package/lib/project-session-vendor.js +54 -0
  86. package/lib/project-sessions.js +64 -241
  87. package/lib/project-user-message.js +30 -1
  88. package/lib/project-workspace-operations.js +126 -0
  89. package/lib/project-workspace-query.js +9 -2
  90. package/lib/project.js +95 -3
  91. package/lib/public/css/clay-tool-activity.css +51 -0
  92. package/lib/public/css/home-mate-creation.css +5 -0
  93. package/lib/public/css/overlays.css +4 -2
  94. package/lib/public/css/project-admin-overview.css +13 -0
  95. package/lib/public/css/session-folders.css +11 -0
  96. package/lib/public/css/session-github.css +3 -0
  97. package/lib/public/css/sticky-notes.css +61 -1
  98. package/lib/public/index.html +1 -1
  99. package/lib/public/modules/admin-settings-projects.js +6 -2
  100. package/lib/public/modules/app-connection.js +8 -1
  101. package/lib/public/modules/app-message-router.js +1 -1
  102. package/lib/public/modules/app-messages.js +18 -3
  103. package/lib/public/modules/app-projects.js +5 -0
  104. package/lib/public/modules/clay-owner-navigation.js +27 -0
  105. package/lib/public/modules/clay-tool-activity.js +140 -0
  106. package/lib/public/modules/clay-tool-presentation.js +338 -0
  107. package/lib/public/modules/command-palette.js +1 -1
  108. package/lib/public/modules/default-ai.js +11 -3
  109. package/lib/public/modules/home-debate-planning.js +5 -5
  110. package/lib/public/modules/home-mate-chat.js +8 -6
  111. package/lib/public/modules/home-mate-creation.js +73 -1
  112. package/lib/public/modules/personal-settings-dialog.js +7 -1
  113. package/lib/public/modules/project-access-recovery.js +17 -0
  114. package/lib/public/modules/project-admin-overview.js +21 -0
  115. package/lib/public/modules/session-folder-layout.js +1 -1
  116. package/lib/public/modules/session-folder-view-menu.js +9 -0
  117. package/lib/public/modules/session-github.js +65 -8
  118. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  119. package/lib/public/modules/sticky-note-markdown.js +72 -6
  120. package/lib/public/modules/sticky-notes-actions.js +30 -0
  121. package/lib/public/modules/sticky-notes-browser.js +9 -2
  122. package/lib/public/modules/sticky-notes-card.js +23 -7
  123. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  124. package/lib/public/modules/sticky-notes-editor.js +108 -44
  125. package/lib/public/modules/sticky-notes-shared.js +50 -2
  126. package/lib/public/modules/sticky-notes-state.js +66 -0
  127. package/lib/public/modules/sticky-notes.js +34 -51
  128. package/lib/public/modules/thinking-lifecycle.js +33 -3
  129. package/lib/public/modules/thinking-view.js +23 -1
  130. package/lib/public/modules/tools.js +34 -6
  131. package/lib/public/modules/user-settings-appearance.js +10 -3
  132. package/lib/public/modules/user-settings-editors.js +1 -1
  133. package/lib/public/modules/user-settings-layout.js +11 -2
  134. package/lib/public/modules/user-settings-preferences.js +6 -2
  135. package/lib/public/modules/user-settings-profile.js +12 -2
  136. package/lib/public/modules/user-settings.js +3 -2
  137. package/lib/public/style.css +2 -0
  138. package/lib/scheduled-task-records.js +3 -1
  139. package/lib/scheduler.js +2 -0
  140. package/lib/sdk-bridge.js +220 -21
  141. package/lib/sdk-message-processor.js +2 -0
  142. package/lib/sdk-response-recovery.js +155 -0
  143. package/lib/server-admin.js +63 -16
  144. package/lib/server-dm.js +1 -1
  145. package/lib/server-global-ws.js +10 -20
  146. package/lib/server-home-chat-events.js +7 -3
  147. package/lib/server-home-chat.js +4 -16
  148. package/lib/server-home-mate-creation.js +1 -0
  149. package/lib/server-home-mcp-consent.js +3 -3
  150. package/lib/server-mates.js +25 -67
  151. package/lib/server-owner-ui.js +77 -0
  152. package/lib/server-path-access.js +38 -0
  153. package/lib/server-project-removal.js +22 -0
  154. package/lib/server.js +99 -29
  155. package/lib/session-config-transition.js +45 -0
  156. package/lib/session-folders.js +7 -2
  157. package/lib/session-github.js +9 -2
  158. package/lib/session-visibility.js +5 -2
  159. package/lib/sessions.js +10 -0
  160. package/lib/terminal-manager.js +52 -0
  161. package/lib/users-permissions.js +2 -8
  162. package/lib/users.js +4 -0
  163. package/lib/workspace-query-access.js +4 -1
  164. package/lib/ws-schema.js +8 -2
  165. package/lib/yoke/adapters/claude.js +47 -8
  166. package/lib/yoke/adapters/codex.js +52 -111
  167. package/lib/yoke/claude-worker-startup.js +28 -0
  168. package/lib/yoke/codex-idle-recovery.js +81 -1
  169. package/lib/yoke/codex-recovery-admission.js +164 -0
  170. package/lib/yoke/session-tool-transport.js +3 -0
  171. 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
- return await startQueryInner(session, text, images, linuxUser, beforePush, onAccepted);
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
- if (!session.queryInstance && session.pendingPush && session.pendingPush.length) {
1454
- // The query never came up (auth/adapter failure already reported to
1455
- // the user). Drop the backlog rather than leaking it into a future
1456
- // unrelated query.
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 === "begin_scheduled_task_interview") {
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 };