clay-server 4.0.0-beta.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +255 -0
  3. package/lib/capsule-mate-turn.js +105 -0
  4. package/lib/capsule-pig-logic.js +300 -0
  5. package/lib/capsule-server-runtimes.js +39 -0
  6. package/lib/capsules/pig/display.js +198 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +84 -0
  9. package/lib/config.js +25 -1
  10. package/lib/daemon-projects.js +13 -8
  11. package/lib/daemon.js +127 -14
  12. package/lib/git-cli.js +95 -1
  13. package/lib/knowledge-import.js +436 -0
  14. package/lib/knowledge-record-store.js +188 -0
  15. package/lib/knowledge-search.js +240 -0
  16. package/lib/mate-knowledge-mcp-server.js +151 -0
  17. package/lib/mate-knowledge-migration.js +283 -0
  18. package/lib/mate-knowledge-service.js +488 -0
  19. package/lib/mate-knowledge-sync.js +482 -0
  20. package/lib/migrate-single-user.js +2 -0
  21. package/lib/notes-lifecycle.js +149 -0
  22. package/lib/notes.js +60 -3
  23. package/lib/os-users.js +3 -1
  24. package/lib/project-capsule-catalog.js +9 -1
  25. package/lib/project-capsule-turn.js +96 -0
  26. package/lib/project-connection.js +15 -1
  27. package/lib/project-file-history.js +223 -0
  28. package/lib/project-file-watch.js +77 -49
  29. package/lib/project-filesystem.js +31 -220
  30. package/lib/project-http.js +15 -1
  31. package/lib/project-knowledge.js +22 -0
  32. package/lib/project-log-context.js +172 -0
  33. package/lib/project-log-feedback-delivery.js +118 -0
  34. package/lib/project-logs-comments.js +137 -0
  35. package/lib/project-logs-context-state.js +42 -0
  36. package/lib/project-logs-mcp-server.js +328 -0
  37. package/lib/project-logs-query.js +185 -0
  38. package/lib/project-logs-root.js +75 -0
  39. package/lib/project-logs-schema.js +259 -0
  40. package/lib/project-logs-service.js +463 -0
  41. package/lib/project-logs-snapshot.js +205 -0
  42. package/lib/project-logs-store.js +495 -0
  43. package/lib/project-logs-versioning.js +132 -0
  44. package/lib/project-logs.js +400 -0
  45. package/lib/project-mate-interaction.js +14 -0
  46. package/lib/project-mate-knowledge.js +110 -0
  47. package/lib/project-memory.js +22 -0
  48. package/lib/project-message-delivery.js +70 -0
  49. package/lib/project-pair-lifecycle.js +460 -0
  50. package/lib/project-request-access.js +66 -0
  51. package/lib/project-session-handoff.js +1 -1
  52. package/lib/project-session-notes.js +64 -13
  53. package/lib/project-session-pair.js +159 -146
  54. package/lib/project-session-spawn.js +1 -1
  55. package/lib/project-sessions.js +58 -13
  56. package/lib/project-user-message.js +53 -4
  57. package/lib/project-worker-permission.js +430 -0
  58. package/lib/project-worker-proposal.js +208 -62
  59. package/lib/project.js +185 -6
  60. package/lib/public/app.js +63 -35
  61. package/lib/public/css/admin.css +2 -2
  62. package/lib/public/css/capsule-ui.css +38 -0
  63. package/lib/public/css/command-palette.css +19 -9
  64. package/lib/public/css/git-placard.css +154 -0
  65. package/lib/public/css/home-hub.css +4 -0
  66. package/lib/public/css/home-session-actions.css +66 -0
  67. package/lib/public/css/home-sidebar.css +66 -0
  68. package/lib/public/css/input.css +84 -3
  69. package/lib/public/css/messages.css +40 -1
  70. package/lib/public/css/mobile-nav.css +80 -0
  71. package/lib/public/css/notifications-center.css +67 -0
  72. package/lib/public/css/pane.css +97 -10
  73. package/lib/public/css/project-logs.css +198 -0
  74. package/lib/public/css/scheduler.css +1 -1
  75. package/lib/public/css/session-actions.css +16 -101
  76. package/lib/public/css/sidebar.css +103 -0
  77. package/lib/public/css/sticky-notes.css +237 -229
  78. package/lib/public/css/worker-proposal.css +36 -3
  79. package/lib/public/index.html +43 -11
  80. package/lib/public/modules/admin.js +8 -8
  81. package/lib/public/modules/app-connection.js +124 -6
  82. package/lib/public/modules/app-home-hub.js +12 -8
  83. package/lib/public/modules/app-message-cards.js +2 -1
  84. package/lib/public/modules/app-messages.js +92 -11
  85. package/lib/public/modules/app-misc.js +2 -14
  86. package/lib/public/modules/app-notifications.js +11 -7
  87. package/lib/public/modules/app-panels.js +3 -2
  88. package/lib/public/modules/app-projects.js +56 -18
  89. package/lib/public/modules/app-rendering.js +1 -0
  90. package/lib/public/modules/capsule-preference.js +44 -0
  91. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  92. package/lib/public/modules/clay-log-links.js +70 -0
  93. package/lib/public/modules/command-palette.js +52 -15
  94. package/lib/public/modules/context-sources.js +0 -3
  95. package/lib/public/modules/git-agent-sessions.js +85 -0
  96. package/lib/public/modules/git-panel.js +111 -84
  97. package/lib/public/modules/git-placard.js +161 -0
  98. package/lib/public/modules/home-chat-empty-state.js +39 -0
  99. package/lib/public/modules/home-chat-identity.js +9 -0
  100. package/lib/public/modules/home-conversations-sheet.js +102 -48
  101. package/lib/public/modules/home-mate-chat.js +15 -35
  102. package/lib/public/modules/home-session-actions.js +6 -0
  103. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  104. package/lib/public/modules/home-sidebar.js +7 -0
  105. package/lib/public/modules/home-surface-boot.js +7 -3
  106. package/lib/public/modules/home-tool-frame.js +182 -0
  107. package/lib/public/modules/home-tools.js +138 -1
  108. package/lib/public/modules/input.js +38 -7
  109. package/lib/public/modules/markdown.js +2 -0
  110. package/lib/public/modules/mate-sidebar.js +0 -8
  111. package/lib/public/modules/message-delivery.js +97 -0
  112. package/lib/public/modules/paste-modal.js +84 -0
  113. package/lib/public/modules/project-activation.js +45 -0
  114. package/lib/public/modules/project-logs-render.js +405 -0
  115. package/lib/public/modules/project-logs.js +461 -0
  116. package/lib/public/modules/project-removal-target.js +26 -0
  117. package/lib/public/modules/scheduler.js +17 -3
  118. package/lib/public/modules/session-actions.js +29 -105
  119. package/lib/public/modules/session-hierarchy.js +54 -0
  120. package/lib/public/modules/sidebar-mobile.js +40 -15
  121. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  122. package/lib/public/modules/sidebar-sessions.js +77 -11
  123. package/lib/public/modules/sidebar.js +17 -5
  124. package/lib/public/modules/split-group-helpers.js +8 -0
  125. package/lib/public/modules/split-pair-ui.js +15 -4
  126. package/lib/public/modules/split-view.js +5 -2
  127. package/lib/public/modules/split-worker-runtime.js +24 -0
  128. package/lib/public/modules/sticky-note-markdown.js +5 -0
  129. package/lib/public/modules/sticky-notes-browser.js +326 -0
  130. package/lib/public/modules/sticky-notes-card.js +360 -0
  131. package/lib/public/modules/sticky-notes-editor.js +291 -0
  132. package/lib/public/modules/sticky-notes-shared.js +90 -0
  133. package/lib/public/modules/sticky-notes.js +170 -946
  134. package/lib/public/modules/thinking-lifecycle.js +175 -0
  135. package/lib/public/modules/thinking-summary.js +28 -0
  136. package/lib/public/modules/thinking-view.js +75 -0
  137. package/lib/public/modules/tool-palette-order.js +143 -0
  138. package/lib/public/modules/tool-palette-overlays.js +159 -0
  139. package/lib/public/modules/tool-palette.js +21 -188
  140. package/lib/public/modules/tools.js +31 -123
  141. package/lib/public/modules/update-snooze.js +169 -0
  142. package/lib/public/modules/user-settings.js +4 -0
  143. package/lib/public/modules/worker-pane-lock.js +176 -0
  144. package/lib/public/modules/worker-proposal-state.js +16 -0
  145. package/lib/public/modules/worker-proposal.js +49 -20
  146. package/lib/public/style.css +3 -1
  147. package/lib/sdk-bridge.js +135 -46
  148. package/lib/sdk-message-processor.js +36 -19
  149. package/lib/server-experimental-settings.js +49 -0
  150. package/lib/server-global-ws.js +11 -0
  151. package/lib/server-home-chat-events.js +31 -1
  152. package/lib/server-home-chat.js +27 -4
  153. package/lib/server-home-clay-entry.js +1 -1
  154. package/lib/server-home-clay-session-links.js +34 -5
  155. package/lib/server-settings.js +5 -0
  156. package/lib/server-tools.js +127 -9
  157. package/lib/server.js +137 -22
  158. package/lib/session-driver-eligibility.js +38 -0
  159. package/lib/session-driver-orchestration.js +31 -0
  160. package/lib/session-hygiene.js +3 -0
  161. package/lib/session-notes-mcp-server.js +25 -5
  162. package/lib/session-pair-factory.js +247 -0
  163. package/lib/session-pair-mcp-server.js +46 -8
  164. package/lib/session-pair-prompts.js +79 -0
  165. package/lib/session-pair-turn-control.js +153 -0
  166. package/lib/session-provenance.js +119 -0
  167. package/lib/session-spawn-mcp-server.js +1 -1
  168. package/lib/session-split-groups.js +4 -1
  169. package/lib/session-title-policy.js +60 -0
  170. package/lib/session-visibility.js +41 -0
  171. package/lib/sessions.js +57 -10
  172. package/lib/tools-registry.js +89 -10
  173. package/lib/update-snooze.js +437 -0
  174. package/lib/users-experimental-preferences.js +30 -0
  175. package/lib/users-permissions.js +6 -6
  176. package/lib/users.js +22 -16
  177. package/lib/workspace-query-access.js +102 -0
  178. package/lib/workspace-query-service.js +24 -17
  179. package/lib/worktree.js +25 -38
  180. package/lib/ws-schema.js +38 -7
  181. package/lib/yoke/adapters/claude-worker.js +16 -6
  182. package/lib/yoke/adapters/claude.js +12 -0
  183. package/lib/yoke/adapters/codex.js +24 -14
  184. package/lib/yoke/vendor-registry.js +1 -1
  185. package/package.json +2 -2
