clay-server 5.2.0-beta.4 → 5.2.0-beta.6

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 (301) 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 +1 -239
  6. package/lib/claude-jsonl-watcher.js +1 -204
  7. package/lib/clay-mate-chat-receipts.js +26 -0
  8. package/lib/clay-mate-chat.js +87 -0
  9. package/lib/clay-mate-knowledge-input.js +26 -0
  10. package/lib/clay-mate-knowledge.js +95 -0
  11. package/lib/clay-mate-lifecycle.js +95 -0
  12. package/lib/clay-product-control-input.js +33 -0
  13. package/lib/clay-product-control-tools.js +51 -0
  14. package/lib/clay-product-control.js +237 -0
  15. package/lib/clay-project-lifecycle.js +154 -0
  16. package/lib/clay-project-record-input.js +44 -0
  17. package/lib/clay-project-records.js +83 -0
  18. package/lib/clay-scheduling-input.js +21 -0
  19. package/lib/clay-scheduling-receipts.js +26 -0
  20. package/lib/clay-scheduling.js +119 -0
  21. package/lib/clay-screen-controls.js +96 -0
  22. package/lib/clay-skill-install.js +1 -33
  23. package/lib/clay-workspace-controls.js +83 -0
  24. package/lib/clay-workspace-input.js +17 -0
  25. package/lib/clay-workspace-receipts.js +26 -0
  26. package/lib/cli-sessions.js +1 -300
  27. package/lib/codex-defaults.js +1 -28
  28. package/lib/context-usage.js +3 -2
  29. package/lib/daemon-project-access.js +11 -0
  30. package/lib/daemon-project-lifecycle-callbacks.js +15 -0
  31. package/lib/daemon-project-lifecycle.js +262 -0
  32. package/lib/daemon-projects.js +1 -1
  33. package/lib/daemon.js +78 -276
  34. package/lib/default-ai-runtime.js +4 -8
  35. package/lib/email-mcp-server.js +1 -1
  36. package/lib/issues-service.js +29 -15
  37. package/lib/kiro-defaults.js +1 -22
  38. package/lib/knowledge-db-import.js +5 -4
  39. package/lib/knowledge-db-service.js +9 -6
  40. package/lib/knowledge-document-index.js +2 -1
  41. package/lib/knowledge-product-operations.js +46 -0
  42. package/lib/mate-creation-mcp-server.js +2 -0
  43. package/lib/mate-knowledge-canonical-access.js +10 -0
  44. package/lib/mate-knowledge-service.js +7 -4
  45. package/lib/mate-lifecycle-service.js +94 -0
  46. package/lib/mate-message-delivery.js +43 -0
  47. package/lib/mate-ready-creation.js +9 -1
  48. package/lib/mate-schedule-input.js +29 -0
  49. package/lib/mate-schedule-lifecycle.js +151 -0
  50. package/lib/mate-scheduling.js +103 -0
  51. package/lib/mates.js +4 -3
  52. package/lib/mcp-proxy.js +2 -32
  53. package/lib/mcp-query-bridge.js +1 -1
  54. package/lib/mcp-source-adapters.js +1 -91
  55. package/lib/mcp-source-context.js +1 -42
  56. package/lib/mcp-source-discovery.js +1 -169
  57. package/lib/mcp-source-plugins.js +1 -68
  58. package/lib/mcp-source-restrictions.js +1 -47
  59. package/lib/mcp-tool-policy.js +1 -26
  60. package/lib/notes-checklist.js +50 -0
  61. package/lib/notes.js +49 -3
  62. package/lib/os-user-diagnostics.js +2 -4
  63. package/lib/os-users.js +24 -70
  64. package/lib/project-admin-overview.js +51 -0
  65. package/lib/project-ask-user.js +4 -2
  66. package/lib/project-connection.js +8 -7
  67. package/lib/project-delegated-follow-up.js +2 -1
  68. package/lib/project-directory-claim.js +65 -0
  69. package/lib/project-file-operations.js +61 -0
  70. package/lib/project-filesystem.js +13 -87
  71. package/lib/project-full-access.js +23 -2
  72. package/lib/project-http.js +4 -4
  73. package/lib/project-knowledge.js +32 -18
  74. package/lib/project-lifecycle-operations.js +98 -0
  75. package/lib/project-lifecycle-process-witness.js +32 -0
  76. package/lib/project-lifecycle-runtime.js +50 -0
  77. package/lib/project-linux-access.js +22 -0
  78. package/lib/project-logs-canonical-clay.js +27 -0
  79. package/lib/project-logs-clay-query.js +16 -0
  80. package/lib/project-logs-schema.js +4 -2
  81. package/lib/project-logs-service.js +8 -7
  82. package/lib/project-logs.js +1 -0
  83. package/lib/project-loop.js +1 -0
  84. package/lib/project-mate-chat-control.js +55 -0
  85. package/lib/project-mate-creation-proposal.js +18 -3
  86. package/lib/project-mcp.js +2 -2
  87. package/lib/project-models.js +14 -31
  88. package/lib/project-notifications.js +74 -16
  89. package/lib/project-ownership.js +77 -0
  90. package/lib/project-pair-result-delivery.js +14 -3
  91. package/lib/project-pair-usage.js +12 -13
  92. package/lib/project-pending-message-queue.js +4 -1
  93. package/lib/project-product-control.js +87 -0
  94. package/lib/project-product-session-queue.js +46 -0
  95. package/lib/project-push-access.js +28 -0
  96. package/lib/project-scheduled-message-queue.js +30 -0
  97. package/lib/project-scheduled-messages.js +63 -64
  98. package/lib/project-scheduled-task-control.js +83 -0
  99. package/lib/project-scheduled-tasks.js +13 -2
  100. package/lib/project-scheduling-access.js +10 -0
  101. package/lib/project-session-actions.js +65 -0
  102. package/lib/project-session-delete.js +8 -1
  103. package/lib/project-session-github.js +47 -12
  104. package/lib/project-session-history-actions.js +167 -0
  105. package/lib/project-session-pair.js +1 -1
  106. package/lib/project-session-runtime-actions.js +64 -0
  107. package/lib/project-session-spawn.js +2 -3
  108. package/lib/project-session-vendor.js +55 -0
  109. package/lib/project-sessions.js +82 -267
  110. package/lib/project-user-message.js +31 -1
  111. package/lib/project-vendor-login.js +6 -36
  112. package/lib/project-workspace-operations.js +126 -0
  113. package/lib/project-workspace-query.js +9 -2
  114. package/lib/project.js +95 -3
  115. package/lib/public/app.js +1 -0
  116. package/lib/public/css/clay-tool-activity.css +51 -0
  117. package/lib/public/css/home-mate-creation.css +5 -0
  118. package/lib/public/css/messages.css +6 -2
  119. package/lib/public/css/overlays.css +4 -2
  120. package/lib/public/css/project-admin-overview.css +13 -0
  121. package/lib/public/css/session-folders.css +11 -0
  122. package/lib/public/css/session-github.css +3 -0
  123. package/lib/public/css/sticky-notes.css +61 -1
  124. package/lib/public/index.html +1 -1
  125. package/lib/public/modules/admin-settings-projects.js +6 -2
  126. package/lib/public/modules/app-connection.js +8 -1
  127. package/lib/public/modules/app-favicon.js +11 -2
  128. package/lib/public/modules/app-header.js +4 -2
  129. package/lib/public/modules/app-loop-ui.js +2 -0
  130. package/lib/public/modules/app-message-router.js +1 -1
  131. package/lib/public/modules/app-messages.js +54 -8
  132. package/lib/public/modules/app-notifications.js +1 -2
  133. package/lib/public/modules/app-panels.js +61 -123
  134. package/lib/public/modules/app-projects.js +5 -0
  135. package/lib/public/modules/app-rate-limit.js +7 -27
  136. package/lib/public/modules/chat-render-runtime.js +3 -0
  137. package/lib/public/modules/clay-owner-navigation.js +27 -0
  138. package/lib/public/modules/clay-tool-activity.js +140 -0
  139. package/lib/public/modules/clay-tool-presentation.js +338 -0
  140. package/lib/public/modules/command-palette.js +1 -1
  141. package/lib/public/modules/default-ai.js +11 -3
  142. package/lib/public/modules/home-debate-planning.js +5 -5
  143. package/lib/public/modules/home-mate-chat.js +8 -6
  144. package/lib/public/modules/home-mate-creation.js +73 -1
  145. package/lib/public/modules/mcp-connection-view.js +1 -1
  146. package/lib/public/modules/permission-control.js +5 -4
  147. package/lib/public/modules/personal-settings-dialog.js +7 -1
  148. package/lib/public/modules/project-access-recovery.js +17 -0
  149. package/lib/public/modules/project-admin-overview.js +21 -0
  150. package/lib/public/modules/scheduler.js +2 -0
  151. package/lib/public/modules/session-create-form.js +10 -10
  152. package/lib/public/modules/session-folder-layout.js +1 -1
  153. package/lib/public/modules/session-folder-view-menu.js +9 -0
  154. package/lib/public/modules/session-github.js +65 -8
  155. package/lib/public/modules/settings-defaults.js +27 -28
  156. package/lib/public/modules/sidebar-session-folders-ui.js +1 -1
  157. package/lib/public/modules/sticky-note-markdown.js +72 -6
  158. package/lib/public/modules/sticky-notes-actions.js +30 -0
  159. package/lib/public/modules/sticky-notes-browser.js +9 -2
  160. package/lib/public/modules/sticky-notes-card.js +23 -7
  161. package/lib/public/modules/sticky-notes-checklist.js +71 -0
  162. package/lib/public/modules/sticky-notes-editor.js +108 -44
  163. package/lib/public/modules/sticky-notes-shared.js +50 -2
  164. package/lib/public/modules/sticky-notes-state.js +66 -0
  165. package/lib/public/modules/sticky-notes.js +34 -51
  166. package/lib/public/modules/thinking-lifecycle.js +33 -3
  167. package/lib/public/modules/thinking-view.js +23 -1
  168. package/lib/public/modules/tools.js +36 -7
  169. package/lib/public/modules/tui-grab.js +1 -2
  170. package/lib/public/modules/user-settings-appearance.js +10 -3
  171. package/lib/public/modules/user-settings-editors.js +1 -1
  172. package/lib/public/modules/user-settings-layout.js +11 -2
  173. package/lib/public/modules/user-settings-preferences.js +6 -2
  174. package/lib/public/modules/user-settings-profile.js +12 -2
  175. package/lib/public/modules/user-settings.js +3 -2
  176. package/lib/public/modules/vendor-login-browser.js +5 -3
  177. package/lib/public/modules/vendor-login.js +2 -2
  178. package/lib/public/modules/vendor-priority.js +15 -1
  179. package/lib/public/muse-avatar.svg +4 -0
  180. package/lib/public/style.css +2 -0
  181. package/lib/safe-bash-commands.js +3 -45
  182. package/lib/scheduled-task-records.js +3 -1
  183. package/lib/scheduler.js +2 -0
  184. package/lib/sdk-bridge.js +334 -403
  185. package/lib/sdk-foreground-admission.js +16 -0
  186. package/lib/sdk-mcp-descriptors.js +2 -37
  187. package/lib/sdk-message-processor.js +52 -17
  188. package/lib/sdk-quota-snapshot.js +1 -1
  189. package/lib/sdk-response-recovery.js +1 -0
  190. package/lib/sdk-worker.js +2 -677
  191. package/lib/server-admin.js +63 -16
  192. package/lib/server-dm.js +1 -1
  193. package/lib/server-global-ws.js +10 -20
  194. package/lib/server-home-chat-events.js +7 -3
  195. package/lib/server-home-chat.js +4 -16
  196. package/lib/server-home-mate-creation.js +1 -0
  197. package/lib/server-home-mcp-consent.js +3 -3
  198. package/lib/server-mates.js +25 -67
  199. package/lib/server-owner-ui.js +77 -0
  200. package/lib/server-path-access.js +38 -0
  201. package/lib/server-project-removal.js +22 -0
  202. package/lib/server.js +100 -30
  203. package/lib/session-config-transition.js +45 -0
  204. package/lib/session-folders.js +7 -2
  205. package/lib/session-fork-history.js +21 -0
  206. package/lib/session-github.js +9 -2
  207. package/lib/session-hygiene.js +3 -35
  208. package/lib/session-permission-mode.js +4 -3
  209. package/lib/session-skill-scope.js +11 -0
  210. package/lib/session-visibility.js +5 -2
  211. package/lib/sessions.js +27 -89
  212. package/lib/terminal-manager.js +52 -0
  213. package/lib/tool-llm.js +1 -1
  214. package/lib/tui-transcript-index.js +1 -132
  215. package/lib/users-permissions.js +2 -8
  216. package/lib/users.js +4 -0
  217. package/lib/vendor-login-browser.js +1 -11
  218. package/lib/workspace-query-access.js +4 -1
  219. package/lib/ws-schema.js +8 -2
  220. package/lib/yoke/acp-agent-profiles.js +5 -3
  221. package/lib/yoke/adapters/antigravity.js +3 -14
  222. package/lib/yoke/adapters/claude-query-contract.js +9 -0
  223. package/lib/yoke/adapters/claude.js +54 -50
  224. package/lib/yoke/adapters/codex-query-contract.js +24 -0
  225. package/lib/yoke/adapters/codex.js +53 -111
  226. package/lib/yoke/adapters/kiro.js +3 -1
  227. package/lib/yoke/adapters/muse-query-contract.js +7 -0
  228. package/lib/yoke/adapters/muse.js +122 -0
  229. package/lib/yoke/auth-events.js +17 -0
  230. package/lib/yoke/claude-hook-installer.js +267 -0
  231. package/lib/yoke/claude-jsonl-watcher.js +204 -0
  232. package/lib/yoke/claude-maintenance.js +55 -0
  233. package/lib/yoke/claude-permission-patterns.js +53 -0
  234. package/lib/yoke/claude-response-recovery.js +155 -0
  235. package/lib/yoke/claude-transcript-hygiene.js +34 -0
  236. package/lib/yoke/claude-worker-startup.js +28 -0
  237. package/lib/yoke/clay-skill-install.js +33 -0
  238. package/lib/yoke/cli-sessions.js +300 -0
  239. package/lib/yoke/codex-app-server.js +7 -12
  240. package/lib/yoke/codex-idle-recovery.js +81 -1
  241. package/lib/yoke/codex-recovery-admission.js +164 -0
  242. package/lib/yoke/configuration.js +13 -0
  243. package/lib/yoke/discovery.js +70 -0
  244. package/lib/yoke/errors.js +8 -0
  245. package/lib/yoke/host-environment.js +7 -0
  246. package/lib/yoke/index.js +9 -51
  247. package/lib/yoke/interface.js +116 -0
  248. package/lib/yoke/kiro-acp-server.js +5 -5
  249. package/lib/yoke/legacy-sessions.js +89 -0
  250. package/lib/yoke/login-protocol.js +25 -0
  251. package/lib/yoke/maintenance.js +12 -0
  252. package/lib/yoke/mcp-proxy.js +32 -0
  253. package/lib/yoke/mcp-source-adapters.js +91 -0
  254. package/lib/yoke/mcp-source-context.js +42 -0
  255. package/lib/yoke/mcp-source-discovery.js +169 -0
  256. package/lib/yoke/mcp-source-plugins.js +68 -0
  257. package/lib/yoke/mcp-source-restrictions.js +47 -0
  258. package/lib/yoke/mcp-tool-policy-normalize.js +28 -0
  259. package/lib/yoke/model-preferences.js +6 -0
  260. package/lib/yoke/muse-artifacts.js +36 -0
  261. package/lib/yoke/muse-controls.js +50 -0
  262. package/lib/yoke/muse-input.js +64 -0
  263. package/lib/yoke/muse-items.js +157 -0
  264. package/lib/yoke/muse-policy.js +52 -0
  265. package/lib/yoke/muse-query.js +428 -0
  266. package/lib/yoke/muse-runtime.js +211 -0
  267. package/lib/yoke/muse-services.js +79 -0
  268. package/lib/yoke/muse-skills.js +79 -0
  269. package/lib/yoke/muse-turns.js +71 -0
  270. package/lib/yoke/options/acp.js +2 -0
  271. package/lib/yoke/options/antigravity.js +2 -0
  272. package/lib/yoke/options/claude.js +21 -0
  273. package/lib/yoke/options/codex-defaults.js +29 -0
  274. package/lib/yoke/options/codex-settings.js +21 -0
  275. package/lib/yoke/options/codex.js +6 -0
  276. package/lib/yoke/options/kiro-defaults.js +22 -0
  277. package/lib/yoke/options/kiro.js +5 -0
  278. package/lib/yoke/options/muse.js +5 -0
  279. package/lib/yoke/provider-contract.js +5 -0
  280. package/lib/yoke/providers/antigravity-executable.js +8 -0
  281. package/lib/yoke/providers/claude-executable.js +40 -0
  282. package/lib/yoke/providers/claude-model-descriptions.js +14 -0
  283. package/lib/yoke/providers/claude-process.js +97 -0
  284. package/lib/yoke/providers/claude-provision.js +75 -0
  285. package/lib/yoke/providers/codex-errors.js +8 -0
  286. package/lib/yoke/providers/muse-executable.js +17 -0
  287. package/lib/yoke/provisioning.js +6 -0
  288. package/lib/yoke/query-options.js +10 -0
  289. package/lib/yoke/response-recovery.js +41 -0
  290. package/lib/yoke/sdk-mcp-descriptors.js +37 -0
  291. package/lib/yoke/sdk-server-factory.js +7 -0
  292. package/lib/yoke/session-history.js +8 -0
  293. package/lib/yoke/session-tool-transport.js +34 -13
  294. package/lib/yoke/settings.js +23 -0
  295. package/lib/yoke/source-locations.js +4 -0
  296. package/lib/yoke/title-query.js +18 -7
  297. package/lib/yoke/tui-transcript-index.js +132 -0
  298. package/lib/yoke/usage.js +48 -0
  299. package/lib/yoke/user-input.js +13 -0
  300. package/lib/yoke/vendor-registry.js +81 -1
  301. package/package.json +3 -2
