clay-server 4.0.0-beta.13 → 4.0.0-beta.15

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 (52) hide show
  1. package/lib/capsule-display-floor.js +51 -0
  2. package/lib/capsule-frame-server.js +229 -0
  3. package/lib/capsule-pig-logic.js +268 -0
  4. package/lib/capsule-server-runtimes.js +41 -0
  5. package/lib/capsule-tictactoe-logic.js +260 -0
  6. package/lib/capsules/pig/display.js +190 -0
  7. package/lib/capsules/pig/manifest.json +9 -0
  8. package/lib/capsules/pig/ui.json +46 -0
  9. package/lib/capsules/tictactoe/manifest.json +9 -0
  10. package/lib/capsules/tictactoe/ui.json +203 -0
  11. package/lib/project-capsule-catalog.js +9 -1
  12. package/lib/project-connection.js +1 -1
  13. package/lib/project-log-feedback-delivery.js +76 -0
  14. package/lib/project-logs.js +4 -0
  15. package/lib/project-pair-lifecycle.js +7 -12
  16. package/lib/project-session-pair.js +29 -39
  17. package/lib/project-worker-proposal.js +203 -72
  18. package/lib/project.js +13 -0
  19. package/lib/public/css/capsule-ui.css +18 -0
  20. package/lib/public/css/home-session-actions.css +66 -0
  21. package/lib/public/css/home-sidebar.css +66 -0
  22. package/lib/public/css/mobile-nav.css +80 -0
  23. package/lib/public/css/sidebar.css +92 -0
  24. package/lib/public/css/worker-proposal.css +34 -1
  25. package/lib/public/modules/app-messages.js +14 -1
  26. package/lib/public/modules/home-conversations-sheet.js +102 -48
  27. package/lib/public/modules/home-session-actions.js +6 -0
  28. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  29. package/lib/public/modules/home-tool-frame.js +165 -0
  30. package/lib/public/modules/home-tools.js +129 -1
  31. package/lib/public/modules/session-hierarchy.js +54 -0
  32. package/lib/public/modules/sidebar-mobile.js +35 -6
  33. package/lib/public/modules/sidebar-session-hierarchy.js +206 -0
  34. package/lib/public/modules/sidebar-sessions.js +49 -7
  35. package/lib/public/modules/worker-proposal-state.js +16 -0
  36. package/lib/public/modules/worker-proposal.js +49 -12
  37. package/lib/sdk-bridge.js +16 -8
  38. package/lib/sdk-message-processor.js +34 -4
  39. package/lib/server-home-chat.js +12 -1
  40. package/lib/server-tools.js +97 -9
  41. package/lib/server.js +3 -0
  42. package/lib/session-driver-eligibility.js +20 -165
  43. package/lib/session-pair-factory.js +31 -28
  44. package/lib/session-pair-mcp-server.js +5 -9
  45. package/lib/session-pair-prompts.js +15 -15
  46. package/lib/session-provenance.js +119 -0
  47. package/lib/session-spawn-mcp-server.js +1 -1
  48. package/lib/sessions.js +40 -2
  49. package/lib/tools-registry.js +44 -10
  50. package/lib/ws-schema.js +8 -3
  51. package/lib/yoke/adapters/claude.js +12 -0
  52. package/package.json +1 -1