@@ -1,48 +1,34 @@
1
1
  var crypto = require("crypto");
2
2
  var yoke = require("./yoke");
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
-
4
+ var driverEligibility = require("./session-driver-eligibility");
5
+ var driverOrchestration = require("./session-driver-orchestration");
5
6
  var MAX_SUMMARY_CHARS = 600;
6
7
  var MAX_PLAN_CHARS = 6000;
7
8
  var MAX_TASK_CHARS = 30000;
8
-
9
+ var MAX_RATIONALE_CHARS = 1000;
9
10
  function modelValue(entry) {
10
11
  if (typeof entry === "string") return entry;
11
12
  return entry && (entry.value || entry.id) || "";
12
13
  }
13
-
14
14
  function modelLabel(entry) {
15
15
  if (typeof entry === "string") return entry;
16
16
  return entry && (entry.displayName || entry.name || entry.value || entry.id) || "";
17
17
  }
18
-
19
- function isFableSession(session, modelsByVendor, fallbackModel) {
20
- if (!session || (session.vendor || "claude") !== "claude") return false;
21
- var effectiveModel = session.model || fallbackModel || "";
22
- var selected = String(effectiveModel).toLowerCase();
23
- if (selected.indexOf("fable") !== -1) return true;
24
- var models = (modelsByVendor && modelsByVendor.claude) || [];
25
- for (var i = 0; i < models.length; i++) {
26
- if (modelValue(models[i]) !== effectiveModel) continue;
27
- if (modelLabel(models[i]).toLowerCase().indexOf("fable") !== -1) return true;
28
- }
29
- return false;
30
- }
31
-
32
18
  function toolResult(value) {
33
19
  return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
34
20
  }
