clay-server 4.0.0-beta.9 → 4.0.1-beta.1

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 (188) 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 +71 -16
  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 +191 -6
  60. package/lib/public/app.js +64 -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 +45 -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-message-router.js +1 -1
  85. package/lib/public/modules/app-messages.js +92 -11
  86. package/lib/public/modules/app-misc.js +2 -14
  87. package/lib/public/modules/app-notifications.js +11 -7
  88. package/lib/public/modules/app-panels.js +3 -2
  89. package/lib/public/modules/app-projects.js +56 -18
  90. package/lib/public/modules/app-rendering.js +1 -0
  91. package/lib/public/modules/capsule-preference.js +44 -0
  92. package/lib/public/modules/chat-bubble-renderer.js +1 -1
  93. package/lib/public/modules/clay-log-links.js +70 -0
  94. package/lib/public/modules/command-palette.js +52 -15
  95. package/lib/public/modules/context-sources.js +0 -3
  96. package/lib/public/modules/git-agent-sessions.js +85 -0
  97. package/lib/public/modules/git-panel.js +111 -84
  98. package/lib/public/modules/git-placard.js +161 -0
  99. package/lib/public/modules/home-chat-empty-state.js +39 -0
  100. package/lib/public/modules/home-chat-identity.js +9 -0
  101. package/lib/public/modules/home-conversations-sheet.js +102 -48
  102. package/lib/public/modules/home-mate-chat.js +15 -35
  103. package/lib/public/modules/home-session-actions.js +6 -0
  104. package/lib/public/modules/home-sidebar-chat-list.js +101 -41
  105. package/lib/public/modules/home-sidebar.js +7 -0
  106. package/lib/public/modules/home-surface-boot.js +7 -3
  107. package/lib/public/modules/home-tool-frame.js +182 -0
  108. package/lib/public/modules/home-tools.js +138 -1
  109. package/lib/public/modules/input.js +38 -7
  110. package/lib/public/modules/markdown.js +2 -0
  111. package/lib/public/modules/mate-sidebar.js +0 -8
  112. package/lib/public/modules/message-delivery.js +97 -0
  113. package/lib/public/modules/paste-modal.js +84 -0
  114. package/lib/public/modules/project-activation.js +45 -0
  115. package/lib/public/modules/project-logs-render.js +405 -0
  116. package/lib/public/modules/project-logs.js +461 -0
  117. package/lib/public/modules/project-removal-target.js +26 -0
  118. package/lib/public/modules/scheduler.js +17 -3
  119. package/lib/public/modules/search-clay-chat.js +47 -6
  120. package/lib/public/modules/search-clay-controls.js +86 -0
  121. package/lib/public/modules/session-actions.js +29 -105
  122. package/lib/public/modules/session-hierarchy.js +54 -0
  123. package/lib/public/modules/sidebar-mobile.js +40 -15
  124. package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
  125. package/lib/public/modules/sidebar-sessions.js +77 -11
  126. package/lib/public/modules/sidebar.js +17 -5
  127. package/lib/public/modules/split-group-helpers.js +8 -0
  128. package/lib/public/modules/split-pair-ui.js +15 -4
  129. package/lib/public/modules/split-view.js +5 -2
  130. package/lib/public/modules/split-worker-runtime.js +24 -0
  131. package/lib/public/modules/sticky-note-markdown.js +5 -0
  132. package/lib/public/modules/sticky-notes-browser.js +326 -0
  133. package/lib/public/modules/sticky-notes-card.js +360 -0
  134. package/lib/public/modules/sticky-notes-editor.js +291 -0
  135. package/lib/public/modules/sticky-notes-shared.js +90 -0
  136. package/lib/public/modules/sticky-notes.js +170 -946
  137. package/lib/public/modules/thinking-lifecycle.js +175 -0
  138. package/lib/public/modules/thinking-summary.js +28 -0
  139. package/lib/public/modules/thinking-view.js +75 -0
  140. package/lib/public/modules/tool-palette-order.js +143 -0
  141. package/lib/public/modules/tool-palette-overlays.js +159 -0
  142. package/lib/public/modules/tool-palette.js +21 -188
  143. package/lib/public/modules/tools.js +31 -123
  144. package/lib/public/modules/update-snooze.js +169 -0
  145. package/lib/public/modules/user-settings.js +4 -0
  146. package/lib/public/modules/worker-pane-lock.js +176 -0
  147. package/lib/public/modules/worker-proposal-state.js +16 -0
  148. package/lib/public/modules/worker-proposal.js +49 -20
  149. package/lib/public/style.css +3 -1
  150. package/lib/sdk-bridge.js +136 -47
  151. package/lib/sdk-message-processor.js +36 -7
  152. package/lib/server-experimental-settings.js +49 -0
  153. package/lib/server-global-ws.js +11 -0
  154. package/lib/server-home-chat-events.js +34 -1
  155. package/lib/server-home-chat.js +62 -4
  156. package/lib/server-home-clay-entry.js +1 -1
  157. package/lib/server-home-clay-session-links.js +34 -5
  158. package/lib/server-settings.js +5 -0
  159. package/lib/server-tools.js +127 -9
  160. package/lib/server.js +137 -22
  161. package/lib/session-driver-eligibility.js +38 -0
  162. package/lib/session-driver-orchestration.js +31 -0
  163. package/lib/session-hygiene.js +3 -0
  164. package/lib/session-notes-mcp-server.js +25 -5
  165. package/lib/session-pair-factory.js +247 -0
  166. package/lib/session-pair-mcp-server.js +46 -8
  167. package/lib/session-pair-prompts.js +79 -0
  168. package/lib/session-pair-turn-control.js +153 -0
  169. package/lib/session-provenance.js +119 -0
  170. package/lib/session-spawn-mcp-server.js +1 -1
  171. package/lib/session-split-groups.js +4 -1
  172. package/lib/session-title-policy.js +60 -0
  173. package/lib/session-visibility.js +41 -0
  174. package/lib/sessions.js +57 -10
  175. package/lib/tools-registry.js +89 -10
  176. package/lib/update-snooze.js +437 -0
  177. package/lib/users-experimental-preferences.js +30 -0
  178. package/lib/users-permissions.js +6 -6
  179. package/lib/users.js +22 -16
  180. package/lib/workspace-query-access.js +102 -0
  181. package/lib/workspace-query-service.js +24 -17
  182. package/lib/worktree.js +25 -38
  183. package/lib/ws-schema.js +44 -7
  184. package/lib/yoke/adapters/claude-worker.js +16 -6
  185. package/lib/yoke/adapters/claude.js +12 -0
  186. package/lib/yoke/adapters/codex.js +24 -14
  187. package/lib/yoke/vendor-registry.js +1 -1
  188. package/package.json +2 -2