package/lib/sdk-bridge.js CHANGED
@@ -1,12 +1,9 @@
1
- const crypto = require("crypto");
2
- var codexQuestionGuidance = require("./yoke/user-input-guidance").codexQuestionGuidance;
1
+ var crypto = require("crypto");
3
2
  var fs = require("fs");
4
3
  var path = require("path");
5
4
  var execSync = require("child_process").execSync;
6
5
  var execFileSync = require("child_process").execFileSync;
7
6
  var usersModule = require("./users");
8
- var { getCodexConfig } = require("./codex-defaults");
9
- var { getKiroConfig } = require("./kiro-defaults");
10
7
  var { splitShellSegments, attachSkillDiscovery } = require("./sdk-skill-discovery");
11
8
  var skillDiscovery = require("./yoke/skill-discovery");
12
9
  var { isSafeBashSegment } = require("./safe-bash-commands");
@@ -19,7 +16,7 @@ var sessionTitlePolicy = require("./session-title-policy");
19
16
  var sessionTitleGenerator = require("./session-title-generator");
20
17
  var loopInterview = require("./project-loop-interview");
21
18
 
22
- var extractMcpDescriptors = require("./sdk-mcp-descriptors").extractMcpDescriptors;
19
+ var extractMcpDescriptors = require("./yoke/sdk-mcp-descriptors").extractMcpDescriptors;
23
20
 
24
21
  // Call an MCP tool handler by server name and tool name.
25
22
  // Returns a promise that resolves with the tool result.
@@ -137,7 +134,7 @@ function createSDKBridge(opts) {
137
134
  }];
138
135
  }
139
136
 
