clay-server 3.4.0-beta.2 → 3.4.0-beta.21

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 (82) hide show
  1. package/lib/daemon-projects.js +3 -3
  2. package/lib/daemon.js +59 -59
  3. package/lib/project-connection.js +35 -7
  4. package/lib/project-debate.js +4 -0
  5. package/lib/project-mate-interaction.js +21 -1
  6. package/lib/project-memory.js +3 -0
  7. package/lib/project-models.js +220 -0
  8. package/lib/project-session-handoff.js +162 -0
  9. package/lib/project-session-pair.js +14 -7
  10. package/lib/project-session-spawn.js +1 -1
  11. package/lib/project-sessions.js +13 -45
  12. package/lib/project-worker-proposal.js +51 -14
  13. package/lib/project.js +38 -80
  14. package/lib/public/antigravity-avatar.png +0 -0
  15. package/lib/public/app.js +29 -0
  16. package/lib/public/copilot-avatar.svg +5 -0
  17. package/lib/public/css/command-palette.css +22 -2
  18. package/lib/public/css/icon-strip.css +29 -13
  19. package/lib/public/css/input.css +56 -0
  20. package/lib/public/css/mates.css +6 -0
  21. package/lib/public/css/menus.css +38 -25
  22. package/lib/public/css/messages.css +9 -0
  23. package/lib/public/css/pane.css +16 -3
  24. package/lib/public/css/pwa-mobile.css +17 -0
  25. package/lib/public/css/session-actions.css +98 -0
  26. package/lib/public/css/title-bar.css +19 -0
  27. package/lib/public/grok-avatar.svg +4 -0
  28. package/lib/public/index.html +29 -7
  29. package/lib/public/junie-avatar.svg +11 -0
  30. package/lib/public/kimi-avatar.svg +4 -0
  31. package/lib/public/modules/agent-config-selects.js +69 -0
  32. package/lib/public/modules/app-header.js +4 -22
  33. package/lib/public/modules/app-messages.js +54 -11
  34. package/lib/public/modules/app-panels.js +36 -83
  35. package/lib/public/modules/app-projects.js +3 -1
  36. package/lib/public/modules/app-rendering.js +19 -4
  37. package/lib/public/modules/background-tasks-ui.js +55 -0
  38. package/lib/public/modules/mate-sidebar.js +11 -3
  39. package/lib/public/modules/model-picker.js +263 -0
  40. package/lib/public/modules/notifications.js +7 -1
  41. package/lib/public/modules/pane-bridge.js +11 -0
  42. package/lib/public/modules/pane-links.js +23 -0
  43. package/lib/public/modules/project-switcher.js +7 -6
  44. package/lib/public/modules/session-actions.js +294 -0
  45. package/lib/public/modules/sidebar-mates.js +1 -1
  46. package/lib/public/modules/sidebar-mobile.js +2 -1
  47. package/lib/public/modules/sidebar-projects.js +34 -43
  48. package/lib/public/modules/sidebar-sessions.js +20 -6
  49. package/lib/public/modules/split-pair-ui.js +16 -77
  50. package/lib/public/modules/split-view.js +3 -0
  51. package/lib/public/modules/tools.js +6 -1
  52. package/lib/public/modules/vendor-priority.js +14 -0
  53. package/lib/public/modules/vendor-selection.js +20 -0
  54. package/lib/public/modules/worker-proposal.js +6 -1
  55. package/lib/public/modules/worktree-location.js +17 -0
  56. package/lib/public/qwen-avatar.svg +4 -0
  57. package/lib/public/style.css +4 -2
  58. package/lib/sdk-bridge.js +103 -56
  59. package/lib/sdk-message-processor.js +26 -4
  60. package/lib/session-handoff-context.js +110 -0
  61. package/lib/session-notes-mcp-server.js +8 -1
  62. package/lib/sessions.js +4 -0
  63. package/lib/worktree.js +9 -4
  64. package/lib/ws-schema.js +9 -1
  65. package/lib/yoke/acp-agent-profiles.js +64 -14
  66. package/lib/yoke/adapters/antigravity.js +417 -0
  67. package/lib/yoke/adapters/claude.js +34 -0
  68. package/lib/yoke/adapters/codex.js +101 -5
  69. package/lib/yoke/adapters/copilot.js +7 -0
  70. package/lib/yoke/adapters/grok.js +7 -0
  71. package/lib/yoke/adapters/junie.js +7 -0
  72. package/lib/yoke/adapters/kimi.js +7 -0
  73. package/lib/yoke/adapters/kiro.js +2 -2
  74. package/lib/yoke/adapters/qwen.js +7 -0
  75. package/lib/yoke/codex-app-server.js +25 -8
  76. package/lib/yoke/index.js +67 -28
  77. package/lib/yoke/instructions.js +3 -0
  78. package/lib/yoke/interface.js +12 -0
  79. package/lib/yoke/vendor-registry.js +61 -6
  80. package/package.json +1 -1
  81. package/lib/public/gemini-avatar.svg +0 -11
  82. package/lib/yoke/adapters/gemini.js +0 -7