@@ -0,0 +1,206 @@
1
+ // Shared Driver/Worker hierarchy behavior for the project desktop and mobile lists.
2
+
3
+ import { iconHtml } from './icons.js';
4
+ import { store } from './store.js';
5
+ import { buildSessionHierarchy } from './session-hierarchy.js';
6
+
7
+ var expansionBySurface = {
8
+ desktop: new Map(),
9
+ mobile: new Map(),
10
+ };
11
+
12
+ export function prepareSidebarHierarchy(sessions) {
13
+ var hierarchy = buildSessionHierarchy(sessions);
14
+ var byDriver = new Map();
15
+ var sessionIds = new Set();
16
+ for (var i = 0; i < hierarchy.roots.length; i++) {
17
+ var root = hierarchy.roots[i];
18
+ if (!root.workers.length) continue;
19
+ byDriver.set(root.driver.id, root);
20
+ sessionIds.add(root.driver.id);
21
+ for (var j = 0; j < root.workers.length; j++) sessionIds.add(root.workers[j].id);
22
+ }
23
+ for (var k = 0; k < hierarchy.orphans.length; k++) sessionIds.add(hierarchy.orphans[k].id);
24
+ return {
25
+ byDriver: byDriver,
26
+ orphans: hierarchy.orphans,
27
+ sessionIds: sessionIds,
28
+ };
29
+ }
30
+
31
+ export function currentWorkerIds() {
32
+ var result = new Set();
33
+ var groups = store.get('splitGroups') || [];
34
+ for (var i = 0; i < groups.length; i++) {
35
+ if (groups[i].pair && Number.isInteger(groups[i].pair.workerId)) result.add(groups[i].pair.workerId);
36
+ }
37
+ return result;
38
+ }
39
+
40
+ export function hierarchyItemMatches(item, matchIds) {
41
+ if (matchIds === null) return true;
42
+ if (item.type === "driver-hierarchy") {
43
+ if (matchIds.has(item.root.driver.id)) return true;
44
+ for (var i = 0; i < item.root.workers.length; i++) {
45
+ if (matchIds.has(item.root.workers[i].id)) return true;
46
+ }
47
+ return false;
48
+ }
49
+ if (item.type === "orphan-workers") {
50
+ for (var j = 0; j < item.workers.length; j++) {
51
+ if (matchIds.has(item.workers[j].id)) return true;
52
+ }
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ function expanded(surface, key, workers, matchIds) {
59
+ var state = expansionBySurface[surface];
60
+ if (state.has(key)) return state.get(key);
61
+ return defaultHierarchyExpanded(workers, matchIds, currentWorkerIds());
62
+ }
63
+
64
+ export function defaultHierarchyExpanded(workers, matchIds, current) {
65
+ var currentIds = current || new Set();
66
+ for (var i = 0; i < workers.length; i++) {
67
+ if (workers[i].active || currentIds.has(workers[i].id) || (matchIds !== null && matchIds.has(workers[i].id))) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function toggle(surface, key, wasExpanded, rerender) {
73
+ expansionBySurface[surface].set(key, !wasExpanded);
74
+ rerender();
75
+ }
76
+
77
+ function visibleWorker(worker, driver, matchIds) {
78
+ return matchIds === null || matchIds.has(worker.id) || matchIds.has(driver.id);
79
+ }
80
+
81
+ export function renderDesktopDriverHierarchy(root, renderSession, rerender, matchIds) {
82
+ var wrapper = document.createElement("div");
83
+ wrapper.className = "session-driver-hierarchy";
84
+ var key = "driver:" + root.driver.id;
85
+ var isExpanded = expanded("desktop", key, root.workers, matchIds);
86
+ var childrenId = "session-workers-" + root.driver.id;
87
+ var row = renderSession(root.driver);
88
+ row.classList.add("session-driver-item");
89
+ var control = document.createElement("button");
90
+ control.type = "button";
91
+ control.className = "session-driver-toggle";
92
+ control.setAttribute("aria-expanded", String(isExpanded));
93
+ control.setAttribute("aria-controls", childrenId);
94
+ control.setAttribute("aria-label", (isExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
95
+ control.innerHTML = iconHtml("chevron-right");
96
+ control.addEventListener("click", function (event) {
97
+ event.preventDefault();
98
+ event.stopPropagation();
99
+ toggle("desktop", key, isExpanded, rerender);
100
+ });
101
+ row.insertBefore(control, row.firstChild);
102
+ wrapper.appendChild(row);
103
+
104
+ var children = document.createElement("div");
105
+ children.id = childrenId;
106
+ children.className = "session-worker-children";
107
+ children.setAttribute("role", "group");
108
+ children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
109
+ children.hidden = !isExpanded;
110
+ var current = currentWorkerIds();
111
+ for (var i = 0; i < root.workers.length; i++) {
112
+ if (!visibleWorker(root.workers[i], root.driver, matchIds)) continue;
113
+ children.appendChild(renderSession(root.workers[i], { worker: true, current: current.has(root.workers[i].id) }));
114
+ }
115
+ wrapper.appendChild(children);
116
+ return wrapper;
117
+ }
118
+
119
+ export function renderDesktopOrphanHierarchy(workers, renderSession, rerender, matchIds) {
120
+ var wrapper = document.createElement("div");
121
+ wrapper.className = "session-driver-hierarchy session-orphan-hierarchy";
122
+ var isExpanded = expanded("desktop", "orphan", workers, matchIds);
123
+ var control = document.createElement("button");
124
+ control.type = "button";
125
+ control.className = "session-orphan-toggle";
126
+ control.setAttribute("aria-expanded", String(isExpanded));
127
+ control.setAttribute("aria-controls", "session-orphan-workers");
128
+ control.innerHTML = iconHtml("chevron-right") + '<span class="session-orphan-title">Unavailable Driver</span><span class="session-worker-count">' + workers.length + "</span>";
129
+ control.addEventListener("click", function () { toggle("desktop", "orphan", isExpanded, rerender); });
130
+ wrapper.appendChild(control);
131
+
132
+ var children = document.createElement("div");
133
+ children.id = "session-orphan-workers";
134
+ children.className = "session-worker-children";
135
+ children.setAttribute("role", "group");
136
+ children.setAttribute("aria-label", "Workers whose Driver is unavailable");
137
+ children.hidden = !isExpanded;
138
+ var current = currentWorkerIds();
139
+ for (var i = 0; i < workers.length; i++) {
140
+ if (matchIds !== null && !matchIds.has(workers[i].id)) continue;
141
+ children.appendChild(renderSession(workers[i], { worker: true, current: current.has(workers[i].id) }));
142
+ }
143
+ wrapper.appendChild(children);
144
+ return wrapper;
145
+ }
146
+
147
+ export function renderMobileDriverHierarchy(root, renderSession, rerender) {
148
+ var wrapper = document.createElement("div");
149
+ wrapper.className = "mobile-driver-hierarchy";
150
+ var key = "driver:" + root.driver.id;
151
+ var isExpanded = expanded("mobile", key, root.workers, null);
152
+ var childrenId = "mobile-session-workers-" + root.driver.id;
153
+ var header = document.createElement("div");
154
+ header.className = "mobile-driver-header";
155
+ var control = document.createElement("button");
156
+ control.type = "button";
157
+ control.className = "mobile-driver-toggle";
158
+ control.setAttribute("aria-expanded", String(isExpanded));
159
+ control.setAttribute("aria-controls", childrenId);
160
+ control.setAttribute("aria-label", (isExpanded ? "Collapse" : "Expand") + " Workers for " + (root.driver.title || "New Session"));
161
+ control.innerHTML = iconHtml("chevron-right");
162
+ control.addEventListener("click", function () { toggle("mobile", key, isExpanded, rerender); });
163
+ header.appendChild(control);
164
+ header.appendChild(renderSession(root.driver));
165
+ wrapper.appendChild(header);
166
+
167
+ var children = document.createElement("div");
168
+ children.id = childrenId;
169
+ children.className = "mobile-worker-children";
170
+ children.setAttribute("role", "group");
171
+ children.setAttribute("aria-label", "Workers for " + (root.driver.title || "New Session"));
172
+ children.hidden = !isExpanded;
173
+ var current = currentWorkerIds();
174
+ for (var i = 0; i < root.workers.length; i++) {
175
+ children.appendChild(renderSession(root.workers[i], { worker: true, current: current.has(root.workers[i].id) }));
176
+ }
177
+ wrapper.appendChild(children);
178
+ return wrapper;
179
+ }
180
+
181
+ export function renderMobileOrphanHierarchy(workers, renderSession, rerender) {
182
+ var wrapper = document.createElement("div");
183
+ wrapper.className = "mobile-driver-hierarchy mobile-orphan-hierarchy";
184
+ var isExpanded = expanded("mobile", "orphan", workers, null);
185
+ var control = document.createElement("button");
186
+ control.type = "button";
187
+ control.className = "mobile-orphan-toggle";
188
+ control.setAttribute("aria-expanded", String(isExpanded));
189
+ control.setAttribute("aria-controls", "mobile-orphan-workers");
190
+ control.innerHTML = iconHtml("chevron-right") + "<span>Unavailable Driver</span><span>" + workers.length + "</span>";
191
+ control.addEventListener("click", function () { toggle("mobile", "orphan", isExpanded, rerender); });
192
+ wrapper.appendChild(control);
193
+
194
+ var children = document.createElement("div");
195
+ children.id = "mobile-orphan-workers";
196
+ children.className = "mobile-worker-children";
197
+ children.setAttribute("role", "group");
198
+ children.setAttribute("aria-label", "Workers whose Driver is unavailable");
199
+ children.hidden = !isExpanded;
200
+ var current = currentWorkerIds();
201
+ for (var i = 0; i < workers.length; i++) {
202
+ children.appendChild(renderSession(workers[i], { worker: true, current: current.has(workers[i].id) }));
203
+ }
204
+ wrapper.appendChild(children);
205
+ return wrapper;
206
+ }
@@ -16,6 +16,12 @@ import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperim
16
16
  import { openGroup, separateGroup } from './split-view.js';
17
17
  import { groupedSessionIds } from './split-group-helpers.js';
18
18
  import { openPairDialog } from './split-pair-ui.js';
19
+ import {
20
+ hierarchyItemMatches,
21
+ prepareSidebarHierarchy,
22
+ renderDesktopDriverHierarchy,
23
+ renderDesktopOrphanHierarchy
24
+ } from './sidebar-session-hierarchy.js';
19
25
 
20
26
 
21
27
  // --- Session state ---
@@ -49,8 +55,8 @@ function sendSessionBookmark(sessionId, bookmarked) {
49
55
  }
50
56
 
51
57
  function compareSessionListItems(a, b) {
52
- var aData = a && a.type === "session" ? a.data : a;
53
- var bData = b && b.type === "session" ? b.data : b;
58
+ var aData = a && (a.type === "session" || a.type === "driver-hierarchy") ? a.data : a;
59
+ var bData = b && (b.type === "session" || b.type === "driver-hierarchy") ? b.data : b;
54
60
  var aBookmarked = !!(aData && aData.bookmarked);
55
61
  var bBookmarked = !!(bData && bData.bookmarked);
56
62
  if (aBookmarked !== bBookmarked) return aBookmarked ? -1 : 1;
@@ -236,6 +242,14 @@ function collectItemSessionIds(item) {
236
242
  if (item.type === "split-group" && Array.isArray(item.members)) {
237
243
  return item.members.map(function (session) { return session.id; });
238
244
  }
245
+ if (item.type === "driver-hierarchy" && item.root) {
246
+ var hierarchyIds = [item.root.driver.id];
247
+ for (var j = 0; j < item.root.workers.length; j++) hierarchyIds.push(item.root.workers[j].id);
248
+ return hierarchyIds;
249
+ }
250
+ if (item.type === "orphan-workers" && Array.isArray(item.workers)) {
251
+ return item.workers.map(function (session) { return session.id; });
252
+ }
239
253
  return [];
240
254
  }
241
255
 
@@ -1246,10 +1260,12 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
1246
1260
 
1247
1261
  // --- Session item rendering ---
1248
1262
 
1249
- function renderSessionItem(s) {
1263
+ function renderSessionItem(s, options) {
1264
+ var itemOptions = options || {};
1250
1265
  var el = document.createElement("div");
1251
1266
  var isMatch = searchMatchIds !== null && searchMatchIds.has(s.id);
1252
1267
  el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
1268
+ if (itemOptions.worker) el.classList.add("session-worker-item");
1253
1269
  el.dataset.sessionId = s.id;
1254
1270
 
1255
1271
  // Keep agent identity separate from the title so each row has a stable
@@ -1281,6 +1297,13 @@ function renderSessionItem(s) {
1281
1297
  textSpan.innerHTML = textHtml;
1282
1298
  el.appendChild(textSpan);
1283
1299
 
1300
+ if (itemOptions.worker) {
1301
+ var workerLabel = document.createElement("span");
1302
+ workerLabel.className = "session-worker-generation" + (itemOptions.current ? " current" : "");
1303
+ workerLabel.textContent = itemOptions.current ? "Current" : (s.workerGeneration ? "Gen " + s.workerGeneration : "Worker");
1304
+ el.appendChild(workerLabel);
1305
+ }
1306
+
1284
1307
  var ageText = relativeTime(s.lastActivity || 0);
1285
1308
  if (ageText) {
1286
1309
  var age = document.createElement("span");
@@ -1325,7 +1348,7 @@ function renderSessionItem(s) {
1325
1348
 
1326
1349
  // Presence avatars (multi-user)
1327
1350
  renderPresenceAvatars(el, String(s.id));
1328
- setupSessionDragHandlers(el, s);
1351
+ if (!itemOptions.worker) setupSessionDragHandlers(el, s);
1329
1352
 
1330
1353
  return el;
1331
1354
  }
@@ -1409,12 +1432,17 @@ export function renderSessionList(sessions) {
1409
1432
  // Group by loopId + date so all runs of the same task on the same day are merged
1410
1433
  var splitGroups = store.get('splitGroups') || [];
1411
1434
  var groupedIds = groupedSessionIds(splitGroups);
1435
+ var hierarchy = prepareSidebarHierarchy(cachedSessions);
1436
+ var hierarchyByDriver = hierarchy.byDriver;
1437
+ var hierarchySessionIds = hierarchy.sessionIds;
1438
+ hierarchySessionIds.forEach(function (id) { groupedIds.delete(id); });
1412
1439
  var sessionsById = new Map();
1413
1440
  for (var si = 0; si < cachedSessions.length; si++) sessionsById.set(cachedSessions[si].id, cachedSessions[si]);
1414
1441
  var loopGroups = {}; // groupKey -> [sessions]
1415
1442
  var normalSessions = [];
1416
1443
  for (var i = 0; i < cachedSessions.length; i++) {
1417
1444
  var s = cachedSessions[i];
1445
+ if (s.sessionRole === "worker") continue;
1418
1446
  if (groupedIds.has(s.id)) continue;
1419
1447
  if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
1420
1448
  // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
@@ -1433,13 +1461,21 @@ export function renderSessionList(sessions) {
1433
1461
  // Build virtual items: normal sessions + one entry per loop group (using latest child's lastActivity)
1434
1462
  var items = [];
1435
1463
  for (var j = 0; j < normalSessions.length; j++) {
1436
- items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1464
+ var driverRoot = hierarchyByDriver.get(normalSessions[j].id);
1465
+ if (driverRoot) items.push({ type: "driver-hierarchy", root: driverRoot, data: driverRoot.driver, lastActivity: driverRoot.lastActivity });
1466
+ else items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1467
+ }
1468
+ if (hierarchy.orphans.length) {
1469
+ var orphanActivity = 0;
1470
+ for (var oi = 0; oi < hierarchy.orphans.length; oi++) orphanActivity = Math.max(orphanActivity, hierarchy.orphans[oi].lastActivity || 0);
1471
+ items.push({ type: "orphan-workers", workers: hierarchy.orphans, lastActivity: orphanActivity });
1437
1472
  }
1438
1473
  for (var sg = 0; sg < splitGroups.length; sg++) {
1439
1474
  var splitGroup = splitGroups[sg];
1440
1475
  var leftMember = sessionsById.get(splitGroup.members[0]);
1441
1476
  var rightMember = sessionsById.get(splitGroup.members[1]);
1442
1477
  if (!leftMember || !rightMember) continue;
1478
+ if (hierarchySessionIds.has(leftMember.id) || hierarchySessionIds.has(rightMember.id)) continue;
1443
1479
  items.push({
1444
1480
  type: "split-group",
1445
1481
  group: splitGroup,
@@ -1470,9 +1506,10 @@ export function renderSessionList(sessions) {
1470
1506
  if (item.type === "session" && item.data && !isSessionVisibleBySearch(item.data.id)) {
1471
1507
  continue;
1472
1508
  }
1509
+ if ((item.type === "driver-hierarchy" || item.type === "orphan-workers") && !hierarchyItemMatches(item, searchMatchIds)) continue;
1473
1510
  if (item.type === "split-group" && searchMatchIds !== null &&
1474
1511
  !searchMatchIds.has(item.members[0].id) && !searchMatchIds.has(item.members[1].id)) continue;
1475
- if (item.type === "session" && item.data && item.data.bookmarked) {
1512
+ if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
1476
1513
  bookmarkedItems.push(item);
1477
1514
  } else {
1478
1515
  regularItems.push(item);
@@ -1489,7 +1526,8 @@ export function renderSessionList(sessions) {
1489
1526
  favoritesContainer.appendChild(emptyHint);
1490
1527
  }
1491
1528
  for (var bi = 0; bi < bookmarkedItems.length; bi++) {
1492
- favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
1529
+ if (bookmarkedItems[bi].type === "driver-hierarchy") favoritesContainer.appendChild(renderDesktopDriverHierarchy(bookmarkedItems[bi].root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
1530
+ else favoritesContainer.appendChild(renderSessionItem(bookmarkedItems[bi].data));
1493
1531
  }
1494
1532
 
1495
1533
  var divider = document.createElement("div");
@@ -1529,6 +1567,10 @@ export function renderSessionList(sessions) {
1529
1567
  }
1530
1568
  } else if (item.type === "split-group") {
1531
1569
  regularContainer.appendChild(renderSplitGroupItem(item.group, item.members));
1570
+ } else if (item.type === "driver-hierarchy") {
1571
+ regularContainer.appendChild(renderDesktopDriverHierarchy(item.root, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
1572
+ } else if (item.type === "orphan-workers") {
1573
+ regularContainer.appendChild(renderDesktopOrphanHierarchy(item.workers, renderSessionItem, function () { renderSessionList(null); }, searchMatchIds));
1532
1574
  } else {
1533
1575
  regularContainer.appendChild(renderSessionItem(item.data));
1534
1576
  }
@@ -0,0 +1,16 @@
1
+ export function workerProposalSelection(msg) {
2
+ var hasSelection = typeof msg.selectedVendor === "string" ||
3
+ typeof msg.selectedModel === "string" || typeof msg.selectedEffort === "string";
4
+ return {
5
+ selected: hasSelection,
6
+ vendor: hasSelection ? (msg.selectedVendor || "") : (msg.recommendedVendor || ""),
7
+ model: hasSelection ? (msg.selectedModel || "") : (msg.recommendedModel || ""),
8
+ effort: hasSelection ? (msg.selectedEffort || "") : (msg.recommendedEffort || "medium"),
9
+ };
10
+ }
11
+
12
+ export function syncWorkerProposalSelection(msg, sync) {
13
+ var selection = workerProposalSelection(msg);
14
+ if (selection.selected && typeof sync === "function") sync(selection);
15
+ return selection;
16
+ }
@@ -2,6 +2,7 @@ import { getWs } from './ws-ref.js';
2
2
  import { addToMessages, scrollToBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
3
3
  import { effortLevelsFor, effortDisplayName } from './app-panels.js';
4
4
  import { iconHtml, refreshIcons } from './icons.js';
5
+ import { workerProposalSelection, syncWorkerProposalSelection } from './worker-proposal-state.js';
5
6
 
6
7
  function optionValue(entry) {
7
8
  if (typeof entry === "string") return entry;
@@ -27,7 +28,7 @@ function planSteps(plan) {
27
28
  }).filter(Boolean).slice(0, 8);
28
29
  }
29
30
 
30
- function buildVendorSelect(msg) {
31
+ function buildVendorSelect(msg, preferred) {
31
32
  var select = document.createElement("select");
32
33
  select.className = "worker-proposal-select worker-proposal-vendor";
33
34
  var installed = (msg.options && msg.options.installedVendors) || [];
@@ -37,7 +38,7 @@ function buildVendorSelect(msg) {
37
38
  option.textContent = VENDOR_NAMES[installed[i]] || installed[i];
38
39
  select.appendChild(option);
39
40
  }
40
- if (installed.indexOf(msg.recommendedVendor) !== -1) select.value = msg.recommendedVendor;
41
+ if (installed.indexOf(preferred) !== -1) select.value = preferred;
41
42
  return select;
42
43
  }
43
44
 
@@ -98,16 +99,30 @@ function statusLabel(status) {
98
99
  if (status === "interrupted") return "Interrupted";
99
100
  if (status === "declined") return "Continuing here";
100
101
  if (status === "error") return "Needs attention";
101
- return "Suggested by Fable";
102
+ return "Awaiting your choice";
103
+ }
104
+
105
+ function decisionLabel(status, autoAccepted) {
106
+ if (autoAccepted) return "Driver recommendation auto-accepted under Full auto";
107
+ if (status === "pending") return "Runtime choice remains with you";
108
+ if (status === "declined") return "Runtime proposal declined by user";
109
+ return "Runtime configuration accepted by user";
102
110
  }
103
111
 
104
112
  function applyState(card, msg) {
105
113
  var status = msg.status || "pending";
106
114
  card.dataset.status = status;
107
- if (msg.autoApproved) card.dataset.autoApproved = "true";
108
- var autoApproved = card.dataset.autoApproved === "true";
115
+ if (msg.autoAccepted || msg.autoApproved) card.dataset.autoAccepted = "true";
116
+ if (msg.autoAccepted === false || msg.autoApproved === false) card.dataset.autoAccepted = "false";
117
+ var autoAccepted = card.dataset.autoAccepted === "true";
118
+ syncWorkerProposalSelection(msg, card._syncWorkerProposalSelection);
109
119
  var badge = card.querySelector(".worker-proposal-status");
110
- if (badge) badge.textContent = statusLabel(status) + (autoApproved ? " · auto-approved" : "");
120
+ if (badge) badge.textContent = statusLabel(status) + (autoAccepted ? " · auto-accepted" : "");
121
+ var decision = card.querySelector(".worker-proposal-decision");
122
+ if (decision) {
123
+ decision.textContent = decisionLabel(status, autoAccepted);
124
+ decision.classList.toggle("automatic", autoAccepted);
125
+ }
111
126
  var error = card.querySelector(".worker-proposal-error");
112
127
  if (error) {
113
128
  error.textContent = msg.error || "";
@@ -120,7 +135,7 @@ function applyState(card, msg) {
120
135
  }
121
136
  setControlsDisabled(card, status !== "pending");
122
137
  var actions = card.querySelector(".worker-proposal-actions");
123
- if (actions) actions.classList.toggle("hidden", autoApproved);
138
+ if (actions) actions.classList.toggle("hidden", autoAccepted);
124
139
  }
125
140
 
126
141
  function sendDecision(card, accepted) {
@@ -150,10 +165,12 @@ export function renderWorkerProposal(msg) {
150
165
  card.className = "worker-proposal-card";
151
166
  card.dataset.proposalId = msg.proposalId;
152
167
  card.dataset.status = msg.status || "pending";
168
+ var runtimeSelection = workerProposalSelection(msg);
153
169
 
154
170
  var header = document.createElement("div");
155
171
  header.className = "worker-proposal-header";
156
- header.innerHTML = '<div class="worker-proposal-mark">' + iconHtml("git-branch") + '</div><div class="worker-proposal-heading"><span class="worker-proposal-kicker">DRIVER / WORKER</span><strong>Let a Worker handle the execution</strong></div><span class="worker-proposal-status"></span>';
172
+ var replacing = msg.action === "replace";
173
+ header.innerHTML = '<div class="worker-proposal-mark">' + iconHtml("git-branch") + '</div><div class="worker-proposal-heading"><span class="worker-proposal-kicker">DRIVER / SPLIT WORKER</span><strong>' + (replacing ? "Configure a replacement Split Worker" : "Configure a Split Worker") + '</strong></div><span class="worker-proposal-status"></span>';
157
174
  card.appendChild(header);
158
175
 
159
176
  var summary = document.createElement("p");
@@ -173,12 +190,24 @@ export function renderWorkerProposal(msg) {
173
190
  card.appendChild(plan);
174
191
  }
175
192
 
193
+ if (msg.recommendationRationale) {
194
+ var rationale = document.createElement("div");
195
+ rationale.className = "worker-proposal-rationale";
196
+ var rationaleLabel = document.createElement("span");
197
+ rationaleLabel.textContent = "Driver's recommendation rationale";
198
+ var rationaleText = document.createElement("p");
199
+ rationaleText.textContent = msg.recommendationRationale;
200
+ rationale.appendChild(rationaleLabel);
201
+ rationale.appendChild(rationaleText);
202
+ card.appendChild(rationale);
203
+ }
204
+
176
205
  var config = document.createElement("div");
177
206
  config.className = "worker-proposal-config";
178
207
  var vendorField = document.createElement("label");
179
208
  vendorField.className = "worker-proposal-field worker-proposal-vendor-field";
180
209
  vendorField.innerHTML = '<span>Worker</span><span class="worker-proposal-vendor-control"><img alt=""></span>';
181
- var vendorSelect = buildVendorSelect(msg);
210
+ var vendorSelect = buildVendorSelect(msg, runtimeSelection.vendor);
182
211
  vendorField.querySelector(".worker-proposal-vendor-control").appendChild(vendorSelect);
183
212
  config.appendChild(vendorField);
184
213
  var modelField = document.createElement("label");
@@ -194,6 +223,10 @@ export function renderWorkerProposal(msg) {
194
223
  config.appendChild(effortField);
195
224
  card.appendChild(config);
196
225
 
226
+ var decision = document.createElement("div");
227
+ decision.className = "worker-proposal-decision";
228
+ card.appendChild(decision);
229
+
197
230
  function syncVendor(preferredModel, preferredEffort) {
198
231
  var vendor = vendorSelect.value;
199
232
  var avatar = vendorField.querySelector("img");
@@ -201,12 +234,16 @@ export function renderWorkerProposal(msg) {
201
234
  fillModels(modelSelect, vendor, msg, preferredModel);
202
235
  fillEffort(card, vendor, modelSelect.value, msg, preferredEffort);
203
236
  }
237
+ card._syncWorkerProposalSelection = function (selection) {
238
+ if (selection.vendor && vendorSelect.value !== selection.vendor) vendorSelect.value = selection.vendor;
239
+ syncVendor(selection.model, selection.effort);
240
+ };
204
241
  vendorSelect.addEventListener("change", function () { syncVendor("", "medium"); });
205
242
  modelSelect.addEventListener("change", function () {
206
243
  var active = card.querySelector(".worker-proposal-effort-btn.active");
207
244
  fillEffort(card, vendorSelect.value, modelSelect.value, msg, active ? active.dataset.effort : "medium");
208
245
  });
209
- syncVendor(msg.recommendedModel || "", msg.recommendedEffort || "medium");
246
+ syncVendor(runtimeSelection.model, runtimeSelection.effort);
210
247
 
211
248
  var error = document.createElement("div");
212
249
  error.className = "worker-proposal-error hidden";
@@ -220,12 +257,12 @@ export function renderWorkerProposal(msg) {
220
257
  var decline = document.createElement("button");
221
258
  decline.type = "button";
222
259
  decline.className = "worker-proposal-action secondary";
223
- decline.textContent = "Continue here";
260
+ decline.textContent = replacing ? "Keep current Worker" : "Continue here";
224
261
  decline.addEventListener("click", function () { sendDecision(card, false); });
225
262
  var accept = document.createElement("button");
226
263
  accept.type = "button";
227
264
  accept.className = "worker-proposal-action primary";
228
- accept.innerHTML = iconHtml("panel-right-open") + "<span>Run with Split Worker</span>";
265
+ accept.innerHTML = iconHtml("panel-right-open") + "<span>" + (replacing ? "Replace Split Worker" : "Run with Split Worker") + "</span>";
229
266
  accept.addEventListener("click", function () { sendDecision(card, true); });
230
267
  actions.appendChild(decline);
231
268
  actions.appendChild(accept);
package/lib/sdk-bridge.js CHANGED
@@ -605,14 +605,12 @@ function createSDKBridge(opts) {
605
605
  return { behavior: "allow", updatedInput: input };
606
606
  }
607
607
 
608
- // Auto-approve the visible Driver/Split Worker pair tools, including the
609
- // Driver's answer to a Worker permission request. The pair is
610
- // user-constructed and both panes are on screen; every one of these tools
611
- // is structurally confined to the caller's own exact pair and re-checks
612
- // Driver eligibility, ownership and the live pair inside its handler, so
613
- // the whitelist grants no authority the handler would not already demand.
614
- // A prompt on each hop would reinstate exactly the approval gate the
615
- // autonomous lifecycle exists to remove.
608
+ // Auto-approve the non-mutating configuration proposal and the visible
609
+ // Driver/Split Worker pair tools, including the Driver's answer to a Worker
610
+ // permission request. Creation and replacement still require a separate
611
+ // human decision in the proposal card; skip-permissions is never treated as
612
+ // that product choice. Every handler is confined to the caller's exact
613
+ // session/pair and re-checks eligibility, ownership and live identity.
616
614
  //
617
615
  // spawn_sessions is deliberately absent: it creates arbitrary sessions.
618
616
  //
@@ -628,6 +626,7 @@ function createSDKBridge(opts) {
628
626
  // can legitimately arrive bare.
629
627
  var PAIR_MCP_PREFIX = "mcp__clay-sessions__";
630
628
  var pairMcpTools = {
629
+ propose_worker: true,
631
630
  send_to_partner: true,
632
631
  read_partner: true,
633
632
  partner_status: true,
@@ -638,6 +637,7 @@ function createSDKBridge(opts) {
638
637
  respond_to_worker_permission: true,
639
638
  };
640
639
  var pairDynamicTools = {
640
+ propose_worker: true,
641
641
  partner_status: true,
642
642
  replace_partner: true,
643
643
  interrupt_partner: true,
@@ -664,6 +664,14 @@ function createSDKBridge(opts) {
664
664
  return { behavior: "allow", updatedInput: input };
665
665
  }
666
666
 
667
+ // Project Logs are exact-session, project-bound tools whose authority is
668
+ // enforced again by the server binding. Canonical changes are append-only
669
+ // revisions, and the surface exposes no destructive delete operation, so
670
+ // a second provider permission prompt adds no additional safety.
671
+ if (toolName.indexOf("mcp__clay-logs__") === 0) {
672
+ return { behavior: "allow", updatedInput: input };
673
+ }
674
+
667
675
  // This tool only prepares a read-only UI projection for the document the
668
676
  // agent is about to edit. The subsequent Edit/Write keeps its own normal
669
677
  // permission behavior.
@@ -3,6 +3,8 @@ var yoke = require("./yoke");
3
3
  var backgroundTaskTiming = require("./background-task-timing");
4
4
  var sessionTitlePolicy = require("./session-title-policy");
5
5
 
6
+ var OVERLOAD_RETRY_LIMIT = 3;
7
+
6
8
  function attachMessageProcessor(ctx) {
7
9
  var sm = ctx.sm;
8
10
  var send = ctx.send;
@@ -487,6 +489,8 @@ function attachMessageProcessor(ctx) {
487
489
  }
488
490
 
489
491
  } else if (parsed.yokeType === "result") {
492
+ var overloadAbortRequested = !!session._overloadAbortRequested;
493
+ session._overloadAbortRequested = false;
490
494
  session.blocks = {};
491
495
  session.sentToolResults = {};
492
496
  session.pendingPermissions = {};
@@ -532,7 +536,9 @@ function attachMessageProcessor(ctx) {
532
536
  session._awaitingTurnResult = false;
533
537
  session._queuedTurnCount = 0;
534
538
  onProcessingChanged();
535
- sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
539
+ if (!overloadAbortRequested) {
540
+ sendAndRecord(session, { type: "error", text: "Claude error: " + execError });
541
+ }
536
542
  sendAndRecord(session, { type: "done", code: 1 });
537
543
  sm.broadcastSessionList();
538
544
  return;
@@ -819,9 +825,33 @@ function attachMessageProcessor(ctx) {
819
825
  }
820
826
 
821
827
  } else if (parsed.yokeType === "api_retry") {
822
- // Transient retry notification, show in UI but don't persist in history
823
- var retryText = parsed.message || parsed.error || "Retrying API request...";
824
- sendToSession(session, { type: "system_info", text: retryText });
828
+ // The Claude SDK may otherwise paint one persistent red error for every
829
+ // retry and remain busy for many minutes. Show one transient status, and
830
+ // stop a repeatedly overloaded turn after a small bounded number of
831
+ // attempts so the user regains control and can retry or change models.
832
+ var apiRetryAttempt = parsed.attempt || 1;
833
+ if (parsed.error === "overloaded") {
834
+ if (apiRetryAttempt === 1) {
835
+ session._overloadAbortRequested = false;
836
+ sendToSession(session, {
837
+ type: "system_info",
838
+ text: "The model is overloaded. Retrying briefly...",
839
+ });
840
+ }
841
+ if (apiRetryAttempt >= OVERLOAD_RETRY_LIMIT && !session._overloadAbortRequested) {
842
+ session._overloadAbortRequested = true;
843
+ sendAndRecord(session, {
844
+ type: "error",
845
+ text: "The model remained overloaded after " + apiRetryAttempt +
846
+ " attempts, so this turn was stopped. Try again or choose another model.",
847
+ });
848
+ if (session.abortController && typeof session.abortController.abort === "function") {
849
+ try { session.abortController.abort(); } catch (e) {}
850
+ }
851
+ }
852
+ } else if (apiRetryAttempt === 1) {
853
+ sendToSession(session, { type: "system_info", text: "The model request failed temporarily. Retrying..." });
854
+ }
825
855
 
826
856
  } else if (parsed.yokeType === "commands_changed") {
827
857
  // Mid-session command list change: rebuild the union with skills (same as