@@ -1,20 +1,19 @@
1
1
  var pairMcp = require("./session-pair-mcp-server");
2
+ var pairPrompts = require("./session-pair-prompts");
3
+ var { attachPairFactory } = require("./session-pair-factory");
4
+ var { attachPairLifecycle } = require("./project-pair-lifecycle");
5
+ var { attachPairTurnControl } = require("./session-pair-turn-control");
6
+ var driverEligibility = require("./session-driver-eligibility"), driverOrchestration = require("./session-driver-orchestration");
7
+ var { attachWorkerPermission } = require("./project-worker-permission");
2
8
  var { attachWorkerProposal } = require("./project-worker-proposal");
3
- var yoke = require("./yoke");
4
-
5
9
  var MAX_RESPONSE_CHARS = 30000;
6
-
7
- function toolResult(value) {
8
- return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
9
- }
10
-
10
+ function toolResult(value) { return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] }); }
11
11
  function toolError(err) {
12
12
  return Promise.resolve({
13
13
  content: [{ type: "text", text: "Error: " + (err.message || err) }],
14
14
  isError: true,
15
15
  });
16
16
  }
17
-
18
17
  function responseText(history, fromIndex) {
19
18
  var text = "";
20
19
  for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
@@ -59,11 +58,17 @@ function attachSessionPair(ctx) {
59
58
  var sm = ctx.sm;
60
59
  var store = ctx.splitStore;
61
60
  var workerProposal;
62
-
63
- function groupAndPartner(caller) {
61
+ var workerPermission;
62
+ var lifecycle;
63
+ var turnControl = attachPairTurnControl({ sm: sm, splitStore: store });
64
+ function groupAndPartner(caller, missingPairMessage) {
64
65
  if (!caller) throw new Error("partner tools require a session-bound tool server");
66
+ // Captured handlers must remain bound to the exact live Session object.
67
+ if (sm.sessions.get(caller.localId) !== caller) {
68
+ throw new Error("this session is no longer live; the partner tools are bound to an exact session");
69
+ }
65
70
  var group = store.groupForMember(caller.localId);
66
- if (!group) throw new Error("this session is not in a split group");
71
+ if (!group) throw new Error(missingPairMessage || "this session is not in a split group");
67
72
  if (group.pair && group.pair.driverId !== caller.localId) {
68
73
  throw new Error("only the configured Driver can direct this pair");
69
74
  }
@@ -93,27 +98,19 @@ function attachSessionPair(ctx) {
93
98
  broadcastDelegation(group, caller, partner, false);
94
99
  }
95
100
 
96
- function resumeDriverWithResult(caller, partner, token) {
97
- if (!token.detached || token.delivered || caller.destroying) return false;
98
- token.delivered = true;
99
- var failure = token.failure || null;
100
- var response = token.response || "";
101
- var text;
102
- if (token.interrupted) {
103
- text = "[Split Worker execution interrupted] The user interrupted the Split 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 Split Worker task delegated through send_to_partner has finished.\n\n" +
106
- "Original task:\n" + token.message + "\n\n" +
107
- (failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
108
- "\n\nReview the result, verify it as needed, and continue the task.";
109
- }
110
- sm.sendAndRecord(caller, {
101
+ function resumeDriverWithMessage(caller, text, meta) {
102
+ if (!caller || caller.destroying) return false;
103
+ if (turnControl.blockedReason(caller)) return false;
104
+ var record = {
111
105
  type: "user_message",
112
106
  text: text,
113
107
  _internal: true,
114
- partnerResult: true,
115
- partnerSessionId: partner.localId,
116
- });
108
+ };
109
+ if (meta) {
110
+ var keys = Object.keys(meta);
111
+ for (var i = 0; i < keys.length; i++) record[keys[i]] = meta[keys[i]];
112
+ }
113
+ sm.sendAndRecord(caller, record);
117
114
  caller.lastActivity = Date.now();
118
115
  var sdk = ctx.getSdk();
119
116
  if (!sdk) {
@@ -138,6 +135,33 @@ function attachSessionPair(ctx) {
138
135
  return true;
139
136
  }
140
137
 
138
+ function resumeDriverWithResult(caller, partner, token) {
139
+ if (!token.detached || token.delivered || caller.destroying) return false;
140
+ token.delivered = true;
141
+ var failure = token.failure || null;
142
+ var response = token.response || "";
143
+ var text;
144
+ if (token.interrupted) {
145
+ text = "[Split Worker execution interrupted] The user interrupted the Split 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.";
146
+ } else {
147
+ text = "A Split Worker task delegated through send_to_partner has finished.\n\n" +
148
+ "Original task:\n" + token.message + "\n\n" +
149
+ (failure ? "Split Worker error:\n" + failure : "Split Worker result:\n" + (response || "(No text response was recorded.)")) +
150
+ "\n\nReview the result, verify it as needed, and continue the task.";
151
+ }
152
+ return resumeDriverWithMessage(caller, text, {
153
+ partnerResult: true,
154
+ partnerSessionId: partner.localId,
155
+ });
156
+ }
157
+
158
+ function requestDetach(worker) {
159
+ var token = worker && worker._pairDelegation;
160
+ if (!token || token.detached || token.detachRequested) return false;
161
+ token.detachRequested = true;
162
+ return true;
163
+ }
164
+
141
165
  function handleTurnDone(partner) {
142
166
  var token = partner && partner._pairDelegation;
143
167
  if (!token) return false;
@@ -179,7 +203,8 @@ function attachSessionPair(ctx) {
179
203
  });
180
204
  return;
181
205
  }
182
- if (Date.now() >= deadline) {
206
+ // Timeouts and permission requests both detach into the push-back path.
207
+ if (Date.now() >= deadline || token.detachRequested) {
183
208
  clearInterval(timer);
184
209
  token.detached = true;
185
210
  monitorPartner(group, caller, partner, token);
@@ -203,13 +228,11 @@ function attachSessionPair(ctx) {
203
228
  async function sendToPartner(args, caller) {
204
229
  try {
205
230
  if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
231
+ var resolved = groupAndPartner(caller,
232
+ "send_to_partner requires an exact existing Driver/Split Worker pair; call propose_worker while unpaired");
206
233
  var message = typeof args.message === "string" ? args.message.trim() : "";
207
234
  if (!message) throw new Error("message is required");
208
- var created = null;
209
- if (caller && !store.groupForMember(caller.localId)) {
210
- created = createWorkerForDriver(caller, args);
211
- }
212
- var resolved = groupAndPartner(caller);
235
+ turnControl.assertWorkerAction(caller);
213
236
  var wait = args.wait !== false;
214
237
  var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
215
238
  timeout = Math.max(1, Math.min(900, timeout));
@@ -247,13 +270,18 @@ function attachSessionPair(ctx) {
247
270
  if (!wait) {
248
271
  token.detached = true;
249
272
  monitorPartner(resolved.group, caller, partner, token);
250
- return toolResult({ status: "running", partnerId: partner.localId, workerCreated: !!created, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
273
+ return toolResult({ status: "running", partnerId: partner.localId, hint: "The completed result will be pushed back automatically. Use read_partner only for an interim status check." });
251
274
  }
252
- var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
253
- if (created) {
254
- completed.workerCreated = true;
255
- completed.partnerId = partner.localId;
275
+ if (!partner.isProcessing && !partner._queryStarting) {
276
+ finishDelegation(resolved.group, caller, partner, token);
277
+ var immediateFailure = errorSince(partner.history || [], token.startIndex);
278
+ return toolResult({
279
+ status: immediateFailure ? "error" : "complete",
280
+ response: responseText(partner.history || [], token.startIndex),
281
+ error: immediateFailure || undefined,
282
+ });
256
283
  }
284
+ var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
257
285
  return toolResult(completed);
258
286
  } catch (e) {
259
287
  if (caller) {
@@ -274,13 +302,15 @@ function attachSessionPair(ctx) {
274
302
  try {
275
303
  var resolved = groupAndPartner(caller);
276
304
  var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
277
- count = Math.max(1, Math.min(5, count));
278
- return toolResult({
305
+ count = Math.max(0, Math.min(5, count));
306
+ var payload = {
279
307
  status: resolved.partner.isProcessing ? "running" : (resolved.partner._lastTurnInterrupted ? "interrupted" : "idle"),
280
308
  partnerId: resolved.partner.localId,
281
309
  title: resolved.partner.title || "New Session",
282
- turns: recentTurns(resolved.partner, count),
283
- });
310
+ turns: count > 0 ? recentTurns(resolved.partner, count) : [],
311
+ };
312
+ payload.capacity = lifecycle.optionalStatus(caller);
313
+ return toolResult(payload);
284
314
  } catch (e) {
285
315
  return toolError(e);
286
316
  }
@@ -311,6 +341,7 @@ function attachSessionPair(ctx) {
311
341
  if (partner.abortController) partner.abortController.abort();
312
342
  }
313
343
  if (partner._pairDelegation) finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
344
+ workerPermission.cancelForSession(partner, "The Driver closed the Split Worker pair.");
314
345
  var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
315
346
  var result = store.dissolve(ws, { id: resolved.group.id });
316
347
  if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
@@ -320,115 +351,85 @@ function attachSessionPair(ctx) {
320
351
  }
321
352
  }
322
353
 
354
+ function handleHumanStop(session) {
355
+ var roles = turnControl.markHumanStop(session);
356
+ if (!roles) return false;
357
+ workerPermission.cancelForSession(roles.worker, "The human stopped this Split Worker turn.");
358
+ return true;
359
+ }
360
+
323
361
  function getToolDefs(boundSession) {
324
362
  if (!boundSession) return pairMcp.getToolDefs({
325
363
  send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
326
364
  read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
327
365
  interrupt: function () { return toolError(new Error("interrupt_partner requires a session-bound tool server")); },
328
366
  close: function () { return toolError(new Error("close_partner requires a session-bound tool server")); },
367
+ status: function () { return toolError(new Error("partner_status requires a session-bound tool server")); },
368
+ replace: function () { return toolError(new Error("replace_partner requires a session-bound tool server")); },
369
+ evaluate: function () { return toolError(new Error("record_partner_evaluation requires a session-bound tool server")); },
329
370
  });
330
- // Mount whenever we have a bound session: MCP servers are fixed for the
331
- // lifetime of a query, and Claude queries live across turns, so gating on
332
- // group membership HERE would permanently hide the tools from a session
333
- // whose split is created mid-conversation (the ad-hoc flow). Handlers
334
- // re-resolve the group on every call, so an ungrouped session gets a
335
- // clear error, and a session that gains a partner later just works.
336
- // The one structural exclusion: a session that is ALREADY a configured
337
- // pair worker at query start never sees the tools.
338
371
  var group = store.groupForMember(boundSession.localId);
339
372
  if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
373
+ var adHocSplit = !!(group && !group.pair);
374
+ if (!driverEligibility.isEligibleDriverSession(boundSession, sm)) return [];
375
+ var persistentCatalog = boundSession.vendor === "codex" && (!group || !!group.pair);
376
+ if (!group && !persistentCatalog) {
377
+ return workerProposal.getToolDefs(boundSession)
378
+ .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
379
+ }
380
+ var lifecycleHandlers = lifecycle.toolHandlers(boundSession);
340
381
  var tools = pairMcp.getToolDefs({
341
- send: function (args) { return sendToPartner(args, boundSession); },
382
+ send: function (args) {
383
+ return turnControl.runOperation(boundSession, "send", args && args.operationId, function () {
384
+ return sendToPartner(args || {}, boundSession);
385
+ });
386
+ },
342
387
  read: function (args) { return readPartner(args, boundSession); },
343
388
  interrupt: function (args) { return interruptPartner(args, boundSession); },
344
389
  close: function (args) { return closePartner(args, boundSession); },
345
- });
346
- return tools.concat(workerProposal.getToolDefs(boundSession));
347
- }
348
-
349
- function validateVendor(vendor) {
350
- var installed = sm.installedVendors || [];
351
- if (!vendor || installed.indexOf(vendor) === -1) throw new Error("vendor is not installed: " + (vendor || "unknown"));
352
- return vendor;
353
- }
354
-
355
- function createPairRecord(ws, msg) {
356
- if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
357
- var driverSpec = msg.driver || {};
358
- var workerSpec = msg.worker || {};
359
- var workerVendor = validateVendor(workerSpec.vendor || sm.lastVendor || "codex");
360
- var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
361
- var driver;
362
- var groupName;
363
- if (Number.isInteger(driverSpec.sessionId)) {
364
- // "Add Split Worker" on an existing session: the current session becomes
365
- // the Driver with its full conversation context intact.
366
- driver = sm.sessions.get(driverSpec.sessionId);
367
- if (!driver) throw new Error("driver session not found");
368
- if ((driver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
369
- if (store.groupForMember(driver.localId)) throw new Error("this session is already in a split group");
370
- groupName = undefined; // auto name from member titles
371
- } else {
372
- var driverVendor = validateVendor(driverSpec.vendor || "claude");
373
- var driverEffort = yoke.clampEffort(driverVendor, driverSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[driverVendor]) || sm.currentEffort || "medium") || null;
374
- driver = sm.createSessionRaw({ ownerId: ownerId, vendor: driverVendor, model: driverSpec.model || null, effort: driverEffort });
375
- driver.title = "Driver · " + ((yoke.getVendorInfo(driverVendor) || {}).displayName || driverVendor);
376
- if (driverSpec.effort) driver.loopSettings = { effort: driverSpec.effort };
377
- groupName = "Agent pair";
390
+ status: lifecycleHandlers.status,
391
+ replace: function (args) {
392
+ return turnControl.runOperation(boundSession, "replace", args && args.operationId, function () {
393
+ return workerProposal.proposeReplacement(args || {}, boundSession);
394
+ });
395
+ },
396
+ evaluate: lifecycleHandlers.evaluate,
397
+ }, { lifecycle: persistentCatalog || !adHocSplit });
398
+ if (persistentCatalog) {
399
+ return workerProposal.getToolDefs(boundSession, { persistent: true })
400
+ .concat(tools)
401
+ .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: true }));
378
402
  }
379
- var workerEffort = yoke.clampEffort(workerVendor, workerSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[workerVendor]) || sm.currentEffort || "medium") || null;
380
- var worker = sm.createSessionRaw({ ownerId: ownerId, vendor: workerVendor, model: workerSpec.model || null, effort: workerEffort });
381
- worker.title = "Split Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
382
- if (workerSpec.effort) worker.loopSettings = { effort: workerSpec.effort };
383
- var result = store.create(ws, {
384
- members: [driver.localId, worker.localId],
385
- pair: { driverId: driver.localId, workerId: worker.localId },
386
- name: groupName,
387
- });
388
- if (!result.ok) throw new Error(result.error);
389
- sm.broadcastSessionList();
390
- return { driver: driver, worker: worker, group: result.group };
403
+ return tools
404
+ .concat(workerPermission.getToolDefs(boundSession, { dormantDriver: false }));
391
405
  }
392
406
 
393
- function createWorkerForDriver(driver, args) {
394
- if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
395
- var installed = sm.installedVendors || [];
396
- if (installed.length === 0) throw new Error("no coding agent is installed for a Split Worker session");
397
- var vendor = typeof args.workerVendor === "string" ? args.workerVendor.trim() : "";
398
- if (vendor) validateVendor(vendor);
399
- if (!vendor) {
400
- if (installed.indexOf("codex") !== -1 && driver.vendor !== "codex") vendor = "codex";
401
- else {
402
- vendor = installed[0];
403
- for (var i = 0; i < installed.length; i++) {
404
- if (installed[i] !== driver.vendor) { vendor = installed[i]; break; }
405
- }
406
- }
407
- }
408
- var ws = {
409
- _clayActiveSession: driver.localId,
410
- _clayUser: driver.ownerId ? { id: driver.ownerId } : null,
411
- };
412
- var created = createPairRecord(ws, {
413
- driver: { sessionId: driver.localId },
414
- worker: {
415
- vendor: vendor,
416
- model: typeof args.workerModel === "string" ? args.workerModel : "",
417
- effort: typeof args.workerEffort === "string" ? args.workerEffort : "",
418
- },
419
- });
420
- sm.sendToSession(driver, { type: "pair_session_created", ok: true, group: created.group });
421
- return created;
422
- }
407
+ var factory = attachPairFactory(ctx);
408
+ var createPairRecord = factory.createPairRecord;
409
+ var createWorkerForDriver = factory.createWorkerForDriver;
410
+ var createPair = factory.createPair;
411
+ var preflightWorkerForDriver = factory.preflightWorkerForDriver;
423
412
 
424
- function createPair(ws, msg) {
425
- try {
426
- var created = createPairRecord(ws, msg);
427
- ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: created.group });
428
- } catch (e) {
429
- ctx.sendTo(ws, { type: "pair_session_created", ok: false, error: e.message || String(e) });
430
- }
431
- }
413
+ lifecycle = attachPairLifecycle({
414
+ sm: sm,
415
+ splitStore: store,
416
+ createWorkerForDriver: function (driver, args) { return createWorkerForDriver(driver, args); },
417
+ preflightWorkerForDriver: function (driver, args) { return preflightWorkerForDriver(driver, args); },
418
+ sendToPartner: function (args, caller) { return sendToPartner(args, caller); },
419
+ finishDelegation: finishDelegation,
420
+ cancelWorkerPermissions: function (worker, reason) {
421
+ return workerPermission.cancelForSession(worker, reason);
422
+ },
423
+ turnControl: turnControl,
424
+ });
425
+
426
+ workerPermission = attachWorkerPermission({
427
+ sm: sm,
428
+ splitStore: store,
429
+ onProcessingChanged: ctx.onProcessingChanged,
430
+ resumeDriverWithMessage: resumeDriverWithMessage,
431
+ requestDetach: requestDetach,
432
+ });
432
433
 
433
434
  workerProposal = attachWorkerProposal({
434
435
  sm: sm,
@@ -440,8 +441,11 @@ function attachSessionPair(ctx) {
440
441
  getLinuxUserForSession: ctx.getLinuxUserForSession,
441
442
  onProcessingChanged: ctx.onProcessingChanged,
442
443
  adapters: ctx.adapters,
444
+ dangerouslySkipPermissions: ctx.dangerouslySkipPermissions,
443
445
  createPairRecord: createPairRecord,
446
+ recordGenerationStart: function (driver, worker) { return lifecycle.recordGenerationStart(driver, worker); },
444
447
  sendToPartner: sendToPartner,
448
+ replacePartner: function (args, session) { return lifecycle.replacePartner(args, session); },
445
449
  });
446
450
 
447
451
  function handleMessage(ws, msg) {
@@ -468,18 +472,27 @@ function attachSessionPair(ctx) {
468
472
  var group = store.groupForMember(session.localId);
469
473
  var pairPrompt = "";
470
474
  if (group && group.pair && group.pair.driverId === session.localId) {
471
- pairPrompt = "You are the Driver in a two-agent pair. The tools send_to_partner, read_partner, interrupt_partner, and close_partner are provided directly to you. Infer the user's intended target from conversational and UI context. References to the user-visible paired or split pane, its session, its activity or status, or a collaborator the user opened resolve to Clay's Split Worker and the appropriate partner tool. Internal Sub-agents are a distinct execution mechanism, not a lexical category; use them only when the user clearly intends internal or background parallel delegation rather than the visible paired session. When ambiguous and a visible pair exists, prefer the visible Split Worker. If the ambiguity would materially change where work runs, ask a concise clarification instead of guessing. Use send_to_partner to delegate concrete bounded work to the Split Worker. The human may also message or stop the Split Worker directly; treat that intervention as authoritative. Use close_partner immediately when the human asks to close, dismiss, or remove the Split Worker pane; it preserves the session history. A completed Split Worker turn leaves the Split Worker session available for more work. Use interrupt_partner when execution must stop or change direction but the pair should remain open; its work is partial and unverified until you review it. If review or user feedback requires corrections to the Split Worker's implementation, delegate a follow-up turn to that Split Worker instead of editing the Split Worker-owned files yourself. If the Split Worker is no longer available, keep the work in the visible Split Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background Sub-agent. 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.";
472
- } else if (!group && !ctx.isMate) {
473
- pairPrompt = "Infer the user's intended target from conversational and UI context. References to a user-visible paired or split pane, its session, its activity or status, or a collaborator the user wants opened resolve to Clay's Split Worker and its partner tools. Internal Sub-agents are a distinct execution mechanism, not a lexical category; use them only when the user clearly intends internal or background parallel delegation rather than a visible paired session. When ambiguity would materially change where work runs, ask a concise clarification instead of guessing. When the user intends a visible collaborator, call send_to_partner directly; Clay will create the paired Split Worker session and open it in the right pane automatically before delivering the task. Do not ask the user to start split mode and do not use a background Sub-agent as a substitute for a visible Split Worker.";
475
+ pairPrompt = pairPrompts.DRIVER_CORE;
476
+ if (driverOrchestration.isHighTierDriverSession(session, sm)) {
477
+ pairPrompt += " " + pairPrompts.DRIVER_DELEGATION;
478
+ }
479
+ } else if (!group && !ctx.isMate && driverEligibility.isEligibleDriverSession(session, sm) &&
480
+ driverOrchestration.isHighTierDriverSession(session, sm)) {
481
+ pairPrompt = pairPrompts.UNPAIRED + " " + workerProposal.getSystemPrompt(session);
474
482
  }
475
- return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
483
+ return pairPrompt;
476
484
  }
477
485
 
478
- return { getToolDefs: getToolDefs, handleMessage: handleMessage, handleTurnDone: handleTurnDone, getSystemPrompt: getSystemPrompt };
486
+ return {
487
+ beginHumanTurn: turnControl.beginHumanTurn,
488
+ getToolDefs: getToolDefs,
489
+ handleHumanStop: handleHumanStop,
490
+ handleMessage: handleMessage,
491
+ handleTurnDone: handleTurnDone,
492
+ getSystemPrompt: getSystemPrompt,
493
+ respondToWorkerProposal: workerProposal.respondToProposal,
494
+ workerPermission: workerPermission,
495
+ };
479
496
  }
480
497
 
481
- module.exports = {
482
- attachSessionPair: attachSessionPair,
483
- responseText: responseText,
484
- recentTurns: recentTurns,
485
- };
498
+ module.exports = { attachSessionPair: attachSessionPair, responseText: responseText, recentTurns: recentTurns };
@@ -198,7 +198,7 @@ function attachSessionSpawn(ctx) {
198
198
  var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
199
199
  var session = create.call(sm, {
200
200
  ownerId: parent.ownerId || null,
201
- sessionVisibility: parent.sessionVisibility || "shared",
201
+ sessionVisibility: "private",
202
202
  vendor: vendor,
203
203
  });
204
204
  session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
@@ -3,7 +3,9 @@ var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var { execFileSync } = require("child_process");
5
5
  var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
6
+ var updateSnooze = require("./update-snooze");
6
7
  var yoke = require("./yoke");
8
+ var sessionVisibility = require("./session-visibility");
7
9
 
8
10
  function vendorSupportsTui(vendor) {
9
11
  var info = yoke.getVendorInfo(vendor);
@@ -71,6 +73,9 @@ function attachSessions(ctx) {
71
73
  var send = ctx.send;
72
74
  var sendTo = ctx.sendTo;
73
75
  var sendToAdmins = ctx.sendToAdmins;
76
+ var broadcastUpdateAvailable = ctx.broadcastUpdateAvailable || function () {};
77
+ var broadcastUpdateStarted = ctx.broadcastUpdateStarted || function () {};
78
+ var sendToUpdateUser = ctx.sendToUpdateUser || function () {};
74
79
  var sendToSession = ctx.sendToSession;
75
80
  var sendToSessionOthers = ctx.sendToSessionOthers;
76
81
  var opts = ctx.opts;
@@ -82,6 +87,13 @@ function attachSessions(ctx) {
82
87
  var getLinuxUserForSession = ctx.getLinuxUserForSession;
83
88
  var ensureProjectAccessForSession = ctx.ensureProjectAccessForSession;
84
89
  var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
90
+ function stopSession(session) {
91
+ if (!session || (!session.isProcessing && !session._queryStarting)) return false;
92
+ if (typeof ctx.onHumanPairStop === "function") ctx.onHumanPairStop(session);
93
+ session.taskStopRequested = true;
94
+ if (session.abortController) session.abortController.abort();
95
+ return true;
96
+ }
85
97
  var hydrateImageRefs = ctx.hydrateImageRefs;
86
98
  var onProcessingChanged = ctx.onProcessingChanged;
87
99
  var broadcastPresence = ctx.broadcastPresence;
@@ -492,7 +504,7 @@ function attachSessions(ctx) {
492
504
  sm.sweepBlankSessions();
493
505
  var sessionOpts = {};
494
506
  if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
495
- if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
507
+ sessionOpts.sessionVisibility = sessionVisibility.normalize(msg.sessionVisibility);
496
508
  if (msg.vendor) sessionOpts.vendor = msg.vendor;
497
509
  // Mode resolution: vendors without a TUI session mode are always GUI.
498
510
  // TUI-capable sessions honor the explicit msg.mode if provided, otherwise
@@ -577,7 +589,8 @@ function attachSessions(ctx) {
577
589
  if (reusable) {
578
590
  if (sessionOpts.vendor && reusable.vendor !== sessionOpts.vendor) reusable.vendor = sessionOpts.vendor;
579
591
  if (!reusable.effort) reusable.effort = sessionOpts.effort;
580
- if (msg.sessionVisibility) reusable.sessionVisibility = msg.sessionVisibility;
592
+ sm.setSessionVisibility(reusable.localId, sessionOpts.sessionVisibility);
593
+ sessionVisibility.revokeViewers(ctx, reusable);
581
594
  sm.switchSession(reusable.localId, ws);
582
595
  newSess = reusable;
583
596
  } else {
@@ -605,9 +618,7 @@ function attachSessions(ctx) {
605
618
  }
606
619
 
607
620
  if (msg.type === "set_session_visibility") {
608
- if (typeof msg.sessionId === "number" && (msg.visibility === "shared" || msg.visibility === "private")) {
609
- sm.setSessionVisibility(msg.sessionId, msg.visibility);
610
- }
621
+ sessionVisibility.handleChange(ctx, ws, msg);
611
622
  return true;
612
623
  }
613
624
 
@@ -897,25 +908,30 @@ function attachSessions(ctx) {
897
908
  }
898
909
 
899
910
  if (msg.type === "set_update_channel") {
900
- if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return true;
911
+ if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
901
912
  var newChannel = msg.channel === "beta" ? "beta" : "stable";
902
913
  setUpdateChannel(newChannel);
903
914
  setLatestVersion(null);
904
915
  if (typeof opts.onSetUpdateChannel === "function") {
905
916
  opts.onSetUpdateChannel(newChannel);
906
917
  }
907
- // Re-fetch with new channel and broadcast to admin clients
918
+ // Re-fetch with new channel and broadcast to admin clients, honouring
919
+ // any snooze each of them has on the version that comes back.
908
920
  fetchVersion(newChannel).then(function (v) {
909
921
  if (v && isNewer(v, currentVersion)) {
910
922
  setLatestVersion(v);
911
- sendToAdmins({ type: "update_available", version: v });
923
+ broadcastUpdateAvailable(v);
912
924
  }
913
925
  }).catch(function () {});
914
926
  return true;
915
927
  }
916
928
 
917
929
  if (msg.type === "check_update") {
918
- if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return true;
930
+ if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
931
+ // A manual check is the user deliberately re-engaging, so it answers
932
+ // truthfully and ends any snooze rather than reporting an update it
933
+ // would then refuse to show.
934
+ updateSnooze.clearSnooze(updateSnooze.userKeyFor(ws, usersModule));
919
935
  var updateChannel = getUpdateChannel();
920
936
  fetchVersion(updateChannel).then(function (v) {
921
937
  if (v && isNewer(v, currentVersion)) {
@@ -928,9 +944,40 @@ function attachSessions(ctx) {
928
944
  return true;
929
945
  }
930
946
 
947
+ if (msg.type === "update_snooze") {
948
+ if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
949
+ var snoozeKey = updateSnooze.userKeyFor(ws, usersModule);
950
+ // The target is always the server's own latest version. A version named
951
+ // in the payload is ignored, as are any timestamps or user ids.
952
+ var snoozeResult = updateSnooze.snooze(
953
+ snoozeKey,
954
+ getLatestVersion(),
955
+ msg.duration,
956
+ Date.now(),
957
+ msg.tzOffsetMinutes
958
+ );
959
+ if (!snoozeResult.ok) {
960
+ sendTo(ws, { type: "update_snoozed", ok: false, error: snoozeResult.error });
961
+ return true;
962
+ }
963
+ // Every live connection of this user, so a second device stops showing
964
+ // a banner the user has already dealt with.
965
+ sendToUpdateUser(snoozeKey, {
966
+ type: "update_snoozed",
967
+ ok: true,
968
+ version: snoozeResult.version,
969
+ until: snoozeResult.until,
970
+ duration: snoozeResult.duration,
971
+ });
972
+ return true;
973
+ }
974
+
931
975
  if (msg.type === "update_now") {
932
- if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return true;
933
- send({ type: "update_started", version: getLatestVersion() || "" });
976
+ if (!updateSnooze.isUpdateAdmin(ws, usersModule)) return true;
977
+ // Installing makes any snooze moot; drop it so the record does not
978
+ // outlive the version it referred to.
979
+ updateSnooze.clearSnooze(updateSnooze.userKeyFor(ws, usersModule));
980
+ broadcastUpdateStarted(getLatestVersion());
934
981
  var _ipc = require("./ipc");
935
982
  var _config = require("./config");
936
983
  _ipc.sendIPCCommand(_config.socketPath(), { cmd: "update" });
@@ -964,10 +1011,7 @@ function attachSessions(ctx) {
964
1011
 
965
1012
  if (msg.type === "stop") {
966
1013
  var session = getSessionForWs(ws);
967
- if (session && session.isProcessing) {
968
- session.taskStopRequested = true;
969
- if (session.abortController) session.abortController.abort();
970
- }
1014
+ stopSession(session);
971
1015
  return true;
972
1016
  }
973
1017
 
@@ -1736,8 +1780,12 @@ function attachSessions(ctx) {
1736
1780
  sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
1737
1781
  return true;
1738
1782
  }
1783
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(wtDirName)) {
1784
+ sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid worktree directory name" });
1785
+ return true;
1786
+ }
1739
1787
  if (typeof onCreateWorktree === "function") {
1740
- var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase);
1788
+ var wtResult = onCreateWorktree(slug, wtBranch, wtDirName, wtBase, ws._clayUser);
1741
1789
  sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
1742
1790
  } else {
1743
1791
  sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
@@ -2026,6 +2074,13 @@ function attachSessions(ctx) {
2026
2074
  handleSessionsMessage: handleSessionsMessage,
2027
2075
  resolveSessionForView: resolveSessionForView,
2028
2076
  respondToAskUser: respondToAskUser,
2077
+ respondToHomePermission: function (ws, msg, session) {
2078
+ if (!session || !msg || !session.pendingPermissions || !session.pendingPermissions[msg.requestId]) return false;
2079
+ var indexedSessionId = sm.permissionRequestIndex[msg.requestId];
2080
+ if (indexedSessionId !== session.localId || sm.sessions.get(indexedSessionId) !== session) return false;
2081
+ return handleSessionsMessage(ws, { type: "permission_response", requestId: msg.requestId, decision: msg.decision });
2082
+ },
2083
+ stopHomeSession: stopSession,
2029
2084
  };
2030
2085
  }
2031
2086