140
- function getCodexSessionMcpServer(session, queryGeneration) {
137
+ function getSessionMcpServer(session, queryGeneration) {
141
138
  if (!session || !Number.isInteger(queryGeneration)) return null;
142
139
  var bridgeArgs = [
143
140
  path.join(__dirname, "yoke", "mcp-bridge-server.js"),
@@ -165,25 +162,16 @@ function createSDKBridge(opts) {
165
162
  return _cachedFreshAuthState;
166
163
  }
167
164
 
168
- function isAuthErrorMessage(errDetail) {
169
- if (!errDetail) return false;
170
- var errLower = String(errDetail).toLowerCase();
171
- return errLower.indexOf("not logged in") !== -1
172
- || errLower.indexOf("unauthenticated") !== -1
173
- || errLower.indexOf("authentication") !== -1
174
- || errLower.indexOf("sign in") !== -1
175
- || errLower.indexOf("log in") !== -1
176
- || errLower.indexOf("please login") !== -1;
177
- }
165
+ var isAuthErrorMessage = require("./yoke/auth-events").isAuthenticationError;
178
166
 
179
167
  function getLoginCommand(vendor) {
180
168
  var info = yoke.getVendorInfo(vendor);
181
- return (info && info.loginCommand) || "claude login";
169
+ return (info && info.loginCommand) || "";
182
170
  }
183
171
 
184
172
  function supportsOsUserIsolation(vendor) {
185
173
  var info = yoke.getVendorInfo(vendor);
186
- return !info || info.osUserIsolation !== false;
174
+ return !!info && info.osUserIsolation === true;
187
175
  }
188
176
 
189
177
  function notifyAuthRequired(session, title, body, authLinuxUser, canAutoLogin, loginCommand) {
@@ -323,6 +311,7 @@ function createSDKBridge(opts) {
323
311
  }
324
312
 
325
313
  function stopIdleReaper() {
314
+ responseRecovery.dispose();
326
315
  if (_idleReaperTimer) {
327
316
  clearInterval(_idleReaperTimer);
328
317
  _idleReaperTimer = null;
@@ -389,6 +378,18 @@ function createSDKBridge(opts) {
389
378
  var sendAndRecord = msgProcessor.sendAndRecord;
390
379
  var sendToSession = msgProcessor.sendToSession;
391
380
 
381
+ var responseRecovery = require("./yoke/response-recovery").attachResponseRecovery(Object.assign({
382
+ getProvider: function(session) { var vendor = session.vendor || sm.defaultVendor || adapter && adapter.vendor; return adapters[vendor] || vendor || adapter; },
383
+ sessions: sm.sessions,
384
+ changed: onProcessingChanged,
385
+ record: sendAndRecord,
386
+ send: sendToSession,
387
+ broadcast: function() { if (sm.broadcastSessionList) sm.broadcastSessionList(); },
388
+ restart: function(session, payload, state) {
389
+ return startQuery(session, payload.text, payload.images, payload.linuxUser, payload.beforePush, null, state);
390
+ },
391
+ }, opts.responseRecovery || {}));
392
+
392
393
  // --- MCP elicitation ---
393
394
 
394
395
  function handleElicitation(session, request, opts) {
@@ -417,6 +418,7 @@ function createSDKBridge(opts) {
417
418
 
418
419
  if (pushModule) {
419
420
  pushModule.sendPush({
421
+ sessionId: session.localId,
420
422
  type: "elicitation",
421
423
  slug: slug,
422
424
  title: (request.serverName || "MCP Server") + " needs input",
@@ -467,6 +469,7 @@ function createSDKBridge(opts) {
467
469
 
468
470
  if (pushModule) {
469
471
  pushModule.sendPush({
472
+ sessionId: session.localId,
470
473
  type: "user_dialog",
471
474
  slug: slug,
472
475
  title: "Claude needs a decision",
@@ -489,44 +492,7 @@ function createSDKBridge(opts) {
489
492
  // Ensures the linux user's .claude project directory exists and is writable,
490
493
  // then pre-copies CLI session file if needed. Called before starting a query
491
494
  // so the worker can resume from the correct session file.
492
- function ensureLinuxUserProjectDir(linuxUser, session) {
493
- try {
494
- var configMod = require("./config");
495
- var osUsersMod = require("./os-users");
496
- var originalHome = configMod.REAL_HOME || require("os").homedir();
497
- var linuxUserHome = osUsersMod.getLinuxUserHome(linuxUser);
498
- var uid = osUsersMod.getLinuxUserUid(linuxUser);
499
- if (originalHome !== linuxUserHome && uid != null) {
500
- var projectSlug = (cwd || "").replace(/\//g, "-");
501
- var dstDir = path.join(linuxUserHome, ".claude", "projects", projectSlug);
502
- // Create and chown the project directory once
503
- if (!fs.existsSync(dstDir)) {
504
- fs.mkdirSync(dstDir, { recursive: true });
505
- try { execFileSync("chown", ["-R", String(uid), path.join(linuxUserHome, ".claude")]); } catch (e2) {}
506
- } else {
507
- try {
508
- var dirStat = fs.statSync(dstDir);
509
- if (dirStat.uid !== uid) {
510
- execFileSync("chown", [String(uid), dstDir]);
511
- }
512
- } catch (e2) {}
513
- }
514
- // Pre-copy CLI session file so the worker can resume the conversation
515
- if (session.cliSessionId) {
516
- var sessionFileName = session.cliSessionId + ".jsonl";
517
- var srcFile = path.join(originalHome, ".claude", "projects", projectSlug, sessionFileName);
518
- var dstFile = path.join(dstDir, sessionFileName);
519
- if (fs.existsSync(srcFile) && !fs.existsSync(dstFile)) {
520
- fs.copyFileSync(srcFile, dstFile);
521
- try { execFileSync("chown", [String(uid), dstFile]); } catch (e2) {}
522
- console.log("[sdk-bridge] Pre-copied CLI session " + session.cliSessionId + " to " + linuxUser);
523
- }
524
- }
525
- }
526
- } catch (copyErr) {
527
- console.log("[sdk-bridge] Dir setup / session pre-copy skipped:", copyErr.message);
528
- }
529
- }
495
+
530
496
 
531
497
  // --- SDK query lifecycle ---
532
498
 
@@ -745,6 +711,34 @@ function createSDKBridge(opts) {
745
711
  // Workspace inspection and proposal posting are session-bound. Keep this
746
712
  // list exact so a future execution/mutation tool cannot inherit approval.
747
713
  var safeWorkspaceTools = {
714
+ "mcp__clay-workspace__read_owner_screen": true,
715
+ "mcp__clay-workspace__inspect_owner_screen_change": true,
716
+ "mcp__clay-workspace__control_owner_screen": true,
717
+ "mcp__clay-workspace__discover_product_controls": true,
718
+ "mcp__clay-workspace__list_owned_mates": true,
719
+ "mcp__clay-workspace__begin_schedule_control_interview": true,
720
+ "mcp__clay-workspace__read_schedule_controls": true,
721
+ "mcp__clay-workspace__inspect_schedule_change": true,
722
+ "mcp__clay-workspace__control_schedules": true,
723
+ "mcp__clay-workspace__read_mate_chat_control": true,
724
+ "mcp__clay-workspace__inspect_mate_chat_change": true,
725
+ "mcp__clay-workspace__control_mate_chat": true,
726
+ "mcp__clay-workspace__read_project_workspace": true,
727
+ "mcp__clay-workspace__read_workspace_receipt": true,
728
+ "mcp__clay-workspace__inspect_workspace_change": true,
729
+ "mcp__clay-workspace__control_project_workspace": true,
730
+ "mcp__clay-workspace__read_project_records": true,
731
+ "mcp__clay-workspace__inspect_project_record_change": true,
732
+ "mcp__clay-workspace__control_project_records": true,
733
+ "mcp__clay-workspace__read_mate_knowledge_control": true,
734
+ "mcp__clay-workspace__inspect_mate_knowledge_change": true,
735
+ "mcp__clay-workspace__control_mate_knowledge": true,
736
+ "mcp__clay-workspace__inspect_mate_control": true,
737
+ "mcp__clay-workspace__control_mate_lifecycle": true,
738
+ "mcp__clay-workspace__inspect_project_control": true,
739
+ "mcp__clay-workspace__control_project_lifecycle": true,
740
+ "mcp__clay-workspace__inspect_session_control": true,
741
+ "mcp__clay-workspace__control_project_session": true,
748
742
  "mcp__clay-workspace__list_projects": true,
749
743
  "mcp__clay-workspace__list_project_sessions": true,
750
744
  "mcp__clay-workspace__search_project_history": true,
@@ -902,6 +896,7 @@ function createSDKBridge(opts) {
902
896
 
903
897
  if (pushModule) {
904
898
  pushModule.sendPush({
899
+ sessionId: session.localId,
905
900
  type: "permission_request",
906
901
  slug: slug,
907
902
  requestId: requestId,
@@ -943,58 +938,13 @@ function createSDKBridge(opts) {
943
938
  * Only returns processes whose cwd matches our project directory.
944
939
  * Returns an array of { pid, command } for each conflicting process found.
945
940
  */
946
- function findConflictingClaude() {
947
- try {
948
- var output = execFileSync("ps", ["ax", "-o", "pid,command"], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
949
- var lines = output.trim().split("\n");
950
- var candidates = [];
951
- for (var i = 1; i < lines.length; i++) { // skip header
952
- var line = lines[i].trim();
953
- var m = line.match(/^(\d+)\s+(.+)/);
954
- if (!m) continue;
955
- var pid = parseInt(m[1], 10);
956
- var cmd = m[2];
957
- // Skip our own process
958
- if (pid === process.pid) continue;
959
- // Skip node processes (our daemon, dev watchers, etc.)
960
- if (/\bnode\b/.test(cmd.split(/\s/)[0])) continue;
961
- // Match actual claude binary (e.g. /Users/x/.claude/local/claude, /usr/local/bin/claude)
962
- if (/\/claude(\s|$)/.test(cmd) || /^claude(\s|$)/.test(cmd)) {
963
- candidates.push({ pid: pid, command: cmd.substring(0, 200) });
964
- }
965
- }
966
-
967
- // Filter to only processes whose cwd matches our project
968
- var results = [];
969
- for (var j = 0; j < candidates.length; j++) {
970
- var c = candidates[j];
971
- try {
972
- // Use /proc/<pid>/cwd symlink (always available on Linux, no lsof dependency)
973
- var procCwd = fs.readlinkSync("/proc/" + c.pid + "/cwd");
974
- if (procCwd === cwd) {
975
- results.push(c);
976
- }
977
- } catch (e) {
978
- // /proc read failed — include as candidate anyway (conservative)
979
- results.push(c);
980
- }
981
- }
982
- return results;
983
- } catch (e) {
984
- return [];
985
- }
941
+ function findConflictingProcesses(session, error) {
942
+ var service = require("./yoke/provider-contract").resolve(getAdapterForSession(session)).processControl;
943
+ return service && typeof service.shouldInspect === "function" && service.shouldInspect(error) && typeof service.findConflicts === "function" ? service.findConflicts(cwd) : [];
986
944
  }
987
-
988
- /**
989
- * Verify that a PID is actually a claude binary process (not arbitrary).
990
- */
991
- function isClaudeProcess(pid) {
992
- try {
993
- var output = execFileSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"] }).trim();
994
- return /\/claude(\s|$)/.test(output) || /^claude(\s|$)/.test(output);
995
- } catch (e) {
996
- return false;
997
- }
945
+ function isProviderProcess(pid, session) {
946
+ var service = require("./yoke/provider-contract").resolve(session ? getAdapterForSession(session) : adapter).processControl;
947
+ return !!(service && typeof service.verifyProcess === "function" && service.verifyProcess(pid));
998
948
  }
999
949
 
1000
950
  function ownsQueryRuntime(session, queryInstance, queryGeneration) {
@@ -1006,6 +956,7 @@ function createSDKBridge(opts) {
1006
956
  function recordRuntimeEffectivePermissionMode(session, queryInstance, queryGeneration, message) {
1007
957
  if (!message || (message.yokeType !== "init" && message.yokeType !== "status") || !message.permissionMode) return false;
1008
958
  if (!ownsQueryRuntime(session, queryInstance, queryGeneration)) return false;
959
+ session._permissionObservationRevision = Number(session._permissionObservationRevision || 0) + 1;
1009
960
  session.effectivePermissionMode = message.permissionMode;
1010
961
  session.permissionModeFallbackReason = null;
1011
962
  sm.saveSessionFile(session);
@@ -1036,7 +987,8 @@ function createSDKBridge(opts) {
1036
987
  return true;
1037
988
  }
1038
989
 
1039
- async function processQueryStream(session, codexCatalogCandidate) {
990
+ async function processQueryStream(session, catalogCandidate, queryContract) {
991
+ queryContract = queryContract || yoke.sessionTools.contract(getAdapterForSession(session));
1040
992
  // Capture references at start so we only clean up OUR resources in finally,
1041
993
  // not resources from a newer query that may have been created after an abort.
1042
994
  var myQueryInstance = session.queryInstance;
@@ -1047,7 +999,7 @@ function createSDKBridge(opts) {
1047
999
  try {
1048
1000
  for await (var msg of myQueryInstance) {
1049
1001
  // An old iterator may drain after Stop or after a replacement query starts.
1050
- if (!ownsQueryRuntime(session, myQueryInstance, myQueryGeneration)) continue;
1002
+ if (!ownsQueryRuntime(session, myQueryInstance, myQueryGeneration) || responseRecovery.stopping(session)) continue;
1051
1003
  if ((session.taskStopRequested || (myAbortController && myAbortController.signal.aborted))
1052
1004
  && msg && msg.yokeType !== "error" && msg.yokeType !== "interrupted") continue;
1053
1005
  if (msg && msg.yokeType !== "text_delta" && msg.yokeType !== "thinking_delta" && msg.yokeType !== "tool_input_delta") {
@@ -1091,6 +1043,7 @@ function createSDKBridge(opts) {
1091
1043
  if (continuationIdentity && !ownsQueryRuntime(session, myQueryInstance, myQueryGeneration)) {
1092
1044
  continue;
1093
1045
  }
1046
+ responseRecovery.event(session, msg);
1094
1047
  processSDKMessage(session, msg);
1095
1048
  if (continuationIdentity) {
1096
1049
  settleContinuationStartup(session, myQueryInstance, myQueryGeneration, true, null);
@@ -1101,26 +1054,21 @@ function createSDKBridge(opts) {
1101
1054
  // permission state. Setter acknowledgement deliberately does not
1102
1055
  // change this field.
1103
1056
  recordRuntimeEffectivePermissionMode(session, myQueryInstance, myQueryGeneration, msg);
1104
- if (codexCatalogCandidate && msg && msg.yokeType === "session_started" && msg.sessionId
1057
+ if (catalogCandidate && msg && msg.yokeType === "session_started" && msg.sessionId
1105
1058
  && session.queryInstance === myQueryInstance
1106
- && Number(session._sdkQueryGeneration || 0) === codexCatalogCandidate.generation
1059
+ && Number(session._sdkQueryGeneration || 0) === catalogCandidate.generation
1107
1060
  && session.cliSessionId === msg.sessionId
1108
1061
  && (!sm.sessions || typeof sm.sessions.get !== "function" || sm.sessions.get(session.localId) === session)) {
1109
- if (codexCatalogCandidate.version) session.codexDynamicToolCatalogVersion = codexCatalogCandidate.version;
1110
- if (codexCatalogCandidate.userInputVersion) session.codexUserInputToolCatalogVersion = codexCatalogCandidate.userInputVersion;
1111
- if (codexCatalogCandidate.sharedBrowserVersion) session.codexSharedBrowserToolCatalogVersion = codexCatalogCandidate.sharedBrowserVersion;
1112
- if (codexCatalogCandidate.issuesVersion) session.codexIssuesToolCatalogVersion = codexCatalogCandidate.issuesVersion;
1113
- if (codexCatalogCandidate.youVersion) session.codexYouToolCatalogVersion = codexCatalogCandidate.youVersion;
1114
- if (codexCatalogCandidate.linearVersion) session.codexLinearToolCatalogVersion = codexCatalogCandidate.linearVersion;
1062
+ queryContract.recordCatalog(session, catalogCandidate.versions);
1115
1063
  sm.saveSessionFile(session);
1116
- codexCatalogCandidate = null;
1064
+ catalogCandidate = null;
1117
1065
  }
1118
1066
  }
1119
1067
  // (getContextUsage moved to processSDKMessage result handler -- fire-and-forget)
1120
1068
  // Stream ended normally after a task stop — no "result" message was sent,
1121
1069
  // so the session is still marked as processing. Send interrupted feedback.
1122
1070
  console.log("[sdk-bridge] processQueryStream ended: isProcessing=" + session.isProcessing + " taskStopRequested=" + session.taskStopRequested);
1123
- var stillOwnsRuntime = session.queryInstance === myQueryInstance;
1071
+ var stillOwnsRuntime = session.queryInstance === myQueryInstance && !responseRecovery.stopping(session);
1124
1072
  settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, "The successor query ended before initialization.");
1125
1073
  if (session.isProcessing && session.taskStopRequested && stillOwnsRuntime) {
1126
1074
  session._lastTurnInterrupted = true;
@@ -1131,9 +1079,7 @@ function createSDKBridge(opts) {
1131
1079
  onProcessingChanged();
1132
1080
  send({ type: "status", processing: false });
1133
1081
  sendAndRecord(session, { type: "thinking_stop" });
1134
- var interruptMsg = (session.vendor && session.vendor !== "claude")
1135
- ? "\u25a0 Conversation interrupted - tell the model what to do differently."
1136
- : "Interrupted \u00b7 What should Claude do instead?";
1082
+ var interruptMsg = (queryContract && queryContract.interruptionMessage) || "■ Conversation interrupted - tell the model what to do differently.";
1137
1083
  sendAndRecord(session, { type: "info", text: interruptMsg });
1138
1084
  sendAndRecord(session, { type: "done", code: 0 });
1139
1085
  sm.broadcastSessionList();
@@ -1158,7 +1104,7 @@ function createSDKBridge(opts) {
1158
1104
  }
1159
1105
  } catch (err) {
1160
1106
  settleContinuationStartup(session, myQueryInstance, myQueryGeneration, false, err.message || "The successor query failed before initialization.");
1161
- if (session.isProcessing && session.queryInstance === myQueryInstance) {
1107
+ if (session.isProcessing && session.queryInstance === myQueryInstance && !responseRecovery.stopping(session)) {
1162
1108
  session.isProcessing = false;
1163
1109
  session._awaitingTurnResult = false;
1164
1110
  session._queuedTurnCount = 0;
@@ -1168,9 +1114,7 @@ function createSDKBridge(opts) {
1168
1114
  session._lastTurnInterrupted = true;
1169
1115
  if (!session.destroying) {
1170
1116
  sendAndRecord(session, { type: "thinking_stop" });
1171
- var interruptMsg2 = (session.vendor && session.vendor !== "claude")
1172
- ? "\u25a0 Conversation interrupted - tell the model what to do differently."
1173
- : "Interrupted \u00b7 What should Claude do instead?";
1117
+ var interruptMsg2 = (queryContract && queryContract.interruptionMessage) || "■ Conversation interrupted - tell the model what to do differently.";
1174
1118
  sendAndRecord(session, { type: "info", text: interruptMsg2 });
1175
1119
  sendAndRecord(session, { type: "done", code: 0 });
1176
1120
  }
@@ -1186,21 +1130,17 @@ function createSDKBridge(opts) {
1186
1130
  console.error("[sdk-bridge] Query stream error for session " + session.localId + ":", errDetail);
1187
1131
  console.error("[sdk-bridge] Stack:", err.stack || "(no stack)");
1188
1132
 
1189
- // Check for conflicting Claude processes only on exit code 1
1190
- var isExitCode1 = err.exitCode === 1 || (err.message && err.message.indexOf("exited with code 1") !== -1);
1191
- var conflicts = isExitCode1 ? findConflictingClaude() : [];
1133
+ var conflicts = findConflictingProcesses(session, err);
1192
1134
  if (conflicts.length > 0) {
1193
- console.error("[sdk-bridge] Found " + conflicts.length + " conflicting Claude process(es):", conflicts.map(function(c) { return "PID " + c.pid; }).join(", "));
1135
+ console.error("[sdk-bridge] Found " + conflicts.length + " conflicting provider process(es):", conflicts.map(function(c) { return "PID " + c.pid; }).join(", "));
1194
1136
  sendAndRecord(session, {
1195
1137
  type: "process_conflict",
1196
- text: "Another Claude Code process is already running in this project.",
1138
+ text: "Another provider process is already running in this project.",
1197
1139
  processes: conflicts,
1198
1140
  });
1199
1141
  } else {
1200
1142
  var errLower = errDetail.toLowerCase();
1201
- var isContextOverflow = errLower.indexOf("prompt is too long") !== -1
1202
- || errLower.indexOf("context_length") !== -1
1203
- || errLower.indexOf("maximum context length") !== -1;
1143
+ var isContextOverflow = require("./yoke/errors").contextOverflow(errDetail);
1204
1144
  var isAuthError = isAuthErrorMessage(errDetail);
1205
1145
  if (isContextOverflow) {
1206
1146
  sendAndRecord(session, {
@@ -1247,7 +1187,7 @@ function createSDKBridge(opts) {
1247
1187
  // chat message already sent above
1248
1188
  }
1249
1189
  } else {
1250
- sendAndRecord(session, { type: "error", text: "Claude process error: " + err.message });
1190
+ sendAndRecord(session, { type: "error", text: "Provider process error: " + err.message });
1251
1191
  }
1252
1192
  }
1253
1193
  sendAndRecord(session, { type: "done", code: 1 });
@@ -1272,7 +1212,8 @@ function createSDKBridge(opts) {
1272
1212
  }
1273
1213
  if (session.messageQueue === myMessageQueue) session.messageQueue = null;
1274
1214
  if (session.abortController === myAbortController) session.abortController = null;
1275
- if (ownsRuntime) {
1215
+ if (ownsRuntime && !responseRecovery.stopping(session)) {
1216
+ responseRecovery.cancel(session);
1276
1217
  session.taskStopRequested = false;
1277
1218
  session.pendingPermissions = {};
1278
1219
  // Preserve only restored legacy MCP entries. Canonical Yoke requests
@@ -1321,7 +1262,7 @@ function createSDKBridge(opts) {
1321
1262
  }
1322
1263
  }
1323
1264
  }
1324
- if (!session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
1265
+ if (!responseRecovery.stopping(session) && !session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
1325
1266
  }
1326
1267
  }
1327
1268
 
@@ -1329,20 +1270,16 @@ function createSDKBridge(opts) {
1329
1270
  if (session.queryInstance) return { query: session.queryInstance, isTemp: false, cleanup: function() {} };
1330
1271
 
1331
1272
  var handle;
1273
+ var selectedAdapter = getAdapterForSession(session);
1332
1274
  try {
1333
- handle = await adapter.createQuery({
1275
+ handle = await selectedAdapter.createQuery({
1334
1276
  cwd: cwd,
1335
1277
  resumeSessionId: session.cliSessionId,
1336
1278
  env: getRuntimeEnv(session),
1337
- adapterOptions: {
1338
- CLAUDE: {
1339
- settingSources: ["user", "project", "local"],
1340
- enableFileCheckpointing: true,
1341
- },
1342
- },
1279
+ adapterOptions: require("./yoke/query-options").map(getAdapterForSession(session), { nativePermissionMode: "default" }),
1343
1280
  });
1344
1281
  } catch (e) {
1345
- sendAndRecord(session, { type: "error", text: "Failed to load Claude SDK: " + (e.message || e) });
1282
+ sendAndRecord(session, { type: "error", text: "Failed to load provider session: " + (e.message || e) });
1346
1283
  throw e;
1347
1284
  }
1348
1285
 
@@ -1409,7 +1346,7 @@ function createSDKBridge(opts) {
1409
1346
  // abort the whole rewind instead of leaving local history trimmed
1410
1347
  // ahead of the real, un-rolled-back provider thread.
1411
1348
  if (!turnId) {
1412
- throw new Error("No turn id recorded for this turn; cannot roll back the Codex thread.");
1349
+ throw new Error("No turn id recorded for this turn; cannot roll back the provider thread.");
1413
1350
  }
1414
1351
  await sessionAdapter.rollbackThread(session.cliSessionId, turnId, {
1415
1352
  linuxUser: getSessionLinuxUser(session) || undefined,
@@ -1425,6 +1362,8 @@ function createSDKBridge(opts) {
1425
1362
 
1426
1363
  async function forkSessionUnified(session, uuid) {
1427
1364
  var sessionAdapter = getAdapterForSession(session);
1365
+ var localHistoryEndIndex = require("./session-fork-history").endIndex(session, uuid,
1366
+ require("./yoke/provider-contract").resolve(sessionAdapter).forkHistoryBoundary);
1428
1367
  var result = await sessionAdapter.forkSession(session.cliSessionId, {
1429
1368
  upToMessageId: uuid,
1430
1369
  dir: cwd,
@@ -1433,8 +1372,10 @@ function createSDKBridge(opts) {
1433
1372
  if (!result || !result.sessionId) throw new Error("Fork returned no session id");
1434
1373
 
1435
1374
  // Adapters with rollbackThread (e.g. Codex) use local history copy
1436
- if (typeof sessionAdapter.rollbackThread === "function") {
1437
- return { sessionId: result.sessionId, useLocalHistory: true };
1375
+ if (result.useLocalHistory === true || typeof sessionAdapter.rollbackThread === "function") {
1376
+ var localFork = { sessionId: result.sessionId, useLocalHistory: true };
1377
+ if (localHistoryEndIndex !== undefined) localFork.localHistoryEndIndex = localHistoryEndIndex;
1378
+ return localFork;
1438
1379
  }
1439
1380
  // Claude: read history from CLI session files
1440
1381
  return { sessionId: result.sessionId, useLocalHistory: false };
@@ -1442,22 +1383,69 @@ function createSDKBridge(opts) {
1442
1383
 
1443
1384
  // Wrapper: marks the boot window so pushMessage can buffer instead of
1444
1385
  // dropping messages that arrive while createQuery is still awaiting.
1445
- async function startQuery(session, text, images, linuxUser, beforePush, onAccepted) {
1386
+ async function startQuery(session, text, images, linuxUser, beforePush, onAccepted, retryState) {
1387
+ if (opts.isLifecycleExecutionPaused && opts.isLifecycleExecutionPaused()) {
1388
+ 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." });
1389
+ session.isProcessing = false; onProcessingChanged(); sendAndRecord(session, { type: "done", code: 1 }); return false;
1390
+ }
1391
+ if (session._responseRecoveryStopPending && !retryState) {
1392
+ session.isProcessing = false;
1393
+ onProcessingChanged();
1394
+ sendAndRecord(session, { type: "error", text: "The previous Claude attempt has not confirmed stopped. Check the connection before sending this message again." });
1395
+ sendAndRecord(session, { type: "done", code: 1 });
1396
+ return false;
1397
+ }
1398
+ if (responseRecovery.stopping(session) && !retryState) return false;
1446
1399
  delete session._lastTurnInterrupted;
1400
+ var payload = { text: text, images: images, linuxUser: linuxUser, beforePush: beforePush };
1401
+ var recoveryState = responseRecovery.begin(session, payload, retryState);
1402
+ if (recoveryState) recoveryState.handle = null;
1403
+ var guardedPush = function() {
1404
+ return !session.destroying && !session.taskStopRequested
1405
+ && (!recoveryState || responseRecovery.valid(session, recoveryState) && !recoveryState.stopping)
1406
+ && (typeof beforePush !== "function" || beforePush() === true);
1407
+ };
1408
+ guardedPush.exactProductDelivery = !!(beforePush && beforePush.exactProductDelivery);
1447
1409
  session._queryGeneration = (session._queryGeneration || 0) + 1;
1448
1410
  session._queryStarting = true;
1411
+ var starting = startQueryInner(session, text, images, linuxUser, guardedPush, onAccepted, recoveryState);
1412
+ if (recoveryState) recoveryState.starting = starting;
1449
1413
  try {
1450
- return await startQueryInner(session, text, images, linuxUser, beforePush, onAccepted);
1414
+ var accepted = await starting;
1415
+ if (accepted === false && (!recoveryState || !recoveryState.stopping)) {
1416
+ responseRecovery.cancel(session);
1417
+ session.isProcessing = false;
1418
+ onProcessingChanged();
1419
+ }
1420
+ return accepted;
1421
+ } catch (error) {
1422
+ // Includes synchronous input queue failures, before stream consumption.
1423
+ var failedHandle = session.queryInstance;
1424
+ if (failedHandle) {
1425
+ try { if (typeof failedHandle.abort === "function") failedHandle.abort(); } catch (e) {}
1426
+ try { if (typeof failedHandle.close === "function") failedHandle.close(); } catch (e) {}
1427
+ }
1428
+ session.queryInstance = null;
1429
+ session.abortController = null;
1430
+ session.messageQueue = null;
1431
+ session.isProcessing = false;
1432
+ session._awaitingTurnResult = false;
1433
+ session._queuedTurnCount = 0;
1434
+ taskLifecycle.clearAll(session);
1435
+ if (!recoveryState || !recoveryState.stopping) {
1436
+ responseRecovery.cancel(session);
1437
+ sendAndRecord(session, { type: "error", text: "The agent connection failed while delivering your message. Please retry or check the connection." });
1438
+ sendAndRecord(session, { type: "done", code: 1 });
1439
+ onProcessingChanged();
1440
+ sm.broadcastSessionList();
1441
+ }
1442
+ return false;
1451
1443
  } finally {
1452
1444
  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);
1445
+ delete session._providerRecoveryWaiting;
1446
+ if (!session.queryInstance && session.pendingPush && session.pendingPush.length) session.pendingPush = [];
1447
+ if (!session.queryInstance && !session.isProcessing && !session._pendingMessageDrainPaused && !responseRecovery.stopping(session)) session.taskStopRequested = false;
1448
+ if (!responseRecovery.stopping(session) && !session.isProcessing && typeof opts.onSessionAvailable === "function") opts.onSessionAvailable(session);
1461
1449
  }
1462
1450
  }
1463
1451
 
@@ -1501,7 +1489,7 @@ function createSDKBridge(opts) {
1501
1489
  };
1502
1490
  }
1503
1491
 
1504
- async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted) {
1492
+ async function startQueryInner(session, text, images, linuxUser, beforePush, onAccepted, recoveryState) {
1505
1493
  var sessionRuntimeEnv = getRuntimeEnv(session);
1506
1494
  var resumedAtQueryStart = !!session.cliSessionId;
1507
1495
  async function ensureVendorReady(vendor) {
@@ -1653,7 +1641,8 @@ function createSDKBridge(opts) {
1653
1641
 
1654
1642
  // Ensure Linux user project directory exists (runs in parallel with worker boot)
1655
1643
  if (linuxUser) {
1656
- ensureLinuxUserProjectDir(linuxUser, session);
1644
+ var preparation = require("./yoke/provider-contract").resolve(sessionAdapter).prepareIsolation;
1645
+ if (typeof preparation === "function") preparation(linuxUser, session, cwd);
1657
1646
  }
1658
1647
 
1659
1648
  session.blocks = {};
@@ -1670,100 +1659,27 @@ function createSDKBridge(opts) {
1670
1659
  session.abortController = new AbortController();
1671
1660
  }
1672
1661
 
1673
- // Build Claude-specific adapter options
1674
- var claudeOpts = {
1675
- settingSources: ["user", "project", "local"],
1676
- includePartialMessages: true,
1677
- enableFileCheckpointing: true,
1678
- extraArgs: { "replay-user-messages": null },
1679
- promptSuggestions: true,
1680
- agentProgressSummaries: true,
1681
- };
1682
-
1683
- // Per-loop settings override global defaults when present
1684
1662
  var ls = session.loopSettings || {};
1685
-
1686
- if (sm.currentBetas && sm.currentBetas.length > 0) {
1687
- claudeOpts.betas = sm.currentBetas;
1688
- }
1689
- var thinkingMode = ls.thinking || sm.currentThinking;
1690
- if (thinkingMode === "disabled") {
1691
- claudeOpts.thinking = { type: "disabled" };
1692
- } else if (thinkingMode === "budget") {
1693
- var budgetTokens = ls.thinkingBudget || sm.currentThinkingBudget;
1694
- if (budgetTokens) claudeOpts.thinking = { type: "enabled", budgetTokens: budgetTokens };
1695
- } else {
1696
- // Adaptive (the default). Request summarized display explicitly:
1697
- // current models default thinking.display to "omitted", which streams
1698
- // thinking blocks whose text is an EMPTY string -- the UI's thinking
1699
- // section stays blank even though thinking happens and is billed.
1700
- // (SDK ThinkingAdaptive: { type: "adaptive", display?: "summarized" | "omitted" })
1701
- claudeOpts.thinking = { type: "adaptive", display: "summarized" };
1702
- }
1703
-
1704
- if (ls.permissionMode) {
1705
- session._loopPermissionMode = ls.permissionMode;
1706
- }
1707
-
1708
- // Pass through any extra SDK settings from LOOP.json
1709
- if (ls.disableAllHooks !== undefined) {
1710
- claudeOpts.settings = Object.assign({}, claudeOpts.settings || {}, { disableAllHooks: ls.disableAllHooks });
1711
- }
1712
-
1713
- if (dangerouslySkipPermissions || (session.mode === "tui" && session.dangerouslySkipPermissions)) {
1714
- claudeOpts.allowDangerouslySkipPermissions = true;
1715
- claudeOpts.permissionMode = "bypassPermissions";
1716
- } else {
1717
- var globalMode = session.permissionMode || sm.currentPermissionMode || "default";
1718
- var effectiveDefault;
1719
- // GUI full access stays inside Clay's canUseTool callback. Passing the
1720
- // SDK bypass mode would require its process-level dangerous escape hatch
1721
- // and would skip Clay's permission policy entirely.
1722
- if (globalMode === "bypassPermissions") effectiveDefault = "default";
1723
- else if (session.acceptEditsAfterStart) effectiveDefault = "acceptEdits";
1724
- else effectiveDefault = globalMode;
1725
- var modeToApply = session._loopPermissionMode || effectiveDefault;
1726
- // Always set permissionMode explicitly, including "default". SDK
1727
- // 0.3.286+ leaves an omitted permissionMode to Claude Code itself,
1728
- // which can apply a settings defaultMode or start in auto mode on
1729
- // third-party providers / with telemetry off -- silently widening
1730
- // past Clay's Ask policy. Sending "default" keeps the SDK's own
1731
- // decision point at the same "default" behavior Clay has always run
1732
- // under: a tool the SDK's own allowed/denied rules or allowedTools
1733
- // list already settles is not re-asked, but anything left undecided
1734
- // still reaches Clay's canUseTool callback, as before.
1735
- claudeOpts.permissionMode = modeToApply || "default";
1736
- }
1737
- if (session.youCurator) {
1738
- claudeOpts.permissionMode = "default";
1739
- claudeOpts.allowDangerouslySkipPermissions = false;
1740
- }
1741
- // Clear one-shot acceptEditsAfterStart regardless of which branch ran above,
1742
- // so the flag does not linger into subsequent turns.
1743
- if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1744
- if (session.cliSessionId && session.lastRewindUuid) {
1745
- claudeOpts.resumeSessionAt = session.lastRewindUuid;
1746
- delete session.lastRewindUuid;
1747
- sm.saveSessionFile(session);
1748
- }
1749
-
1750
- // Pass linuxUser to adapter for worker-based queries
1663
+ if (ls.permissionMode) session._loopPermissionMode = ls.permissionMode;
1664
+ var permissionMode = session.permissionMode || sm.currentPermissionMode || "default";
1665
+ var nativeMode = permissionMode === "bypassPermissions" ? "default" : session.acceptEditsAfterStart ? "acceptEdits" : permissionMode;
1666
+ var isolationHome = null;
1751
1667
  if (linuxUser) {
1752
- claudeOpts.linuxUser = linuxUser;
1753
- claudeOpts.singleTurn = !!session.singleTurn;
1754
- try {
1755
- claudeOpts.originalHome = require("./os-users").resolveOsUserInfo(linuxUser).home;
1756
- } catch (e) {
1757
- claudeOpts.originalHome = null;
1758
- }
1759
- claudeOpts.projectPath = session.cwd || null;
1760
- claudeOpts._perfT0 = t0;
1761
- // Pass previous worker state for reuse
1762
- if (session._adapterWorkerState) {
1763
- claudeOpts._workerState = session._adapterWorkerState;
1764
- session._adapterWorkerState = null;
1765
- }
1766
- }
1668
+ try { isolationHome = require("./os-users").resolveOsUserInfo(linuxUser).home; } catch (error) {}
1669
+ }
1670
+ var executionIntent = {
1671
+ settings: sm, permissionMode: permissionMode, nativePermissionMode: session._loopPermissionMode || nativeMode,
1672
+ nativeBypass: dangerouslySkipPermissions || session.mode === "tui" && session.dangerouslySkipPermissions,
1673
+ readOnly: !!session.youCurator, betas: sm.currentBetas,
1674
+ thinking: { mode: ls.thinking || sm.currentThinking, budget: ls.thinkingBudget || sm.currentThinkingBudget },
1675
+ disableHooks: ls.disableAllHooks, resumeAt: session.cliSessionId && session.lastRewindUuid || undefined,
1676
+ singleTurn: !!session.singleTurn, startedAt: t0,
1677
+ isolation: { user: linuxUser, home: isolationHome, projectPath: session.cwd || null, workerState: session._adapterWorkerState },
1678
+ sessionBridge: null,
1679
+ };
1680
+ if (session.acceptEditsAfterStart) delete session.acceptEditsAfterStart;
1681
+ if (executionIntent.resumeAt) { delete session.lastRewindUuid; sm.saveSessionFile(session); }
1682
+ if (linuxUser && session._adapterWorkerState) session._adapterWorkerState = null;
1767
1683
 
1768
1684
  // Pick a model from the session or its vendor-specific default, then
1769
1685
  // validate it against the session vendor's known model list.
@@ -1789,20 +1705,21 @@ function createSDKBridge(opts) {
1789
1705
  sm.saveSessionFile(session);
1790
1706
  }
1791
1707
 
1792
- var codexConfig = getCodexConfig(sm);
1793
- var kiroConfig = getKiroConfig(sm);
1794
1708
  session._sdkQueryGeneration = Number(session._sdkQueryGeneration || 0) + 1;
1795
1709
  // Individual feature modules own their transport capability checks. Do not
1796
1710
  // suppress unrelated session tools merely because one feature cannot be
1797
1711
  // delivered by this provider.
1798
1712
  var sessionToolDefs = getSessionToolDefs(session) || [];
1799
1713
  var queryGeneration = Number(session._sdkQueryGeneration || 0);
1714
+ executionIntent.sessionBridge = getAcpMcpServers(session);
1715
+ var queryContract = yoke.sessionTools.contract(sessionAdapter);
1716
+ function needsCatalogRecovery(family, version) {
1717
+ return yoke.sessionTools.needsRecovery(sessionAdapter, session, family, version);
1718
+ }
1800
1719
  var hasYouCatalog = sessionToolDefs.some(function (tool) { return tool.name === "search_user_knowledge"; });
1801
- var recoverCodexYouTools = sessionAdapter.vendor === "codex" && !!session.cliSessionId
1802
- && hasYouCatalog && Number(session.codexYouToolCatalogVersion || 0) < 1;
1720
+ var recoverYouTools = needsCatalogRecovery("knowledge", 1) && hasYouCatalog;
1803
1721
  var hasLinearCatalog = sessionToolDefs.some(function (tool) { return tool.name === "link_session_linear"; });
1804
- var recoverCodexLinearTools = sessionAdapter.vendor === "codex" && !!session.cliSessionId
1805
- && hasLinearCatalog && Number(session.codexLinearToolCatalogVersion || 0) < 1 && canUseSessionTools(session) === true;
1722
+ var recoverLinearTools = needsCatalogRecovery("linear", 1) && hasLinearCatalog && canUseSessionTools(session) === true;
1806
1723
  var hasIssueCatalog = false;
1807
1724
  for (var issueToolIndex = 0; issueToolIndex < sessionToolDefs.length; issueToolIndex++) {
1808
1725
  if (sessionToolDefs[issueToolIndex].name === "create_issue") {
@@ -1811,24 +1728,23 @@ function createSDKBridge(opts) {
1811
1728
  }
1812
1729
  }
1813
1730
  var hasSharedBrowserCatalog = sessionToolDefs.some(function (definition) { return definition.name === "shared_browser"; });
1814
- var recoverCodexBrowserTools = sessionAdapter.vendor === "codex" && !!session.cliSessionId
1815
- && hasSharedBrowserCatalog && Number(session.codexSharedBrowserToolCatalogVersion || 0) < 6;
1816
- var recoverCodexIssueTools = sessionAdapter.vendor === "codex"
1817
- && !!session.cliSessionId
1818
- && hasIssueCatalog
1819
- && Number(session.codexIssuesToolCatalogVersion || 0) < 1;
1731
+ var recoverBrowserTools = needsCatalogRecovery("browser", 6) && hasSharedBrowserCatalog;
1732
+ var recoverIssueTools = needsCatalogRecovery("issues", 1) && hasIssueCatalog;
1820
1733
  function queryContextAvailable() {
1734
+ if (queryRetired()) return false;
1821
1735
  if (Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
1822
1736
  if (sm && sm.sessions && typeof sm.sessions.get === "function" && sm.sessions.get(session.localId) !== session) return false;
1823
1737
  return canUseSessionTools(session) === true;
1824
1738
  }
1739
+ var hasMateScheduleTool = sessionToolDefs.some(function (tool) { return yoke.sessionTools.MATE_SCHEDULE_TOOLS.indexOf(tool.name) !== -1; });
1740
+ var recoverMateSchedules = needsCatalogRecovery() && hasMateScheduleTool && queryContextAvailable();
1825
1741
  var hasScheduleTool = false;
1826
1742
  var hasScheduleInterviewTool = false;
1827
1743
  var scheduleInterviewActive = null;
1828
1744
  var scheduleQuestionLimit = null;
1829
1745
  for (var sessionToolIndex = 0; sessionToolIndex < sessionToolDefs.length; sessionToolIndex++) {
1830
1746
  if (yoke.sessionTools.SCHEDULE_TOOLS.indexOf(sessionToolDefs[sessionToolIndex].name) !== -1) hasScheduleTool = true;
1831
- if (sessionToolDefs[sessionToolIndex].name === "begin_scheduled_task_interview") {
1747
+ if (["begin_scheduled_task_interview", "begin_mate_schedule_interview", "begin_schedule_control_interview"].indexOf(sessionToolDefs[sessionToolIndex].name) >= 0) {
1832
1748
  hasScheduleInterviewTool = true;
1833
1749
  scheduleInterviewActive = sessionToolDefs[sessionToolIndex].scheduleInterviewActive || null;
1834
1750
  scheduleQuestionLimit = sessionToolDefs[sessionToolIndex].structuredQuestionLimit || null;
@@ -1838,10 +1754,10 @@ function createSDKBridge(opts) {
1838
1754
  var activeLoopInterview = !isDriverOperatedSession(session) && !session.loopInterviewHandoff && !activeRun && !!loopInterview.activeInterviewId(session);
1839
1755
  var structuredInputSession = hasScheduleInterviewTool || activeLoopInterview || (!isDriverOperatedSession(session) && session.ralphCraftingMode === true && session.loop && session.loop.role === "crafting");
1840
1756
  var requestedUserInputMode = session.debateSetupMode || session.mateCreationMode ? "fallback" : "auto";
1841
- if (structuredInputSession || sessionAdapter.vendor === "codex") requestedUserInputMode = "fallback";
1757
+ if (structuredInputSession) requestedUserInputMode = "fallback";
1758
+ if (requestedUserInputMode === "auto" && queryContract.defaultUserInputMode) requestedUserInputMode = queryContract.defaultUserInputMode;
1842
1759
  var selectedUserInputMode = yoke.userInput.selectMode(sessionAdapter, requestedUserInputMode);
1843
- var recoverCodexQuestionTools = sessionAdapter.vendor === "codex" && !!session.cliSessionId
1844
- && Number(session.codexUserInputToolCatalogVersion || 0) < 1 && queryContextAvailable();
1760
+ var recoverQuestionTools = needsCatalogRecovery("questions", 1) && queryContextAvailable();
1845
1761
  function structuredInputContextAvailable() {
1846
1762
  if (!queryContextAvailable()) return false;
1847
1763
  // A schedule-triggered fallback shares the schedule feature's live
@@ -1860,9 +1776,10 @@ function createSDKBridge(opts) {
1860
1776
  return "Ask exactly one question at a time, wait for the answer, then ask the next question.";
1861
1777
  }
1862
1778
  var sessionUserInputHandler = function(request, respond) {
1863
- if (!structuredInputContextAvailable()) {
1779
+ if (queryRetired() || !structuredInputContextAvailable()) {
1864
1780
  return respond.cancel("This structured-input request belongs to an unavailable or older query.");
1865
1781
  }
1782
+ responseRecovery.progress(session);
1866
1783
  var questionError = scheduleQuestionError(request);
1867
1784
  if (questionError) return respond.cancel(questionError);
1868
1785
  if (request.presentation === "elicitation" && request.diagnostics && request.diagnostics.elicitation) {
@@ -1882,21 +1799,33 @@ function createSDKBridge(opts) {
1882
1799
  return opts.onUserInputRequest(session, request, respond);
1883
1800
  };
1884
1801
  var localMcpServers = getMcpServers(session);
1885
- if (sessionAdapter.vendor === "claude" && hasScheduleTool && typeof sessionAdapter.createToolServer === "function") {
1802
+ if (queryContract.toolTransport === "sdk-mcp" && (hasScheduleTool || hasMateScheduleTool)
1803
+ && typeof sessionAdapter.createToolServer !== "function") {
1804
+ throw new Error("The adapter cannot mount native schedule tools without createToolServer.");
1805
+ }
1806
+ if (queryContract.toolTransport === "sdk-mcp" && hasScheduleTool) {
1886
1807
  var scheduledTaskToolDefs = [];
1887
1808
  for (var scheduledToolIndex = 0; scheduledToolIndex < sessionToolDefs.length; scheduledToolIndex++) {
1888
1809
  if (yoke.sessionTools.SCHEDULE_TOOLS.indexOf(sessionToolDefs[scheduledToolIndex].name) !== -1) scheduledTaskToolDefs.push(sessionToolDefs[scheduledToolIndex]);
1889
1810
  }
1890
1811
  if (scheduledTaskToolDefs.length) {
1891
1812
  var scheduledTaskServer = sessionAdapter.createToolServer({ name: "clay-scheduled-tasks", version: "1.0.0", tools: scheduledTaskToolDefs });
1892
- if (scheduledTaskServer) {
1893
- localMcpServers = Object.assign({}, localMcpServers || {});
1894
- localMcpServers["clay-scheduled-tasks"] = scheduledTaskServer;
1895
- }
1813
+ if (!scheduledTaskServer) throw new Error("The adapter could not mount scheduled-task tools.");
1814
+ localMcpServers = Object.assign({}, localMcpServers || {});
1815
+ localMcpServers["clay-scheduled-tasks"] = scheduledTaskServer;
1896
1816
  }
1897
1817
  }
1818
+ if (queryContract.toolTransport === "sdk-mcp" && hasMateScheduleTool) {
1819
+ localMcpServers = Object.assign({}, localMcpServers || {});
1820
+ localMcpServers["clay-mate-schedules"] = sessionAdapter.createToolServer({ name: "clay-mate-schedules", version: "1.0.0",
1821
+ tools: sessionToolDefs.filter(function (tool) { return yoke.sessionTools.MATE_SCHEDULE_TOOLS.indexOf(tool.name) !== -1; }) });
1822
+ if (!localMcpServers["clay-mate-schedules"]) throw new Error("The adapter could not mount Mate schedule tools.");
1823
+ }
1898
1824
  if (selectedUserInputMode === "fallback") {
1899
1825
  var userInputServer = yoke.userInput.createFallbackServer(sessionAdapter, sessionUserInputHandler, { force: true, maxQuestions: scheduleQuestionLimit });
1826
+ if (queryContract.toolTransport === "sdk-mcp" && !userInputServer) {
1827
+ throw new Error("The adapter could not mount structured-input tools.");
1828
+ }
1900
1829
  if (userInputServer) {
1901
1830
  localMcpServers = Object.assign({}, localMcpServers || {});
1902
1831
  localMcpServers[userInputServer.name || "yoke-user-input"] = userInputServer;
@@ -1933,9 +1862,9 @@ function createSDKBridge(opts) {
1933
1862
 
1934
1863
  var externalQueryBridge = require('./mcp-query-bridge').createQueryBridge({ valid: queryContextAvailable,
1935
1864
  getServers: function () { return getRemoteMcpServers ? getRemoteMcpServers(session) || {} : {}; } });
1936
- var externalToolDefs = sessionAdapter.vendor === 'codex' ? externalQueryBridge.dynamicDefs() : [];
1865
+ var externalToolDefs = queryContract.externalToolTransport === "dynamic" ? externalQueryBridge.dynamicDefs() : [];
1937
1866
  if (externalToolDefs.length) { sessionToolDefs = sessionToolDefs.concat(externalToolDefs); queryToolSnapshots.set(session, { generation: queryGeneration, defs: sessionToolDefs.slice() }); }
1938
- var recoverCodexExternalTools = sessionAdapter.vendor === 'codex' && !!session.cliSessionId && (externalToolDefs.length > 0 || isMate);
1867
+ var recoverExternalTools = needsCatalogRecovery() && (externalToolDefs.length > 0 || isMate);
1939
1868
 
1940
1869
  // Give fresh queries an immediate provisional label. The visible user
1941
1870
  // message in history wins over `text`, which may contain an internal
@@ -1951,6 +1880,10 @@ function createSDKBridge(opts) {
1951
1880
  }
1952
1881
 
1953
1882
  var modelSwitchGeneration = session._queryGeneration;
1883
+ function queryRetired() {
1884
+ return Number(session._sdkQueryGeneration || 0) !== queryGeneration || session.taskStopRequested
1885
+ || session.destroying || responseRecovery.stopping(session) || (handle && session.queryInstance !== handle);
1886
+ }
1954
1887
  var queryOpts = {
1955
1888
  onModelSwitch: function(change) {
1956
1889
  if (session._queryGeneration !== modelSwitchGeneration || session.taskStopRequested) return;
@@ -1965,7 +1898,7 @@ function createSDKBridge(opts) {
1965
1898
  clayBridgeToken: clayBridgeToken,
1966
1899
  linuxUser: linuxUser || undefined,
1967
1900
  env: sessionRuntimeEnv,
1968
- skillOptions: getSessionSkillOptions(session),
1901
+ skillOptions: require("./session-skill-scope").bind(session, sm.sessions, getSessionSkillOptions(session), queryContextAvailable, usersModule.isMultiUser()),
1969
1902
  // appendSystemPrompt, NOT systemPrompt: a plain-string systemPrompt
1970
1903
  // REPLACES the entire Claude Code system prompt (sdk.d.ts:2096), which
1971
1904
  // would strip the driver session down to the two-sentence pair preset.
@@ -1973,7 +1906,7 @@ function createSDKBridge(opts) {
1973
1906
  // codex/kiro: prepend to the first message).
1974
1907
  appendSystemPrompt: [
1975
1908
  typeof opts.getSessionSystemPrompt === "function" ? opts.getSessionSystemPrompt(session) : "",
1976
- sessionAdapter.vendor === "codex" && queryContextAvailable() ? codexQuestionGuidance() : "",
1909
+ typeof queryContract.questionGuidance === "function" && queryContextAvailable() ? queryContract.questionGuidance() : "",
1977
1910
  ].filter(Boolean).join("\n\n") || undefined,
1978
1911
  model: queryModel,
1979
1912
  // Effort is remembered per vendor and clamped to the session vendor's
@@ -1990,10 +1923,22 @@ function createSDKBridge(opts) {
1990
1923
  toolServers: mergedMcpServers,
1991
1924
  toolServerDescriptors: extractMcpDescriptors(mergedMcpServers) || undefined,
1992
1925
  dynamicTools: sessionToolDefs,
1993
- // Codex persists dynamic tools with a thread. A stale authorized Driver
1926
+ // Conversation-scoped catalogs persist definitions. A stale authorized Driver
1994
1927
  // gets this exact-query bridge; its handlers still reauthorize live.
1995
- sessionMcpServer: (recoverCodexIssueTools || recoverCodexQuestionTools || recoverCodexBrowserTools || recoverCodexLinearTools || recoverCodexYouTools || recoverCodexExternalTools) ? getCodexSessionMcpServer(session, queryGeneration) : undefined,
1928
+ sessionMcpServer: (recoverMateSchedules || recoverIssueTools || recoverQuestionTools || recoverBrowserTools || recoverLinearTools || recoverYouTools || recoverExternalTools) ? getSessionMcpServer(session, queryGeneration) : undefined,
1929
+ onRecoveryAdmission: function(waiting) {
1930
+ if (!queryContextAvailable()) return;
1931
+ if (waiting) session._providerRecoveryWaiting = queryGeneration;
1932
+ else delete session._providerRecoveryWaiting;
1933
+ },
1934
+ onRecoveryWaiting: function(waiting) {
1935
+ if (!queryContextAvailable()) return;
1936
+ sendAndRecord(session, { type: 'informational', level: 'info', content: waiting
1937
+ ? 'Tool recovery is waiting for this conversation’s runtime to become safely available. Your message is retained; Stop cancels the wait.'
1938
+ : 'Tool recovery is ready. Continuing your retained message.' });
1939
+ },
1996
1940
  callDynamicTool: function (toolName, args) {
1941
+ if (queryRetired() || !queryContextAvailable()) return Promise.reject(new Error("This session tool query is no longer available."));
1997
1942
  var toolDef = findSessionToolDef(toolName);
1998
1943
  if (toolDef && typeof toolDef.handler === "function") {
1999
1944
  return toolDef.handler(args || {});
@@ -2003,7 +1948,10 @@ function createSDKBridge(opts) {
2003
1948
  resumeSessionId: session.cliSessionId || undefined,
2004
1949
  abortController: linuxUser ? undefined : session.abortController,
2005
1950
  canUseTool: function(toolName, input, toolOpts) {
2006
- var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
1951
+ if (queryRetired()) return { behavior: "deny", message: "This attempt has stopped." };
1952
+ responseRecovery.progress(session);
1953
+ var questionInput = yoke.userInput.toolQuestions(sessionAdapter, toolName, input);
1954
+ var nativeQuestionError = questionInput ? scheduleQuestionError(questionInput) : null;
2007
1955
  if (nativeQuestionError) return { behavior: "deny", message: nativeQuestionError };
2008
1956
  var permissionName = toolName;
2009
1957
  var permissionToolDef = findSessionToolDef(toolName);
@@ -2011,7 +1959,10 @@ function createSDKBridge(opts) {
2011
1959
  return handleCanUseTool(session, permissionName, input, toolOpts);
2012
1960
  },
2013
1961
  preToolUsePolicy: function(toolName, input) {
2014
- var nativeQuestionError = toolName === "AskUserQuestion" ? scheduleQuestionError(input) : null;
1962
+ if (queryRetired()) return Promise.resolve({ behavior: "deny", message: "This attempt has stopped." });
1963
+ responseRecovery.progress(session);
1964
+ var questionInput = yoke.userInput.toolQuestions(sessionAdapter, toolName, input);
1965
+ var nativeQuestionError = questionInput ? scheduleQuestionError(questionInput) : null;
2015
1966
  if (nativeQuestionError) return Promise.resolve({ behavior: "deny", message: nativeQuestionError });
2016
1967
  var permissionName = toolName;
2017
1968
  var permissionToolDef = findSessionToolDef(toolName);
@@ -2024,47 +1975,45 @@ function createSDKBridge(opts) {
2024
1975
  validateUserInputRequest: scheduleQuestionError,
2025
1976
  userInputMode: selectedUserInputMode,
2026
1977
  onElicitation: function(request, elicitOpts) {
1978
+ if (queryRetired()) return Promise.resolve({ action: "cancel" });
1979
+ responseRecovery.progress(session);
2027
1980
  return handleElicitation(session, request, elicitOpts);
2028
1981
  },
2029
1982
  onUserDialog: function(dialogRequest, dialogOpts) {
1983
+ if (queryRetired()) return Promise.resolve({ behavior: "cancelled" });
1984
+ responseRecovery.progress(session);
2030
1985
  return handleUserDialog(session, dialogRequest, dialogOpts);
2031
1986
  },
2032
1987
  callMcpTool: function(serverName, toolName, args) {
1988
+ if (queryRetired()) return Promise.reject(new Error("This attempt has stopped."));
1989
+ responseRecovery.progress(session);
2033
1990
  return callMcpToolHandler(mergedMcpServers, serverName, toolName, args);
2034
1991
  },
2035
- adapterOptions: {
2036
- CLAUDE: claudeOpts,
2037
- CODEX: {
2038
- // Always use "never" (auto-approve) because Clay handles tool
2039
- // permissions via its own UI (checkToolWhitelist + handleCanUseTool).
2040
- // Codex's native approval prompts are terminal-based and cannot be
2041
- // relayed through Clay's web UI, causing MCP tool calls to hang.
2042
- approvalPolicy: "never",
2043
- sandboxMode: session.youCurator ? "read-only" : codexConfig.sandbox,
2044
- webSearchMode: session.youCurator ? "disabled" : codexConfig.webSearch,
2045
- },
2046
- ACP: {
2047
- mcpServers: getAcpMcpServers(session),
2048
- },
2049
- ANTIGRAVITY: {
2050
- dangerouslySkipPermissions: dangerouslySkipPermissions
2051
- || session.permissionMode === "bypassPermissions",
2052
- },
2053
- KIRO: {
2054
- engine: kiroConfig.engine,
2055
- mode: kiroConfig.mode,
2056
- // ACP accepts additional stdio MCP servers per session. The bridge is
2057
- // bound to this Clay session so partner calls cannot leak across
2058
- // concurrent Kiro sessions or users.
2059
- mcpServers: getAcpMcpServers(session),
2060
- },
2061
- },
1992
+ adapterOptions: require("./yoke/query-options").map(sessionAdapter, executionIntent),
2062
1993
  };
2063
1994
 
1995
+ if (recoveryState) {
1996
+ if (!recoveryState.context) recoveryState.context = { appendSystemPrompt: queryOpts.appendSystemPrompt, model: queryOpts.model, effort: queryOpts.effort };
1997
+ Object.assign(queryOpts, recoveryState.context);
1998
+ }
2064
1999
  var handle;
2000
+ var startupHandle = session.queryInstance;
2065
2001
  console.log("[sdk-bridge] calling adapter.createQuery... vendor=" + sessionAdapter.vendor);
2066
2002
  try {
2067
2003
  handle = await sessionAdapter.createQuery(queryOpts);
2004
+ // A guarded delivery must still own the exact startup after provider
2005
+ // creation. Never replace a newer handle or publish to a deleted target.
2006
+ if (beforePush && beforePush.exactProductDelivery) {
2007
+ var startupCurrent = false;
2008
+ try { startupCurrent = sm.sessions.get(session.localId) === session && session.queryInstance === startupHandle && Number(session._sdkQueryGeneration || 0) === queryGeneration && beforePush() === true; } catch (guardError) {}
2009
+ if (!startupCurrent) {
2010
+ try { if (handle.abort) handle.abort(); } catch (abortError) {}
2011
+ try { if (handle.close) handle.close(); } catch (closeError) {}
2012
+ if (session.queryInstance === startupHandle) session.isProcessing = false;
2013
+ return false;
2014
+ }
2015
+ }
2016
+ if (recoveryState) recoveryState.handle = handle;
2068
2017
  console.log("[sdk-bridge] createQuery returned handle, vendor=" + sessionAdapter.vendor);
2069
2018
  // SDK accepted the explicit title. Provisional labels remain eligible
2070
2019
  // for semantic generation after the first Home exchange (or the normal
@@ -2084,6 +2033,7 @@ function createSDKBridge(opts) {
2084
2033
  console.error("[sdk-bridge] Failed to create query for session " + session.localId + ":", e.message || e);
2085
2034
  console.error("[sdk-bridge] cliSessionId:", session.cliSessionId, "resume:", !!session.cliSessionId);
2086
2035
  console.error("[sdk-bridge] Stack:", e.stack || "(no stack)");
2036
+ if (beforePush && beforePush.exactProductDelivery && (sm.sessions.get(session.localId) !== session || session.queryInstance !== startupHandle || Number(session._sdkQueryGeneration || 0) !== queryGeneration)) return false;
2087
2037
  session.isProcessing = false;
2088
2038
  session._awaitingTurnResult = false;
2089
2039
  session._queuedTurnCount = 0;
@@ -2118,11 +2068,9 @@ function createSDKBridge(opts) {
2118
2068
 
2119
2069
  // Store QueryHandle on session for iteration and control.
2120
2070
  session.queryInstance = handle;
2121
- // Capabilities come from the installed query handle, never a model-name
2122
- // table. `acceptEdits` and MCP overrides are Claude SDK semantics; a
2123
- // vendor may expose the generic setter without implementing either.
2124
- session.permissionCapabilities = require("./session-permission-mode").capabilities(session, sm.defaultVendor);
2125
- session.permissionCapabilities.mcpOverride = sessionAdapter.vendor === "claude" && typeof handle.setMcpPermissionModeOverride === "function";
2071
+ // An installed setter and the adapter's explicit protocol contract are
2072
+ // both required; generic handle stubs do not establish runtime support.
2073
+ session.permissionCapabilities = require("./session-permission-mode").capabilities(session, sm.defaultVendor, sessionAdapter);
2126
2074
  session.effectivePermissionMode = null;
2127
2075
  sm.saveSessionFile(session);
2128
2076
  sendToSession(session, { type: "config_state", model: session.model || defaultModelForVendor(session.vendor || "claude"), vendor: session.vendor || "claude", sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effectivePermissionMode: null, permissionCapabilities: session.permissionCapabilities, mcpPermissionModeOverrides: session.mcpPermissionModeOverrides || {}, effort: session.effort || sm.currentEffort || "medium", betas: sm.currentBetas || [] });
@@ -2138,6 +2086,7 @@ function createSDKBridge(opts) {
2138
2086
  }
2139
2087
 
2140
2088
  if (typeof beforePush === "function" && beforePush() !== true) {
2089
+ try { if (typeof handle.abort === "function") handle.abort(); } catch (e) {}
2141
2090
  try { handle.close(); } catch (e) {}
2142
2091
  if (session.queryInstance === handle) session.queryInstance = null;
2143
2092
  session.messageQueue = null;
@@ -2147,6 +2096,13 @@ function createSDKBridge(opts) {
2147
2096
  session._queuedTurnCount = 0;
2148
2097
  taskLifecycle.clearAll(session);
2149
2098
  onProcessingChanged();
2099
+ if (recoveryState && !responseRecovery.stopping(session)) {
2100
+ sendAndRecord(session, { type: "thinking_stop" });
2101
+ sendAndRecord(session, { type: session.taskStopRequested ? "info" : "error", text: session.taskStopRequested
2102
+ ? "Stopped before sending your message."
2103
+ : "This request is no longer eligible for delivery. Please review it and send it again." });
2104
+ sendAndRecord(session, { type: "done", code: session.taskStopRequested ? 0 : 1 });
2105
+ }
2150
2106
  sm.broadcastSessionList();
2151
2107
  return false;
2152
2108
  }
@@ -2198,32 +2154,27 @@ function createSDKBridge(opts) {
2198
2154
  }
2199
2155
 
2200
2156
  session.lastActivityAt = Date.now();
2201
- var codexCatalogCandidate = null;
2202
- if (sessionAdapter.vendor === "codex" && !resumedAtQueryStart) {
2157
+ var catalogCandidate = null;
2158
+ if (queryContract.catalogLifetime === "conversation" && typeof queryContract.recordCatalog === "function" && !resumedAtQueryStart) {
2203
2159
  var requiredCatalogTools = { begin_scheduled_task_interview: false, propose_scheduled_task: false, list_scheduled_tasks: false, read_scheduled_task: false, update_scheduled_task: false, report_scheduled_task_outcome: false, ask_user_questions: false };
2204
2160
  for (var catalogToolIndex = 0; catalogToolIndex < sessionToolDefs.length; catalogToolIndex++) {
2205
2161
  if (Object.prototype.hasOwnProperty.call(requiredCatalogTools, sessionToolDefs[catalogToolIndex].name)) requiredCatalogTools[sessionToolDefs[catalogToolIndex].name] = true;
2206
2162
  }
2207
2163
  var completeInterviewCatalog = requiredCatalogTools.begin_scheduled_task_interview && requiredCatalogTools.propose_scheduled_task && requiredCatalogTools.list_scheduled_tasks && requiredCatalogTools.read_scheduled_task && requiredCatalogTools.update_scheduled_task && requiredCatalogTools.ask_user_questions;
2208
2164
  if (completeInterviewCatalog || requiredCatalogTools.report_scheduled_task_outcome) {
2209
- codexCatalogCandidate = { generation: queryGeneration, version: yoke.sessionTools.CODEX_CATALOG_VERSION };
2165
+ catalogCandidate = { generation: queryGeneration, versions: { schedules: yoke.sessionTools.SCHEDULE_CATALOG_VERSION } };
2210
2166
  }
2211
- if (requiredCatalogTools.ask_user_questions) {
2212
- codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, { generation: queryGeneration, userInputVersion: 1 });
2213
- }
2214
- if (hasYouCatalog) codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, { generation: queryGeneration, youVersion: 1 });
2215
- if (hasLinearCatalog) codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, { generation: queryGeneration, linearVersion: 1 });
2216
- if (hasSharedBrowserCatalog) {
2217
- codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, { generation: queryGeneration, sharedBrowserVersion: 6 });
2218
- }
2219
- if (hasIssueCatalog) {
2220
- codexCatalogCandidate = Object.assign({}, codexCatalogCandidate || {}, {
2221
- generation: queryGeneration,
2222
- issuesVersion: 1,
2223
- });
2167
+ function markCatalog(family, version) {
2168
+ if (!catalogCandidate) catalogCandidate = { generation: queryGeneration, versions: {} };
2169
+ catalogCandidate.versions[family] = version;
2224
2170
  }
2171
+ if (requiredCatalogTools.ask_user_questions) markCatalog("questions", 1);
2172
+ if (hasYouCatalog) markCatalog("knowledge", 1);
2173
+ if (hasLinearCatalog) markCatalog("linear", 1);
2174
+ if (hasSharedBrowserCatalog) markCatalog("browser", 6);
2175
+ if (hasIssueCatalog) markCatalog("issues", 1);
2225
2176
  }
2226
- session.streamPromise = processQueryStream(session, codexCatalogCandidate).catch(function(err) {
2177
+ session.streamPromise = processQueryStream(session, catalogCandidate, queryContract).catch(function(err) {
2227
2178
  });
2228
2179
  return true;
2229
2180
  }
@@ -2235,6 +2186,19 @@ function createSDKBridge(opts) {
2235
2186
  // dropped messages in both gap windows, which is what made a session show
2236
2187
  // the processing indicator without ever answering until a second send.
2237
2188
  function pushMessage(session, text, images, deliveryMeta) {
2189
+ if (session._responseRecoveryStopPending && !responseRecovery.stopping(session)) return false;
2190
+ function trackDelivery() {
2191
+ responseRecovery.enqueue(session, {
2192
+ text: text, images: images, linuxUser: session.lastLinuxUser || null,
2193
+ });
2194
+ }
2195
+ if (responseRecovery.stopping(session)) {
2196
+ // Keep admission serialized while the old execution is terminating.
2197
+ session.pendingPush = session.pendingPush || [];
2198
+ session.pendingPush.push(Object.assign({ text: text, images: images || null }, deliveryMeta || {}));
2199
+ trackDelivery();
2200
+ return true;
2201
+ }
2238
2202
  session.lastActivityAt = Date.now();
2239
2203
  if (session._runtimeRefreshRequested && !session._queryStarting) {
2240
2204
  session._runtimeRefreshRequested = false;
@@ -2261,6 +2225,8 @@ function createSDKBridge(opts) {
2261
2225
  console.error("[sdk-bridge] QueryHandle rejected message for session " + session.localId + ":", e.message || e);
2262
2226
  }
2263
2227
  if (delivered) {
2228
+ require('./sdk-foreground-admission').take(session);
2229
+ trackDelivery();
2264
2230
  delete session._lastTurnInterrupted;
2265
2231
  if (session._awaitingTurnResult) {
2266
2232
  session._queuedTurnCount = (session._queuedTurnCount || 0) + 1;
@@ -2285,6 +2251,7 @@ function createSDKBridge(opts) {
2285
2251
  if (session._queryStarting) {
2286
2252
  session.pendingPush = session.pendingPush || [];
2287
2253
  session.pendingPush.push(Object.assign({ text: text, images: images || null }, deliveryMeta || {}));
2254
+ trackDelivery();
2288
2255
  return true;
2289
2256
  }
2290
2257
  return false;
@@ -2365,65 +2332,8 @@ function createSDKBridge(opts) {
2365
2332
  // Detect which vendor binaries are installed for this user.
2366
2333
  // In multi-user mode, runs checks as the specific Linux user.
2367
2334
  function detectInstalledVendors(linuxUser) {
2368
- var execFileSync = require("child_process").execFileSync;
2369
- var fs = require("fs");
2370
- var result = [];
2371
-
2372
- function tryLookup(name) {
2373
- try {
2374
- if (linuxUser) {
2375
- execFileSync("su", ["-", linuxUser, "-c", "which " + name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
2376
- } else {
2377
- if (process.platform === "win32") execFileSync("where", [name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
2378
- else execFileSync("which", [name], { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
2379
- }
2380
- return true;
2381
- } catch (e) {
2382
- return false;
2383
- }
2384
- }
2385
-
2386
- // Claude: check if binary is in PATH
2387
- if (tryLookup(yoke.getVendorInfo("claude").binaryName)) result.push("claude");
2388
-
2389
- // Codex: check bundled binary or PATH
2390
- var codexBin = null;
2391
- try {
2392
- codexBin = require("./yoke/codex-app-server").findCodexPath();
2393
- } catch (e) {}
2394
- if ((codexBin && fs.existsSync(codexBin)) || tryLookup(yoke.getVendorInfo("codex").binaryName)) result.push("codex");
2395
-
2396
- var subprocessVendorKeys = ["antigravity", "opencode", "kimi", "grok", "copilot", "qwen", "junie"];
2397
- var acpProfiles = require("./yoke/acp-agent-profiles");
2398
- for (var subprocessVendorIndex = 0; subprocessVendorIndex < subprocessVendorKeys.length; subprocessVendorIndex++) {
2399
- var subprocessVendor = subprocessVendorKeys[subprocessVendorIndex];
2400
- var subprocessInfo = yoke.getVendorInfo(subprocessVendor);
2401
- if (!linuxUser || subprocessInfo.osUserIsolation) {
2402
- if (subprocessVendor === "antigravity") {
2403
- if (tryLookup(subprocessInfo.binaryName)) result.push(subprocessVendor);
2404
- } else {
2405
- var acpProfile = acpProfiles.getAcpAgentProfile(subprocessVendor);
2406
- if (acpProfiles.findAcpAgentPath(acpProfile)) result.push(subprocessVendor);
2407
- }
2408
- } else {
2409
- console.log("[sdk-bridge] " + subprocessInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
2410
- }
2411
- }
2412
-
2413
- // Kiro has no per-user ACP spawn path yet. Do not advertise the daemon's
2414
- // binary to an OS-isolated user, even if that user also has Kiro installed.
2415
- var kiroInfo = yoke.getVendorInfo("kiro");
2416
- if (!linuxUser || kiroInfo.osUserIsolation) {
2417
- var kiroBin = null;
2418
- try {
2419
- kiroBin = require("./yoke/kiro-acp-server").findKiroPath();
2420
- } catch (e) {}
2421
- if ((kiroBin && fs.existsSync(kiroBin)) || tryLookup(kiroInfo.binaryName)) result.push("kiro");
2422
- } else {
2423
- console.log("[sdk-bridge] " + kiroInfo.displayName + " hidden for OS-isolated user " + linuxUser + ": per-user spawning is not implemented");
2424
- }
2425
-
2426
- return result;
2335
+ var installed = require("./yoke/discovery").installed({ user: linuxUser });
2336
+ return Object.keys(installed).filter(function(vendor) { return installed[vendor]; });
2427
2337
  }
2428
2338
 
2429
2339
  function getAvailableVendors(linuxUser) {
@@ -2495,7 +2405,20 @@ function createSDKBridge(opts) {
2495
2405
  sendModelInfoForVendor(null, defaultVendor, defaultModelForVendor(defaultVendor));
2496
2406
  }
2497
2407
 
2498
- async function setModel(session, model) {
2408
+ async function setModel(session, model, options) {
2409
+ if (options && typeof options.revalidate === "function") {
2410
+ var vendor = session.vendor || sm.defaultVendor || "claude";
2411
+ return require("./session-config-transition").applySessionConfig({
2412
+ sm: sm, session: session, revalidate: options.revalidate, method: "setModel", nativeValue: model, changes: { model: model },
2413
+ publish: function () {
2414
+ sm.defaultModelByVendor = sm.defaultModelByVendor || {};
2415
+ sm.defaultModelByVendor[vendor] = model;
2416
+ sm.broadcastSessionList();
2417
+ sendModelInfoForVendor(session, vendor, model);
2418
+ 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 || [] });
2419
+ },
2420
+ });
2421
+ }
2499
2422
  // Normalize to string id in case a { value, displayName } object slips in
2500
2423
  if (model && typeof model !== "string") {
2501
2424
  model = modelEntryValue(model);
@@ -2531,7 +2454,22 @@ function createSDKBridge(opts) {
2531
2454
  }
2532
2455
  }
2533
2456
 
2534
- async function setEffort(session, effort) {
2457
+ async function setEffort(session, effort, options) {
2458
+ if (options && typeof options.revalidate === "function") {
2459
+ var vendor = session.vendor || sm.defaultVendor || "claude";
2460
+ var value = yoke.clampEffort(vendor, effort);
2461
+ if (!value || value !== effort) throw new Error("Unsupported reasoning effort.");
2462
+ return require("./session-config-transition").applySessionConfig({
2463
+ sm: sm, session: session, revalidate: options.revalidate, method: "setEffort", nativeValue: value, changes: { effort: value },
2464
+ publish: function () {
2465
+ sm.currentEffortByVendor = sm.currentEffortByVendor || {};
2466
+ sm.currentEffortByVendor[vendor] = value;
2467
+ sm.currentEffort = value;
2468
+ sm.broadcastSessionList();
2469
+ sendToSession(session, { type: "config_state", sessionId: session.localId, vendor: vendor, effort: value, model: session.model || "", mode: session.permissionMode || sm.currentPermissionMode || "default", betas: sm.currentBetas || [] });
2470
+ },
2471
+ });
2472
+ }
2535
2473
  // Remember the pick per vendor (clamped) so vendor switches don't leak
2536
2474
  // unsupported levels across sessions; sm.currentEffort stays as the
2537
2475
  // display value for the session the user just configured.
@@ -2652,16 +2590,7 @@ function createSDKBridge(opts) {
2652
2590
  message: "Read-only access. You cannot make changes via @mention.",
2653
2591
  });
2654
2592
  },
2655
- adapterOptions: {
2656
- CLAUDE: {
2657
- settingSources: ["user"],
2658
- includePartialMessages: true,
2659
- // Explicit, not omitted: SDK 0.3.286+ leaves an omitted
2660
- // permissionMode to Claude Code's own default/auto behavior,
2661
- // which could bypass the deny-by-default canUseTool above.
2662
- permissionMode: "default",
2663
- },
2664
- },
2593
+ adapterOptions: require("./yoke/query-options").map(mentionAdapter, { instructionSources: ["user"], readOnly: true }),
2665
2594
  });
2666
2595
  } catch (e) {
2667
2596
  opts.onError("Failed to create mention query: " + (e.message || e));
@@ -2800,12 +2729,14 @@ function createSDKBridge(opts) {
2800
2729
  startQuery: startQuery,
2801
2730
  startQueryWithAcceptance: startQueryWithAcceptance,
2802
2731
  pushMessage: pushMessage,
2732
+ cancelResponseRecovery: responseRecovery.cancel,
2803
2733
  refreshSessionRuntime: refreshSessionRuntime,
2804
2734
  refreshEnvironmentRuntime: refreshEnvironmentRuntime,
2805
2735
  setModel: setModel,
2806
2736
  setEffort: setEffort,
2807
2737
  setPermissionMode: setPermissionMode,
2808
- isClaudeProcess: isClaudeProcess,
2738
+ isProviderProcess: isProviderProcess,
2739
+ isClaudeProcess: isProviderProcess, // Compatibility export; production uses the provider contract.
2809
2740
  permissionPushTitle: permissionPushTitle,
2810
2741
  permissionPushBody: permissionPushBody,
2811
2742
  warmup: warmup,