@@ -0,0 +1,162 @@
1
+ var yoke = require("./yoke");
2
+ var contextBuilder = require("./session-handoff-context");
3
+
4
+ function hasUserContext(session) {
5
+ var history = (session && session.history) || [];
6
+ for (var i = 0; i < history.length; i++) {
7
+ if (history[i] && history[i].type === "user_message" && history[i].text) return true;
8
+ if (history[i] && history[i].type === "handoff_context" && history[i].request) return true;
9
+ }
10
+ return false;
11
+ }
12
+
13
+ function attachSessionHandoff(ctx) {
14
+ var sm = ctx.sm;
15
+
16
+ function sendResult(ws, ok, details) {
17
+ ctx.sendTo(ws, Object.assign({ type: "session_handoff_result", ok: ok }, details || {}));
18
+ }
19
+
20
+ function validate(ws, source, targetVendor) {
21
+ if (ctx.isMate) throw new Error("Session handoff is only available in projects.");
22
+ if (!source) throw new Error("No active session was found.");
23
+ if (ctx.splitStore && ctx.splitStore.groupForMember(source.localId)) {
24
+ throw new Error("Open the session by itself before continuing it in another agent.");
25
+ }
26
+ if (source.isProcessing || source._queryStarting) {
27
+ throw new Error("Wait for the current response to finish before continuing in another agent.");
28
+ }
29
+ if (!hasUserContext(source)) throw new Error("This session does not have any conversation to continue.");
30
+ if (!targetVendor) throw new Error("Choose an agent for the handoff.");
31
+ if (!ctx.adapters[targetVendor] || (sm.installedVendors || []).indexOf(targetVendor) === -1) {
32
+ throw new Error("The selected coding agent is not installed.");
33
+ }
34
+ var linuxUser = ctx.getLinuxUserForSession(source);
35
+ var vendorInfo = yoke.getVendorInfo(targetVendor);
36
+ if (linuxUser && vendorInfo && vendorInfo.osUserIsolation === false) {
37
+ throw new Error((vendorInfo.displayName || targetVendor) + " is not available for OS-isolated users.");
38
+ }
39
+ var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
40
+ if ((source.ownerId || null) !== ownerId) throw new Error("Session access denied.");
41
+ return linuxUser;
42
+ }
43
+
44
+ function quietRecord(session, entry) {
45
+ session.history.push(entry);
46
+ sm.appendToSessionFile(session, entry);
47
+ }
48
+
49
+ function handoff(ws, msg) {
50
+ var source = sm.sessions.get(ws._clayActiveSession) || null;
51
+ var targetVendor = typeof msg.targetVendor === "string" ? msg.targetVendor.trim() : "";
52
+ var targetModel = typeof msg.model === "string" ? msg.model.trim() : "";
53
+ var linuxUser;
54
+ try {
55
+ linuxUser = validate(ws, source, targetVendor);
56
+ } catch (err) {
57
+ sendResult(ws, false, { error: err.message || String(err) });
58
+ return;
59
+ }
60
+
61
+ var targetEffort = yoke.clampEffort(
62
+ targetVendor,
63
+ msg.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[targetVendor]) || sm.currentEffort || "medium"
64
+ ) || null;
65
+ var target = sm.createSessionRaw({
66
+ ownerId: source.ownerId || null,
67
+ sessionVisibility: source.sessionVisibility || "shared",
68
+ vendor: targetVendor,
69
+ model: targetModel || null,
70
+ effort: targetEffort,
71
+ });
72
+ var targetName = (yoke.getVendorInfo(targetVendor) || {}).displayName || targetVendor;
73
+ target.title = (source.title || "Continued session") + " · " + targetName;
74
+ target.handoff = {
75
+ sourceSessionId: source.localId,
76
+ sourceVendor: source.vendor || sm.defaultVendor || "claude",
77
+ createdAt: Date.now(),
78
+ mode: "context",
79
+ sourceHistoryIndex: source.history.length,
80
+ };
81
+
82
+ var sourceEntry = {
83
+ type: "handoff_created",
84
+ targetSessionId: target.localId,
85
+ targetVendor: targetVendor,
86
+ targetTitle: target.title,
87
+ _ts: Date.now(),
88
+ };
89
+ var targetEntry = {
90
+ type: "handoff_context",
91
+ sourceSessionId: source.localId,
92
+ sourceVendor: target.handoff.sourceVendor,
93
+ sourceTitle: source.title || "Untitled session",
94
+ targetVendor: targetVendor,
95
+ request: contextBuilder.latestUserRequest(source.history),
96
+ _ts: Date.now(),
97
+ };
98
+ quietRecord(source, sourceEntry);
99
+ quietRecord(target, targetEntry);
100
+
101
+ var prompt = contextBuilder.buildHandoffContext({
102
+ cwd: ctx.cwd,
103
+ source: source,
104
+ targetVendor: targetVendor,
105
+ });
106
+ target.isProcessing = true;
107
+ target.lastActivity = Date.now();
108
+ target.sentToolResults = {};
109
+ sm.switchSession(target.localId, ws);
110
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
111
+ sendResult(ws, true, {
112
+ sourceSessionId: source.localId,
113
+ targetSessionId: target.localId,
114
+ targetVendor: targetVendor,
115
+ });
116
+
117
+ var sdk = ctx.getSdk();
118
+ function failStart(err) {
119
+ target.isProcessing = false;
120
+ sm.sendAndRecord(target, { type: "error", text: "Could not start the handoff session: " + (err.message || String(err)) });
121
+ if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
122
+ sm.broadcastSessionList();
123
+ }
124
+ if (!sdk || typeof sdk.startQuery !== "function") {
125
+ failStart(new Error("SDK bridge is not ready."));
126
+ return;
127
+ }
128
+ target._queryStartTs = Date.now();
129
+ var startPromise;
130
+ try {
131
+ startPromise = sdk.startQuery(target, prompt, undefined, linuxUser);
132
+ } catch (err) {
133
+ failStart(err);
134
+ return;
135
+ }
136
+ Promise.resolve(startPromise).catch(failStart);
137
+ }
138
+
139
+ function handleMessage(ws, msg) {
140
+ if (msg.type === "handoff_session_options") {
141
+ ctx.sendTo(ws, {
142
+ type: "handoff_session_options",
143
+ installedVendors: sm.installedVendors || [],
144
+ modelsByVendor: sm.modelsByVendor || {},
145
+ capabilitiesByVendor: sm.capabilitiesByVendor || {},
146
+ });
147
+ return true;
148
+ }
149
+ if (msg.type === "handoff_session") {
150
+ handoff(ws, msg);
151
+ return true;
152
+ }
153
+ return false;
154
+ }
155
+
156
+ return { handleMessage: handleMessage };
157
+ }
158
+
159
+ module.exports = {
160
+ attachSessionHandoff: attachSessionHandoff,
161
+ hasUserContext: hasUserContext,
162
+ };
@@ -98,10 +98,15 @@ function attachSessionPair(ctx) {
98
98
  token.delivered = true;
99
99
  var failure = token.failure || null;
100
100
  var response = token.response || "";
101
- var text = "A Worker task delegated through send_to_partner has finished.\n\n" +
102
- "Original task:\n" + token.message + "\n\n" +
103
- (failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
104
- "\n\nReview the result, verify it as needed, and continue the task.";
101
+ var text;
102
+ if (token.interrupted) {
103
+ text = "[Worker execution interrupted] The user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
104
+ } else {
105
+ text = "A Worker task delegated through send_to_partner has finished.\n\n" +
106
+ "Original task:\n" + token.message + "\n\n" +
107
+ (failure ? "Worker error:\n" + failure : "Worker result:\n" + (response || "(No text response was recorded.)")) +
108
+ "\n\nReview the result, verify it as needed, and continue the task.";
109
+ }
105
110
  sm.sendAndRecord(caller, {
106
111
  type: "user_message",
107
112
  text: text,
@@ -146,6 +151,7 @@ function attachSessionPair(ctx) {
146
151
  }
147
152
  token.response = responseText(partner.history || [], token.startIndex);
148
153
  token.failure = errorSince(partner.history || [], token.startIndex);
154
+ token.interrupted = !token.failure && !!partner._lastTurnInterrupted;
149
155
  finishDelegation(group, caller, partner, token);
150
156
  return resumeDriverWithResult(caller, partner, token);
151
157
  }
@@ -165,8 +171,9 @@ function attachSessionPair(ctx) {
165
171
  clearInterval(timer);
166
172
  finishDelegation(group, caller, partner, token);
167
173
  var failure = errorSince(partner.history || [], token.startIndex);
174
+ var interrupted = !failure && !!partner._lastTurnInterrupted;
168
175
  resolve({
169
- status: failure ? "error" : "complete",
176
+ status: failure ? "error" : (interrupted ? "interrupted" : "complete"),
170
177
  response: responseText(partner.history || [], token.startIndex),
171
178
  error: failure || undefined,
172
179
  });
@@ -260,7 +267,7 @@ function attachSessionPair(ctx) {
260
267
  var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
261
268
  count = Math.max(1, Math.min(5, count));
262
269
  return toolResult({
263
- status: resolved.partner.isProcessing ? "running" : "idle",
270
+ status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
264
271
  partnerId: resolved.partner.localId,
265
272
  title: resolved.partner.title || "New Session",
266
273
  turns: recentTurns(resolved.partner, count),
@@ -383,7 +390,7 @@ function attachSessionPair(ctx) {
383
390
  var group = store.groupForMember(session.localId);
384
391
  var pairPrompt = "";
385
392
  if (group && group.pair && group.pair.driverId === session.localId) {
386
- pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
393
+ pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner and read_partner are provided directly to you. Use send_to_partner to delegate concrete bounded work to the Worker. A completed Worker turn leaves the Worker session available for more work. If a Worker turn is interrupted, its work is partial and unverified; review it and decide next steps with the user. If review or user feedback requires corrections to the Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If send_to_partner reports that the Worker is no longer available, create a replacement with spawn_sessions and delegate the remaining implementation rather than taking it over. If a non-waiting delegation finishes later, Clay pushes the result back and resumes you automatically; use read_partner only when you need an interim status check. Integrate and verify the final outcome. Do not search the project for implementations of these tools, and do not delegate work that must be performed sequentially in this same session.";
387
394
  }
388
395
  return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
389
396
  }
@@ -319,7 +319,7 @@ function attachSessionSpawn(ctx) {
319
319
  statuses.push({
320
320
  localId: session.localId,
321
321
  title: session.title || "New Session",
322
- status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
322
+ status: session.isProcessing ? "running" : (session._lastTurnInterrupted ? "interrupted" : (hasSessionError(session) ? "error" : "done")),
323
323
  turnCount: session.turnCount || 0,
324
324
  lastActivity: session.lastActivity || session.createdAt || 0,
325
325
  });
@@ -658,10 +658,6 @@ function attachSessions(ctx) {
658
658
  // (resume_tui_session), and born-GUI never auto-converts to a terminal.
659
659
  resolveSessionForView(xmTarget, ws);
660
660
  }
661
- // If the target session's vendor doesn't own the currently cached
662
- // model, clear sm.currentModel so the UI and next query don't leak
663
- // the previous session's vendor-specific model into this one. A
664
- // session-specific model always wins and survives daemon restarts.
665
661
  var switchTargetSess = sm.sessions.get(msg.id);
666
662
  var switchTargetVendor = switchTargetSess && (switchTargetSess.vendor || sm.defaultVendor || "claude");
667
663
  if (switchTargetSess && !switchTargetSess.effort) {
@@ -671,22 +667,6 @@ function attachSessions(ctx) {
671
667
  ) || null;
672
668
  sm.saveSessionFile(switchTargetSess);
673
669
  }
674
- if (switchTargetSess && switchTargetSess.model) {
675
- sm.currentModel = switchTargetSess.model;
676
- } else if (switchTargetSess && sm.currentModel) {
677
- var targetVendor = switchTargetSess.vendor || sm.defaultVendor || null;
678
- var tvModels = (targetVendor && sm.modelsByVendor && sm.modelsByVendor[targetVendor]) || [];
679
- var found = false;
680
- var _curLc = sm.currentModel.toLowerCase();
681
- for (var tvi = 0; tvi < tvModels.length; tvi++) {
682
- var tvEntry = tvModels[tvi];
683
- var tvVal = typeof tvEntry === "string" ? tvEntry : (tvEntry && (tvEntry.value || tvEntry.id)) || "";
684
- if (tvVal === sm.currentModel || (tvVal && (tvVal.toLowerCase().indexOf(_curLc) !== -1 || _curLc.indexOf(tvVal.toLowerCase()) !== -1))) { found = true; break; }
685
- }
686
- if (tvModels.length > 0 && !found) {
687
- sm.currentModel = "";
688
- }
689
- }
690
670
  // Check access in multi-user mode
691
671
  if (usersModule.isMultiUser() && ws._clayUser) {
692
672
  var switchTarget = sm.sessions.get(msg.id);
@@ -958,14 +938,6 @@ function attachSessions(ctx) {
958
938
  return true;
959
939
  }
960
940
 
961
- if (msg.type === "set_model" && msg.model) {
962
- var session = getSessionForWs(ws);
963
- if (session) {
964
- sdk.setModel(session, msg.model);
965
- }
966
- return true;
967
- }
968
-
969
941
  if (msg.type === "reload_skills") {
970
942
  var session = getSessionForWs(ws);
971
943
  if (session && sdk.reloadSkills) {
@@ -997,11 +969,7 @@ function attachSessions(ctx) {
997
969
  " is bound to '" + vendorSession.vendor + "', refused rebind to '" + msg.vendor + "'");
998
970
  } else {
999
971
  vendorSession.vendor = msg.vendor;
1000
- // Clear the shared model so the next query uses the vendor's default
1001
- // instead of leaking the previous vendor's model into a fresh session.
1002
- if (sm.currentModel) {
1003
- sm.currentModel = "";
1004
- }
972
+ if (vendorSession.vendor !== msg.vendor) vendorSession.model = "";
1005
973
  sm.saveSessionFile(vendorSession);
1006
974
  sm.broadcastSessionList();
1007
975
  }
@@ -1012,11 +980,11 @@ function attachSessions(ctx) {
1012
980
  type: "model_info",
1013
981
  model: "",
1014
982
  models: vendorModels,
1015
- vendor: msg.vendor,
983
+ vendor: msg.vendor, sessionId: vendorSession ? vendorSession.localId : null,
1016
984
  availableVendors: sm.availableVendors || [],
1017
985
  installedVendors: sm.installedVendors || [],
1018
986
  });
1019
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
987
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1020
988
  }
1021
989
  return true;
1022
990
  }
@@ -1052,7 +1020,7 @@ function attachSessions(ctx) {
1052
1020
  sm.broadcastSessionList();
1053
1021
  sdk.setPermissionMode(session, msg.mode);
1054
1022
  }
1055
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1023
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1056
1024
  return true;
1057
1025
  }
1058
1026
 
@@ -1113,7 +1081,7 @@ function attachSessions(ctx) {
1113
1081
  if (session) {
1114
1082
  sdk.setPermissionMode(session, msg.mode);
1115
1083
  }
1116
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1084
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1117
1085
  return true;
1118
1086
  }
1119
1087
 
@@ -1126,7 +1094,7 @@ function attachSessions(ctx) {
1126
1094
  if (session) {
1127
1095
  sdk.setPermissionMode(session, msg.mode);
1128
1096
  }
1129
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1097
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1130
1098
  return true;
1131
1099
  }
1132
1100
 
@@ -1142,7 +1110,7 @@ function attachSessions(ctx) {
1142
1110
  sdk.setEffort(session, sessionEffort).catch(function (e) {
1143
1111
  sendTo(ws, { type: "error", text: "Failed to change reasoning effort: " + (e.message || e) });
1144
1112
  });
1145
- sm.sendToSession(session, { type: "config_state", model: session.model || sm.currentModel || "", mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1113
+ sm.sendToSession(session, { type: "config_state", model: session.model || ((sm.defaultModelByVendor || {})[effortVendor]) || "", vendor: effortVendor, sessionId: session.localId, mode: session.permissionMode || sm.currentPermissionMode || "default", effort: sessionEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1146
1114
  }
1147
1115
  return true;
1148
1116
  }
@@ -1152,7 +1120,7 @@ function attachSessions(ctx) {
1152
1120
  opts.onSetServerDefaultEffort(msg.effort);
1153
1121
  }
1154
1122
  sm.currentEffort = msg.effort;
1155
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1123
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1156
1124
  return true;
1157
1125
  }
1158
1126
 
@@ -1161,20 +1129,20 @@ function attachSessions(ctx) {
1161
1129
  opts.onSetProjectDefaultEffort(slug, msg.effort);
1162
1130
  }
1163
1131
  sm.currentEffort = msg.effort;
1164
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1132
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1165
1133
  return true;
1166
1134
  }
1167
1135
 
1168
1136
  if (msg.type === "set_betas") {
1169
1137
  sm.currentBetas = msg.betas || [];
1170
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1138
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1171
1139
  return true;
1172
1140
  }
1173
1141
 
1174
1142
  if (msg.type === "set_thinking") {
1175
1143
  sm.currentThinking = msg.thinking || "adaptive";
1176
1144
  if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
1177
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1145
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1178
1146
  return true;
1179
1147
  }
1180
1148
 
@@ -1456,7 +1424,7 @@ function attachSessions(ctx) {
1456
1424
  if (decision === "allow_accept_edits") {
1457
1425
  sdk.setPermissionMode(session, "acceptEdits");
1458
1426
  sm.currentPermissionMode = "acceptEdits";
1459
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1427
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1460
1428
  pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1461
1429
  sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
1462
1430
  return true;
@@ -1485,7 +1453,7 @@ function attachSessions(ctx) {
1485
1453
 
1486
1454
  // Update permission mode for the new session
1487
1455
  sm.currentPermissionMode = "acceptEdits";
1488
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1456
+ send({ type: "config_state", vendor: null, sessionId: null, mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1489
1457
 
1490
1458
  // Build prompt from plan content (sent from client) or plan file path
1491
1459
  var clientPlanContent = msg.planContent || "";
@@ -40,7 +40,7 @@ function attachWorkerProposal(ctx) {
40
40
  function isEligible(session) {
41
41
  if (ctx.isMate || !session || session.mode === "tui") return false;
42
42
  if (store.groupForMember(session.localId)) return false;
43
- return isFableSession(session, sm.modelsByVendor, sm.currentModel);
43
+ return isFableSession(session, sm.modelsByVendor, (sm.defaultModelByVendor || {})[session.vendor || "claude"]);
44
44
  }
45
45
 
46
46
  function safeModelsByVendor(installed) {
@@ -112,7 +112,7 @@ function attachWorkerProposal(ctx) {
112
112
  }
113
113
  var models = options.modelsByVendor[vendor] || [];
114
114
  var model = modelIsAvailable(options, vendor, args.recommendedModel) ? (args.recommendedModel || "") : "";
115
- var currentModel = session.model || sm.currentModel || "";
115
+ var currentModel = session.model || (sm.defaultModelByVendor || {})[session.vendor || "claude"] || "";
116
116
  if (vendor === session.vendor && model === currentModel) model = "";
117
117
  if (!model && models.length > 0) {
118
118
  for (var j = 0; j < models.length; j++) {
@@ -154,6 +154,18 @@ function attachWorkerProposal(ctx) {
154
154
  }, patch));
155
155
  }
156
156
 
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
+
157
169
  async function propose(args, session) {
158
170
  if (!isEligible(session)) return toolResult({ error: "Worker suggestions are only available in an unpaired Fable session." });
159
171
  if (hasPendingProposal(session)) return toolResult({ error: "A Worker suggestion is already awaiting a decision." });
@@ -166,6 +178,7 @@ function attachWorkerProposal(ctx) {
166
178
  var options = proposalOptions();
167
179
  if (options.installedVendors.length === 0) return toolResult({ error: "No coding agent is installed for a Worker session." });
168
180
  var recommendation = chooseRecommendation(args, session, options);
181
+ var autoApprove = skipPermissionsEnabled(session);
169
182
  var proposal = {
170
183
  type: "worker_proposal",
171
184
  proposalId: "worker_" + crypto.randomUUID(),
@@ -178,7 +191,22 @@ function attachWorkerProposal(ctx) {
178
191
  recommendedEffort: recommendation.effort,
179
192
  options: options,
180
193
  };
194
+ if (autoApprove) proposal.autoApproved = true;
181
195
  sm.sendAndRecord(session, proposal);
196
+ if (autoApprove) {
197
+ var accepted = await acceptProposal(session, proposal, {
198
+ vendor: recommendation.vendor,
199
+ model: recommendation.model,
200
+ effort: recommendation.effort,
201
+ autoApproved: true,
202
+ }, autoApprovalWs(session));
203
+ if (!accepted.ok) return toolResult({ error: accepted.error || "Could not start the Worker." });
204
+ return toolResult({
205
+ status: "running",
206
+ proposalId: proposal.proposalId,
207
+ instruction: "The Worker was auto-approved and started because skip permissions is enabled. Its result will return for review.",
208
+ });
209
+ }
182
210
  return toolResult({
183
211
  status: "posted",
184
212
  proposalId: proposal.proposalId,
@@ -220,6 +248,8 @@ function attachWorkerProposal(ctx) {
220
248
  var followup;
221
249
  if (result.status === "complete") {
222
250
  followup = "[Worker execution completed]\nReview and verify the Worker's result. The Worker session remains available: if the implementation needs corrections or additional edits, send a follow-up with send_to_partner instead of taking over the Worker-owned files yourself. If that Worker is no longer available, create a replacement with spawn_sessions for the remaining implementation.\n\n" + (result.response || "The Worker completed without a text summary.");
251
+ } else if (result.status === "interrupted") {
252
+ followup = "[Worker execution interrupted]\nThe user interrupted the Worker mid-turn. Its work is PARTIAL and unverified — do not treat it as finished. Review what was done and decide next steps with the user.";
223
253
  } else if (result.status === "running") {
224
254
  followup = "[Worker execution is still running]\nUse read_partner to inspect progress before completing the task.";
225
255
  } else {
@@ -238,30 +268,24 @@ function attachWorkerProposal(ctx) {
238
268
  return session;
239
269
  }
240
270
 
241
- async function respondToProposal(ws, msg) {
242
- var session = sessionForResponse(ws);
243
- var proposal = findProposal(session, msg.proposalId);
244
- if (!proposal) throw new Error("Worker suggestion not found");
245
- if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
246
- if (!msg.accepted) {
247
- updateProposal(session, proposal, { status: "declined" });
248
- await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
249
- return { ok: true, status: "declined" };
250
- }
271
+ async function acceptProposal(session, proposal, msg, ws) {
251
272
  var options = proposal.options || proposalOptions();
252
273
  var vendor = msg.vendor || proposal.recommendedVendor;
253
274
  var model = msg.model || "";
254
275
  if (options.installedVendors.indexOf(vendor) === -1) throw new Error("Selected Worker vendor is not installed");
255
276
  if (!modelIsAvailable(options, vendor, model)) throw new Error("Selected Worker model is unavailable");
256
277
  var effort = yoke.clampEffort(vendor, msg.effort || proposal.recommendedEffort || "medium") || "";
257
- updateProposal(session, proposal, { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort });
278
+ var startingPatch = { status: "starting", selectedVendor: vendor, selectedModel: model, selectedEffort: effort };
279
+ if (msg.autoApproved) startingPatch.autoApproved = true;
280
+ updateProposal(session, proposal, startingPatch);
258
281
  try {
259
282
  var created = ctx.createPairRecord(ws, {
260
283
  driver: { sessionId: session.localId },
261
284
  worker: { vendor: vendor, model: model, effort: effort },
262
285
  });
263
286
  updateProposal(session, proposal, { status: "running", groupId: created.group.id, workerId: created.worker.localId });
264
- ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
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 });
265
289
  runWorker(session, proposal).catch(function (err) {
266
290
  updateProposal(session, proposal, { status: "error", error: err.message || String(err) });
267
291
  resumeDriver(session, "[Worker execution failed]\n" + (err.message || String(err))).catch(function () {});
@@ -273,6 +297,19 @@ function attachWorkerProposal(ctx) {
273
297
  }
274
298
  }
275
299
 
300
+ async function respondToProposal(ws, msg) {
301
+ var session = sessionForResponse(ws);
302
+ var proposal = findProposal(session, msg.proposalId);
303
+ if (!proposal) throw new Error("Worker suggestion not found");
304
+ if (proposal.status !== "pending") throw new Error("Worker suggestion has already been resolved");
305
+ if (!msg.accepted) {
306
+ updateProposal(session, proposal, { status: "declined" });
307
+ await resumeDriver(session, "[Worker suggestion declined]\nContinue this task in the current session using the plan you already prepared.");
308
+ return { ok: true, status: "declined" };
309
+ }
310
+ return acceptProposal(session, proposal, msg, ws);
311
+ }
312
+
276
313
  function handleMessage(ws, msg) {
277
314
  if (msg.type !== "worker_proposal_response") return false;
278
315
  respondToProposal(ws, msg).catch(function (err) {