35
-
36
21
  function attachWorkerProposal(ctx) {
37
22
  var sm = ctx.sm;
38
23
  var store = ctx.splitStore;
24
+ function isLiveSession(session) {
25
+ return !!session && sm.sessions.get(session.localId) === session;
26
+ }
39
27
 
28
+ function canOffer(session) { return !ctx.isMate && isLiveSession(session) && driverEligibility.isEligibleDriverSession(session, sm); }
40
29
  function isEligible(session) {
41
- if (ctx.isMate || !session || session.mode === "tui") return false;
42
- if (store.groupForMember(session.localId)) return false;
43
- return isFableSession(session, sm.modelsByVendor, (sm.defaultModelByVendor || {})[session.vendor || "claude"]);
30
+ return canOffer(session) && !store.groupForMember(session.localId);
44
31
  }
45
-
46
32
  function safeModelsByVendor(installed) {
47
33
  var result = {};
48
34
  for (var i = 0; i < installed.length; i++) {
@@ -95,7 +81,21 @@ function attachWorkerProposal(ctx) {
95
81
  for (var i = 0; i < models.length; i++) {
96
82
  if (modelValue(models[i]) === model) return true;
97
83
  }
98
- return models.length === 0;
84
+ return false;
85
+ }
86
+
87
+ function effortIsAvailable(options, vendor, model, effort) {
88
+ var capabilities = options.capabilitiesByVendor[vendor] || {};
89
+ if (capabilities.effort === false) return !effort;
90
+ var clamped = yoke.clampEffort(vendor, effort) || "";
91
+ if (!effort || clamped !== effort) return false;
92
+ var models = options.modelsByVendor[vendor] || [];
93
+ for (var i = 0; i < models.length; i++) {
94
+ if (modelValue(models[i]) !== model) continue;
95
+ var levels = models[i] && models[i].supportedEffortLevels;
96
+ return !Array.isArray(levels) || levels.length === 0 || levels.indexOf(effort) !== -1;
97
+ }
98
+ return true;
99
99
  }
100
100
 
101
101
  function chooseRecommendation(args, session, options) {
@@ -117,7 +117,6 @@ function attachWorkerProposal(ctx) {
117
117
  if (!model && models.length > 0) {
118
118
  for (var j = 0; j < models.length; j++) {
119
119
  if (vendor === session.vendor && modelValue(models[j]) === currentModel) continue;
120
- if (modelLabel(models[j]).toLowerCase().indexOf("fable") !== -1) continue;
121
120
  model = modelValue(models[j]);
122
121
  break;
123
122
  }
@@ -127,6 +126,31 @@ function attachWorkerProposal(ctx) {
127
126
  return { vendor: vendor, model: model, effort: effort };
128
127
  }
129
128
 
129
+ function skipPermissionsEnabled(session) {
130
+ return !!(session && (session.permissionMode === "bypassPermissions" ||
131
+ session.dangerouslySkipPermissions || ctx.dangerouslySkipPermissions));
132
+ }
133
+
134
+ function recommendationCanAutoAccept(args, recommendation, options) {
135
+ var requestedVendor = typeof args.recommendedVendor === "string" ? args.recommendedVendor.trim() : "";
136
+ var requestedModel = typeof args.recommendedModel === "string" ? args.recommendedModel.trim() : "";
137
+ var requestedEffort = typeof args.recommendedEffort === "string" ? args.recommendedEffort.trim() : "";
138
+ if (!requestedVendor || requestedVendor !== recommendation.vendor) return false;
139
+ if (!requestedModel || requestedModel !== recommendation.model ||
140
+ !modelIsAvailable(options, requestedVendor, requestedModel)) return false;
141
+ var capabilities = options.capabilitiesByVendor[requestedVendor] || {};
142
+ if (capabilities.effort === false) return !requestedEffort && !recommendation.effort;
143
+ return requestedEffort === recommendation.effort &&
144
+ effortIsAvailable(options, requestedVendor, requestedModel, requestedEffort);
145
+ }
146
+
147
+ function autoAcceptanceWs(session) {
148
+ return {
149
+ _clayActiveSession: session.localId,
150
+ _clayUser: session.ownerId ? { id: session.ownerId } : null,
151
+ };
152
+ }
153
+
130
154
  function findProposal(session, proposalId) {
131
155
  var history = (session && session.history) || [];
132
156
  for (var i = history.length - 1; i >= 0; i--) {
@@ -154,31 +178,24 @@ function attachWorkerProposal(ctx) {
154
178
  }, patch));
155
179
  }
156
180
 
157
- function skipPermissionsEnabled(session) {
158
- return !!session && (session.permissionMode === "bypassPermissions" || session.dangerouslySkipPermissions === true);
159
- }
160
-
161
- function autoApprovalWs(session) {
162
- return {
163
- _clayActiveSession: session.localId,
164
- _clayUser: session.ownerId ? { id: session.ownerId } : null,
165
- _autoApproval: true,
166
- };
167
- }
168
-
169
181
  async function propose(args, session) {
170
- if (!isEligible(session)) return toolResult({ error: "Split Worker suggestions are only available in an unpaired Fable session." });
182
+ if (!isEligible(session)) return toolResult({ error: "Split Worker proposals are only available in an eligible unpaired Driver session." });
171
183
  if (hasPendingProposal(session)) return toolResult({ error: "A Split Worker suggestion is already awaiting a decision." });
172
184
  var summary = typeof args.summary === "string" ? args.summary.trim() : "";
173
185
  var plan = typeof args.plan === "string" ? args.plan.trim() : "";
174
186
  var task = typeof args.message === "string" ? args.message.trim() : "";
175
- if (!summary || !plan || !task) return toolResult({ error: "summary, plan, and message are required." });
187
+ var rationale = typeof args.recommendationRationale === "string" ? args.recommendationRationale.trim() : "";
188
+ if (!summary || !plan || !task || !rationale) {
189
+ return toolResult({ error: "summary, plan, message, and recommendationRationale are required." });
190
+ }
176
191
  if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
177
192
  await ensureModelCatalogs();
193
+ if (!isEligible(session)) {
194
+ return toolResult({ error: "The Driver session changed while Split Worker runtimes were loading." });
195
+ }
178
196
  var options = proposalOptions();
179
197
  if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
180
198
  var recommendation = chooseRecommendation(args, session, options);
181
- var autoApprove = skipPermissionsEnabled(session);
182
199
  var proposal = {
183
200
  type: "worker_proposal",
184
201
  proposalId: "worker_" + crypto.randomUUID(),
@@ -189,22 +206,22 @@ function attachWorkerProposal(ctx) {
189
206
  recommendedVendor: recommendation.vendor,
190
207
  recommendedModel: recommendation.model,
191
208
  recommendedEffort: recommendation.effort,
209
+ recommendationRationale: rationale.slice(0, MAX_RATIONALE_CHARS),
192
210
  options: options,
193
211
  };
194
- if (autoApprove) proposal.autoApproved = true;
195
212
  sm.sendAndRecord(session, proposal);
196
- if (autoApprove) {
213
+ if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(args, recommendation, options)) {
197
214
  var accepted = await acceptProposal(session, proposal, {
198
215
  vendor: recommendation.vendor,
199
216
  model: recommendation.model,
200
217
  effort: recommendation.effort,
201
- autoApproved: true,
202
- }, autoApprovalWs(session));
203
- if (!accepted.ok) return toolResult({ error: accepted.error || "Could not start the Split Worker." });
218
+ }, autoAcceptanceWs(session), true);
204
219
  return toolResult({
205
- status: "running",
220
+ status: accepted.ok ? "auto_accepted" : "posted",
206
221
  proposalId: proposal.proposalId,
207
- instruction: "The Split Worker was auto-approved and started because skip permissions is enabled. Its result will return for review.",
222
+ instruction: accepted.ok
223
+ ? "The recorded Split Worker configuration was auto-accepted under the Driver session's full-access mode. End this turn now while the exact paired Worker runs."
224
+ : "The automatic decision failed closed. The configuration card remains pending for the user; end this turn and wait for their decision.",
208
225
  });
209
226
  }
210
227
  return toolResult({
@@ -214,6 +231,75 @@ function attachWorkerProposal(ctx) {
214
231
  });
215
232
  }
216
233
 
234
+ async function proposeReplacement(args, session) {
235
+ var group = session && store.groupForMember(session.localId);
236
+ if (ctx.isMate || !isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) || !group ||
237
+ !group.pair || group.pair.driverId !== session.localId) {
238
+ return toolResult({ error: "Split Worker replacement proposals require the exact paired Driver session." });
239
+ }
240
+ var sourceGroupId = group.id;
241
+ var sourceWorkerId = group.pair.workerId;
242
+ if (hasPendingProposal(session)) return toolResult({ error: "A Split Worker proposal is already awaiting a decision." });
243
+ var task = typeof args.message === "string" ? args.message.trim() : "";
244
+ var rationale = typeof args.recommendationRationale === "string" ? args.recommendationRationale.trim() : "";
245
+ if (!task) return toolResult({ error: "message is required so an accepted replacement delegates exactly once." });
246
+ if (!rationale) return toolResult({ error: "recommendationRationale is required for the replacement audit trail." });
247
+ if (task.length > MAX_TASK_CHARS) return toolResult({ error: "The Split Worker task is too long." });
248
+ await ensureModelCatalogs();
249
+ var liveGroup = store.groupForMember(session.localId);
250
+ if (!isLiveSession(session) || !driverEligibility.isEligibleDriverSession(session, sm) ||
251
+ !liveGroup || liveGroup.id !== sourceGroupId || !liveGroup.pair ||
252
+ liveGroup.pair.driverId !== session.localId || liveGroup.pair.workerId !== sourceWorkerId) {
253
+ return toolResult({ error: "The Driver/Split Worker pair changed while replacement runtimes were loading." });
254
+ }
255
+ var options = proposalOptions();
256
+ if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Split Worker session." });
257
+ var recommendationArgs = {
258
+ recommendedVendor: args.workerVendor,
259
+ recommendedModel: args.workerModel,
260
+ recommendedEffort: args.workerEffort,
261
+ };
262
+ var recommendation = chooseRecommendation(recommendationArgs, session, options);
263
+ var proposal = {
264
+ type: "worker_proposal",
265
+ proposalId: "worker_" + crypto.randomUUID(),
266
+ action: "replace",
267
+ summary: "Replace the current Split Worker before the next delegated task.",
268
+ plan: "1. Preserve the current Worker's session and history\n2. Create the selected replacement runtime\n3. Delegate the next task exactly once",
269
+ message: task,
270
+ status: "pending",
271
+ recommendedVendor: recommendation.vendor,
272
+ recommendedModel: recommendation.model,
273
+ recommendedEffort: recommendation.effort,
274
+ recommendationRationale: rationale.slice(0, MAX_RATIONALE_CHARS),
275
+ options: options,
276
+ sourceGroupId: sourceGroupId,
277
+ sourceWorkerId: sourceWorkerId,
278
+ interrupt: args.interrupt === true,
279
+ evaluation: args.evaluation || null,
280
+ };
281
+ sm.sendAndRecord(session, proposal);
282
+ if (skipPermissionsEnabled(session) && recommendationCanAutoAccept(recommendationArgs, recommendation, options)) {
283
+ var accepted = await acceptProposal(session, proposal, {
284
+ vendor: recommendation.vendor,
285
+ model: recommendation.model,
286
+ effort: recommendation.effort,
287
+ }, autoAcceptanceWs(session), true);
288
+ return toolResult({
289
+ status: accepted.ok ? "auto_accepted" : "posted",
290
+ proposalId: proposal.proposalId,
291
+ instruction: accepted.ok
292
+ ? "The recorded replacement configuration was auto-accepted under the Driver session's full-access mode."
293
+ : "The automatic replacement failed closed. The card remains pending for the user.",
294
+ });
295
+ }
296
+ return toolResult({
297
+ status: "posted",
298
+ proposalId: proposal.proposalId,
299
+ instruction: "The replacement configuration is visible in the chat. End this turn now and wait for the user's decision.",
300
+ });
301
+ }
302
+
217
303
  function resumeDriver(session, text) {
218
304
  var sdk = ctx.getSdk();
219
305
  if (!sdk) return Promise.reject(new Error("SDK bridge is not ready"));
@@ -268,31 +354,86 @@ function attachWorkerProposal(ctx) {
268
354
  return session;
269
355
  }
270
356
 
271
- async function acceptProposal(session, proposal, msg, ws) {
272
- var options = proposal.options || proposalOptions();
357
+ async function acceptProposal(session, proposal, msg, ws, trustedAutoAcceptance) {
358
+ if (!isLiveSession(session)) throw new Error("The Driver session is no longer live");
359
+ if (proposal.action !== "replace" && !isEligible(session)) {
360
+ throw new Error("The Driver is no longer eligible to create this Split Worker");
361
+ }
362
+ var approvedGroup = null;
363
+ if (proposal.action === "replace") {
364
+ approvedGroup = store.groupForMember(session.localId);
365
+ if (!driverEligibility.isEligibleDriverSession(session, sm) || !approvedGroup ||
366
+ approvedGroup.id !== proposal.sourceGroupId || !approvedGroup.pair ||
367
+ approvedGroup.pair.driverId !== session.localId || approvedGroup.pair.workerId !== proposal.sourceWorkerId) {
368
+ throw new Error("The Driver/Split Worker pair changed before the replacement was approved");
369
+ }
370
+ }
371
+ var options = proposalOptions();
273
372
  var vendor = msg.vendor || proposal.recommendedVendor;
274
373
  var model = msg.model || "";
275
374
  if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Split Worker vendor is not installed");
276
375
  if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Split Worker model is unavailable");
277
- var effort = yoke.clampEffort(vendor, msg.effort || proposal.recommendedEffort || "medium") || "";
278
- var startingPatch = { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort };
279
- if (msg.autoApproved) startingPatch.autoApproved = true;
376
+ var requestedEffort = msg.effort || proposal.recommendedEffort || "medium";
377
+ var effort = yoke.clampEffort(vendor, requestedEffort) || "";
378
+ if ((msg.effort && effort !== msg.effort) || !effortIsAvailable(options, vendor, model, effort)) {
379
+ throw new Error("Selected Split Worker reasoning effort is unavailable");
380
+ }
381
+ var autoAccepted = trustedAutoAcceptance === true;
382
+ var startingPatch = {
383
+ status: "starting",
384
+ selectedVendor: vendor,
385
+ selectedModel: model,
386
+ selectedEffort: effort,
387
+ autoAccepted: autoAccepted,
388
+ decisionMode: autoAccepted ? "driver_recommendation" : "user",
389
+ decidedAt: Date.now(),
390
+ };
280
391
  updateProposal(session, proposal, startingPatch);
281
392
  try {
393
+ if (proposal.action === "replace") {
394
+ var liveGroup = approvedGroup;
395
+ var replaced = await ctx.replacePartner({
396
+ interrupt: proposal.interrupt === true,
397
+ workerVendor: vendor,
398
+ workerModel: model,
399
+ workerEffort: effort,
400
+ evaluation: proposal.evaluation || undefined,
401
+ }, session);
402
+ liveGroup = store.groupForMember(session.localId);
403
+ updateProposal(session, proposal, { status: "running", groupId: liveGroup.id, workerId: replaced.workerSessionId });
404
+ runWorker(session, proposal).catch(function (err) {
405
+ updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
406
+ resumeDriver(session, "[Split Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
407
+ });
408
+ return { ok: true, status: "running", group: liveGroup, replacement: replaced };
409
+ }
282
410
  var created = ctx.createPairRecord(ws, {
283
411
  driver: { sessionId: session.localId },
284
412
  worker: { vendor: vendor, model: model, effort: effort },
285
413
  });
414
+ ctx.recordGenerationStart(session, created.worker);
286
415
  updateProposal(session, proposal, { status: "running", groupId: created.group.id, workerId: created.worker.localId });
287
- if (ws._autoApproval) sm.sendToSession(session, { type: "pair_session_created", ok: true, group: created.group });
288
- else ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
416
+ ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
289
417
  runWorker(session, proposal).catch(function (err) {
290
418
  updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
291
419
  resumeDriver(session, "[Split Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
292
420
  });
293
421
  return { ok: true, status: "running", group: created.group };
294
422
  } catch (err) {
295
- updateProposal(session, proposal, { status: "pending", error: err.message || String(err) });
423
+ if (proposal.action === "replace") {
424
+ var restoredGroup = store.groupForMember(session.localId);
425
+ if (restoredGroup && restoredGroup.pair && restoredGroup.pair.driverId === session.localId &&
426
+ restoredGroup.pair.workerId === proposal.sourceWorkerId) {
427
+ proposal.sourceGroupId = restoredGroup.id;
428
+ }
429
+ }
430
+ updateProposal(session, proposal, {
431
+ status: "pending",
432
+ error: err.message || String(err),
433
+ autoAccepted: false,
434
+ decisionMode: null,
435
+ decidedAt: null,
436
+ });
296
437
  return { ok: false, status: "pending", error: err.message || String(err) };
297
438
  }
298
439
  }
@@ -304,7 +445,10 @@ function attachWorkerProposal(ctx) {
304
445
  if (proposal.status !== "pending") throw new Error("Split Worker suggestion has already been resolved");
305
446
  if (!msg.accepted) {
306
447
  updateProposal(session, proposal, { status: "declined" });
307
- await resumeDriver(session, "[Split Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
448
+ var declinedText = proposal.action === "replace"
449
+ ? "[Split Worker replacement declined]\nContinue with the existing Split Worker and the current task."
450
+ : "[Split Worker proposal declined]\nContinue this task in the current Driver session using the plan you already prepared.";
451
+ await resumeDriver(session, declinedText);
308
452
  return { ok: true, status: "declined" };
309
453
  }
310
454
  return acceptProposal(session, proposal, msg, ws);
@@ -318,11 +462,12 @@ function attachWorkerProposal(ctx) {
318
462
  return true;
319
463
  }
320
464
 
321
- function getToolDefs(session) {
322
- if (!isEligible(session)) return [];
465
+ function getToolDefs(session, options) {
466
+ var persistent = !!(options && options.persistent);
467
+ if (!isEligible(session) && !(persistent && canOffer(session))) return [];
323
468
  return [{
324
469
  name: "propose_worker",
325
- description: "After making a concise plan for an implementation-heavy task, suggest that the user run the execution with another model in a visible Split Worker session. Use this only before substantial execution begins, never for small edits, explanations, or tasks the user asked you to keep in this session.",
470
+ description: "Propose a visible Split Worker for implementation-heavy execution. This non-mutating tool only shows the user a card where they choose vendor, model, and effort. It never creates a session or delegates work until the user accepts.",
326
471
  inputSchema: buildShape({
327
472
  summary: { type: "string", description: "One short sentence explaining why a Split Worker is useful for this task." },
328
473
  plan: { type: "string", description: "A concise numbered implementation plan to show in the approval card." },
@@ -330,19 +475,21 @@ function attachWorkerProposal(ctx) {
330
475
  recommendedVendor: { type: "string", description: "Optional installed vendor id for the Split Worker." },
331
476
  recommendedModel: { type: "string", description: "Optional exact Split Worker model id. Omit when uncertain." },
332
477
  recommendedEffort: { type: "string", description: "Optional reasoning effort: minimal, low, medium, high, xhigh, or max." },
333
- }, ["summary", "plan", "message"]),
478
+ recommendationRationale: { type: "string", description: "Concise Driver-authored explanation of why the recommended vendor, model, and effort fit this exact task." },
479
+ }, ["summary", "plan", "message", "recommendationRationale"]),
334
480
  handler: function (args) { return propose(args || {}, session); },
335
481
  }];
336
482
  }
337
483
 
338
484
  function getSystemPrompt(session) {
339
- if (!isEligible(session)) return "";
340
- return "You can introduce Clay's Driver/Split Worker workflow with propose_worker. For an implementation-heavy task that is likely to consume substantial execution time, first reason about the work and form a concise plan. Before editing files or running substantial execution, call propose_worker once with that plan and a complete Split Worker instruction, recommending another suitable execution model. Use it conservatively: do not suggest a Split Worker for questions, investigations, tiny fixes, or when the user explicitly wants you to execute here. After posting a proposal, end the turn and wait. If accepted, the Split Worker runs visibly and its result returns to you for review.";
485
+ if (!isEligible(session) || !driverOrchestration.isHighTierDriverSession(session, sm)) return "";
486
+ return "For implementation-heavy work, use propose_worker before substantial execution. Work is implementation-heavy when it spans multiple modules, crosses client/server/data boundaries, requires a migration, or contains independent investigation and execution. Do not skip delegation merely because you can implement it yourself; preserve the Driver for decomposition, runtime selection, judgment, review, and integration. The user's explicit request to work directly takes precedence, and work that must remain sequential in this session stays here. Recommend an exact available vendor, model, and effort, and give a concise recommendationRationale explaining why all three fit the task. Clay always records and shows the runtime configuration card. In full-access mode Clay may auto-accept that exact validated recommendation; otherwise it waits for the user's explicit choice. After posting, end the turn. If accepted, Clay creates the exact Driver/Split Worker pair and delegates the proposed task once. If declined, continue in this Driver session. Do not call send_to_partner while unpaired.";
341
487
  }
342
488
 
343
489
  return {
344
490
  getToolDefs: getToolDefs,
345
491
  getSystemPrompt: getSystemPrompt,
492
+ proposeReplacement: proposeReplacement,
346
493
  handleMessage: handleMessage,
347
494
  respondToProposal: respondToProposal,
348
495
  };
@@ -350,5 +497,4 @@ function attachWorkerProposal(ctx) {
350
497
 
351
498
  module.exports = {
352
499
  attachWorkerProposal: attachWorkerProposal,
353
- isFableSession: isFableSession,
354
500
  };