clay-server 3.8.0-beta.2 → 3.8.0-beta.3

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.
@@ -203,9 +203,13 @@ function attachSessionPair(ctx) {
203
203
  async function sendToPartner(args, caller) {
204
204
  try {
205
205
  if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
206
- var resolved = groupAndPartner(caller);
207
206
  var message = typeof args.message === "string" ? args.message.trim() : "";
208
207
  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);
209
213
  var wait = args.wait !== false;
210
214
  var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
211
215
  timeout = Math.max(1, Math.min(900, timeout));
@@ -243,9 +247,14 @@ function attachSessionPair(ctx) {
243
247
  if (!wait) {
244
248
  token.detached = true;
245
249
  monitorPartner(resolved.group, caller, partner, token);
246
- 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." });
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." });
251
+ }
252
+ var completed = await waitForPartner(resolved.group, caller, partner, token, timeout);
253
+ if (created) {
254
+ completed.workerCreated = true;
255
+ completed.partnerId = partner.localId;
247
256
  }
248
- return toolResult(await waitForPartner(resolved.group, caller, partner, token, timeout));
257
+ return toolResult(completed);
249
258
  } catch (e) {
250
259
  if (caller) {
251
260
  var group = store.groupForMember(caller.localId);
@@ -277,10 +286,46 @@ function attachSessionPair(ctx) {
277
286
  }
278
287
  }
279
288
 
289
+ function interruptPartner(args, caller) {
290
+ try {
291
+ var resolved = groupAndPartner(caller);
292
+ var partner = resolved.partner;
293
+ if (!partner.isProcessing && !partner._queryStarting) {
294
+ return toolResult({ status: "idle", partnerId: partner.localId, title: partner.title || "New Session" });
295
+ }
296
+ partner.taskStopRequested = true;
297
+ if (partner.abortController) partner.abortController.abort();
298
+ return toolResult({ status: "interrupting", partnerId: partner.localId, title: partner.title || "New Session" });
299
+ } catch (e) {
300
+ return toolError(e);
301
+ }
302
+ }
303
+
304
+ function closePartner(args, caller) {
305
+ try {
306
+ var resolved = groupAndPartner(caller);
307
+ var partner = resolved.partner;
308
+ var interrupted = !!(partner.isProcessing || partner._queryStarting);
309
+ if (interrupted) {
310
+ partner.taskStopRequested = true;
311
+ if (partner.abortController) partner.abortController.abort();
312
+ }
313
+ if (partner._pairDelegation) finishDelegation(resolved.group, caller, partner, partner._pairDelegation);
314
+ var ws = { _clayUser: caller.ownerId ? { id: caller.ownerId } : null };
315
+ var result = store.dissolve(ws, { id: resolved.group.id });
316
+ if (!result.ok) throw new Error(result.error || "could not close the Worker pair");
317
+ return toolResult({ status: "closed", partnerId: partner.localId, interrupted: interrupted, historyPreserved: true });
318
+ } catch (e) {
319
+ return toolError(e);
320
+ }
321
+ }
322
+
280
323
  function getToolDefs(boundSession) {
281
324
  if (!boundSession) return pairMcp.getToolDefs({
282
325
  send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
283
326
  read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
327
+ interrupt: function () { return toolError(new Error("interrupt_partner requires a session-bound tool server")); },
328
+ close: function () { return toolError(new Error("close_partner requires a session-bound tool server")); },
284
329
  });
285
330
  // Mount whenever we have a bound session: MCP servers are fixed for the
286
331
  // lifetime of a query, and Claude queries live across turns, so gating on
@@ -295,6 +340,8 @@ function attachSessionPair(ctx) {
295
340
  var tools = pairMcp.getToolDefs({
296
341
  send: function (args) { return sendToPartner(args, boundSession); },
297
342
  read: function (args) { return readPartner(args, boundSession); },
343
+ interrupt: function (args) { return interruptPartner(args, boundSession); },
344
+ close: function (args) { return closePartner(args, boundSession); },
298
345
  });
299
346
  return tools.concat(workerProposal.getToolDefs(boundSession));
300
347
  }
@@ -343,6 +390,37 @@ function attachSessionPair(ctx) {
343
390
  return { driver: driver, worker: worker, group: result.group };
344
391
  }
345
392
 
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 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
+ }
423
+
346
424
  function createPair(ws, msg) {
347
425
  try {
348
426
  var created = createPairRecord(ws, msg);
@@ -390,7 +468,9 @@ function attachSessionPair(ctx) {
390
468
  var group = store.groupForMember(session.localId);
391
469
  var pairPrompt = "";
392
470
  if (group && group.pair && group.pair.driverId === session.localId) {
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.";
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. Use send_to_partner to delegate concrete bounded work to the Worker. The human may also message or stop the Worker directly; treat that intervention as authoritative. Use close_partner immediately when the human asks to close, dismiss, or remove the Worker pane; it preserves the session history. A completed Worker turn leaves the 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 Worker's implementation, delegate a follow-up turn to that Worker instead of editing the Worker-owned files yourself. If the Worker is no longer available, keep the work in the visible Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background session. 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 = "When the user asks you to open or use a visible Worker, call send_to_partner directly. Clay will create the paired 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 Worker.";
394
474
  }
395
475
  return [pairPrompt, workerProposal.getSystemPrompt(session)].filter(Boolean).join("\n\n");
396
476
  }
@@ -247,7 +247,7 @@ function attachWorkerProposal(ctx) {
247
247
  });
248
248
  var followup;
249
249
  if (result.status === "complete") {
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.");
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, keep the work in the visible Worker flow and use send_to_partner again after the stale pair is removed; never substitute a background session.\n\n" + (result.response || "The Worker completed without a text summary.");
251
251
  } else if (result.status === "interrupted") {
252
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.";
253
253
  } else if (result.status === "running") {
@@ -137,13 +137,7 @@ body.pane-mode #input-area {
137
137
  .split-pair-role-driver { color: var(--accent); background: color-mix(in srgb, var(--accent) 13%, transparent); }
138
138
  .split-pair-role-worker { color: var(--text-secondary); background: var(--bg-alt); }
139
139
 
140
- /* Role badges are buttons: Driver click clears roles, Worker click takes over. */
141
- button.split-pair-role {
142
- border: 0;
143
- cursor: pointer;
144
- font-family: inherit;
145
- }
146
- button.split-pair-role:hover { filter: brightness(1.25); }
140
+ /* Pair roles are status labels. Control remains with the Driver through MCP. */
147
141
 
148
142
  /* Ad-hoc splits: role assignment in each pane header. ALWAYS fully
149
143
  visible; hover/opacity gating made the control undiscoverable. */
@@ -265,16 +265,10 @@ export function syncPairChrome(host, split) {
265
265
  (function (sessionId, header) {
266
266
  var isDriver = sessionId === group.pair.driverId;
267
267
  var role = isDriver ? "Driver" : "Worker";
268
- var badge = document.createElement("button");
269
- badge.type = "button";
268
+ var badge = document.createElement("span");
270
269
  badge.className = "split-pair-role split-pair-role-" + role.toLowerCase();
271
270
  badge.textContent = role;
272
- badge.title = isDriver
273
- ? "Driver — click to clear the Driver/Worker roles"
274
- : "Worker — click to make this session the Driver instead";
275
- badge.addEventListener("click", function () {
276
- sendSetPair(group.id, isDriver ? null : sessionId);
277
- });
271
+ badge.title = isDriver ? "Driver controls this Worker" : "Worker controlled by the Driver";
278
272
  var title = header.querySelector(".split-pane-title");
279
273
  var access = header.querySelector(".split-pane-full-access");
280
274
  header.insertBefore(badge, access ? access.nextSibling : (title ? title.nextSibling : null));
@@ -6,11 +6,14 @@ function getToolDefs(handlers) {
6
6
  return [
7
7
  {
8
8
  name: "send_to_partner",
9
- description: "Delegate one concrete task to the other session in this split. The partner works visibly in its own pane. A completed turn does not end the Worker session: reuse the same Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
9
+ description: "Delegate one concrete task to the Worker. If this session has no pair yet, Clay creates a real Worker session and opens it visibly in the right pane before starting the task. A completed turn does not end the Worker session: reuse the same Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
10
10
  inputSchema: buildShape({
11
11
  message: { type: "string", description: "The complete task or question for the partner." },
12
12
  wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
13
13
  timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
14
+ workerVendor: { type: "string", description: "Optional Worker vendor to use only when creating a new pair. Defaults to a suitable installed vendor." },
15
+ workerModel: { type: "string", description: "Optional Worker model to use only when creating a new pair." },
16
+ workerEffort: { type: "string", description: "Optional Worker reasoning effort to use only when creating a new pair." },
14
17
  }, ["message"]),
15
18
  handler: function (args) { return handlers.send(args || {}); },
16
19
  },
@@ -22,6 +25,18 @@ function getToolDefs(handlers) {
22
25
  }),
23
26
  handler: function (args) { return handlers.read(args || {}); },
24
27
  },
28
+ {
29
+ name: "interrupt_partner",
30
+ description: "Interrupt the Worker's current task. Use this when the task is going in the wrong direction, needs to be reprioritized, or must stop before the next instruction. The Worker returns any partial result to you for review.",
31
+ inputSchema: buildShape({}),
32
+ handler: function (args) { return handlers.interrupt(args || {}); },
33
+ },
34
+ {
35
+ name: "close_partner",
36
+ description: "Close the visible Worker pane and dissolve the Driver/Worker pair while preserving both session histories. If the Worker is still running, Clay interrupts it before closing the pair.",
37
+ inputSchema: buildShape({}),
38
+ handler: function (args) { return handlers.close(args || {}); },
39
+ },
25
40
  ];
26
41
  }
27
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "3.8.0-beta.2",
3
+ "version": "3.8.0-beta.3",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",