clay-server 2.47.0-beta.6 → 3.0.0-beta.2

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 (46) hide show
  1. package/lib/daemon.js +6 -27
  2. package/lib/project-connection.js +21 -8
  3. package/lib/project-session-pair.js +312 -0
  4. package/lib/project-session-spawn.js +8 -4
  5. package/lib/project-sessions.js +108 -10
  6. package/lib/project-user-message.js +18 -8
  7. package/lib/project.js +33 -0
  8. package/lib/public/app.js +20 -0
  9. package/lib/public/css/menus.css +83 -1
  10. package/lib/public/css/messages.css +14 -0
  11. package/lib/public/css/pane.css +420 -0
  12. package/lib/public/css/sidebar.css +40 -0
  13. package/lib/public/css/sticky-notes.css +2 -0
  14. package/lib/public/index.html +4 -0
  15. package/lib/public/modules/app-connection.js +6 -0
  16. package/lib/public/modules/app-favicon.js +5 -0
  17. package/lib/public/modules/app-header.js +101 -6
  18. package/lib/public/modules/app-messages.js +59 -6
  19. package/lib/public/modules/app-panels.js +49 -27
  20. package/lib/public/modules/app-rendering.js +11 -4
  21. package/lib/public/modules/pane-bridge.js +41 -0
  22. package/lib/public/modules/pane-session.js +18 -0
  23. package/lib/public/modules/sidebar-sessions.js +175 -1
  24. package/lib/public/modules/sidebar.js +2 -0
  25. package/lib/public/modules/split-group-helpers.js +18 -0
  26. package/lib/public/modules/split-pair-ui.js +324 -0
  27. package/lib/public/modules/split-view.js +499 -0
  28. package/lib/public/modules/sticky-notes.js +32 -91
  29. package/lib/public/style.css +1 -0
  30. package/lib/sdk-bridge.js +111 -20
  31. package/lib/sdk-message-processor.js +2 -2
  32. package/lib/server.js +24 -4
  33. package/lib/session-hygiene.js +100 -0
  34. package/lib/session-pair-mcp-server.js +28 -0
  35. package/lib/session-split-groups.js +298 -0
  36. package/lib/sessions.js +163 -34
  37. package/lib/ws-request.js +14 -0
  38. package/lib/ws-schema.js +13 -0
  39. package/lib/yoke/adapters/claude.js +32 -0
  40. package/lib/yoke/adapters/codex.js +100 -77
  41. package/lib/yoke/adapters/kiro.js +34 -35
  42. package/lib/yoke/codex-app-server.js +56 -6
  43. package/lib/yoke/index.js +1 -0
  44. package/lib/yoke/skill-discovery.js +196 -0
  45. package/lib/yoke/vendor-registry.js +34 -0
  46. package/package.json +2 -2
package/lib/daemon.js CHANGED
@@ -1352,13 +1352,15 @@ var ipc = createIPCServer(socketPath(), function (msg) {
1352
1352
  // skill_listing / deferred_tools attachments which can run into the
1353
1353
  // tens of KB on its own)
1354
1354
  // 2. mtime older than 60 seconds (avoid racing an in-flight warmup)
1355
- // 3. exactly one user message, content text equals literal "hi"
1355
+ // 3. exactly one real user prompt equal to literal "hi" (the automatic
1356
+ // interruption record written when the probe is aborted is ignored)
1356
1357
  // 4. zero assistant messages (real users virtually always get a reply)
1357
1358
  //
1358
1359
  // Multi-user OS-isolated installs have per-user ~/.claude trees that the
1359
1360
  // daemon process can't traverse; the inline cleanup in claude-worker.js
1360
1361
  // covers those.
1361
1362
  function cleanupSdkWarmupSessions() {
1363
+ var sessionHygiene = require("./session-hygiene");
1362
1364
  var projectsRoot = path.join(REAL_HOME, ".claude", "projects");
1363
1365
  var projectDirs;
1364
1366
  try { projectDirs = fs.readdirSync(projectsRoot, { withFileTypes: true }); }
@@ -1388,9 +1390,7 @@ function cleanupSdkWarmupSessions() {
1388
1390
  var raw;
1389
1391
  try { raw = fs.readFileSync(fp, "utf8"); } catch (e) { continue; }
1390
1392
  var lines = raw.split("\n");
1391
- var userCount = 0;
1392
- var assistantCount = 0;
1393
- var userText = null;
1393
+ var events = [];
1394
1394
  var parseOk = true;
1395
1395
  for (var li = 0; li < lines.length; li++) {
1396
1396
  var line = lines[li];
@@ -1398,31 +1398,10 @@ function cleanupSdkWarmupSessions() {
1398
1398
  var ev;
1399
1399
  try { ev = JSON.parse(line); } catch (e) { parseOk = false; break; }
1400
1400
  if (!ev || typeof ev !== "object") continue;
1401
- if (ev.type === "user") {
1402
- userCount++;
1403
- if (userCount > 1) break;
1404
- var msg = ev.message;
1405
- var content = msg && msg.content;
1406
- if (typeof content === "string") {
1407
- userText = content;
1408
- } else if (Array.isArray(content)) {
1409
- var parts = [];
1410
- for (var ci = 0; ci < content.length; ci++) {
1411
- if (content[ci] && content[ci].type === "text" && typeof content[ci].text === "string") {
1412
- parts.push(content[ci].text);
1413
- }
1414
- }
1415
- userText = parts.join("");
1416
- }
1417
- } else if (ev.type === "assistant") {
1418
- assistantCount++;
1419
- if (assistantCount > 0) break;
1420
- }
1401
+ events.push(ev);
1421
1402
  }
1422
1403
  if (!parseOk) continue;
1423
- if (userCount !== 1) continue;
1424
- if (assistantCount !== 0) continue;
1425
- if (userText !== "hi") continue;
1404
+ if (!sessionHygiene.isClaudeWarmupTranscript(events)) continue;
1426
1405
 
1427
1406
  try { fs.unlinkSync(fp); deleted++; } catch (e) {}
1428
1407
  }
@@ -39,6 +39,7 @@ function attachConnection(ctx) {
39
39
  var _loop = ctx._loop;
40
40
  var _mcp = ctx._mcp;
41
41
  var _notifications = ctx._notifications;
42
+ var _splitGroups = ctx._splitGroups;
42
43
  var hydrateImageRefs = ctx.hydrateImageRefs;
43
44
  var resolveSessionForView = ctx.resolveSessionForView;
44
45
  var broadcastClientCount = ctx.broadcastClientCount;
@@ -128,9 +129,19 @@ function attachConnection(ctx) {
128
129
  } else if (!usersModule.isMultiUser()) {
129
130
  allSessions = allSessions.filter(function (s) { return !s.ownerId; });
130
131
  }
131
- var restoredState = findRestoredActiveSession(ws, wsUser, allSessions);
132
+ var restoredState = ws._clayPane
133
+ ? { active: null, storedPresence: null }
134
+ : findRestoredActiveSession(ws, wsUser, allSessions);
132
135
  var restoredActive = restoredState.active;
133
136
  var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
137
+ var initialEffort = (restoredActive && restoredActive.effort) || yoke.clampEffort(
138
+ initialVendor,
139
+ (sm.currentEffortByVendor && sm.currentEffortByVendor[initialVendor]) || sm.currentEffort || "medium"
140
+ ) || null;
141
+ if (restoredActive && !restoredActive.effort && initialEffort) {
142
+ restoredActive.effort = initialEffort;
143
+ sm.saveSessionFile(restoredActive);
144
+ }
134
145
  var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
135
146
  sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked, vendors: yoke.VENDOR_REGISTRY });
136
147
  // Update notifications are pushed on a scheduled interval (see
@@ -139,10 +150,11 @@ function attachConnection(ctx) {
139
150
  if (sm.slashCommands) {
140
151
  sendTo(ws, { type: "slash_commands", commands: sm.slashCommands });
141
152
  }
142
- if (sm.currentModel) {
143
- sendTo(ws, { type: "model_info", model: sm.currentModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
153
+ var initialModel = (restoredActive && restoredActive.model) || sm.currentModel || "";
154
+ if (initialModel) {
155
+ sendTo(ws, { type: "model_info", model: initialModel, models: initialModels, vendor: initialVendor, availableVendors: sm.availableVendors || [], installedVendors: sm.installedVendors || [] });
144
156
  }
145
- sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
157
+ sendTo(ws, { type: "config_state", model: initialModel, mode: sm.currentPermissionMode || "default", effort: initialEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
146
158
  sendTo(ws, { type: "last_vendor", vendor: sm.lastVendor || "" });
147
159
  sendTo(ws, Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
148
160
  sendTo(ws, { type: "term_list", terminals: tm.list() });
@@ -186,13 +198,14 @@ function attachConnection(ctx) {
186
198
  type: "session_list",
187
199
  sessions: allSessions.map(function (s) { return sm.mapSessionForClient(s); }),
188
200
  });
201
+ if (_splitGroups) _splitGroups.sendConnectionState(ws);
189
202
 
190
203
  // Restore active session for this client from server-side presence
191
204
  var active = restoredState.active;
192
205
  var presenceKey = wsUser ? wsUser.id : "_default";
193
206
  var storedPresence = restoredState.storedPresence;
194
207
  var autoCreated = false;
195
- if (!active) {
208
+ if (!active && !ws._clayPane) {
196
209
  var autoOpts = {};
197
210
  if (wsUser && usersModule.isMultiUser()) autoOpts.ownerId = wsUser.id;
198
211
  active = sm.createSession(autoOpts, ws);
@@ -212,7 +225,7 @@ function attachConnection(ctx) {
212
225
  try { resolveSessionForView(active, ws); } catch (e) {}
213
226
  }
214
227
  var _vendorCaps = (sm.capabilitiesByVendor && sm.capabilitiesByVendor[active.vendor || sm.defaultVendor || "claude"]) || {};
215
- sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended });
228
+ sendTo(ws, { type: "session_switched", id: active.localId, cliSessionId: active.cliSessionId || null, loop: active.loop || null, vendor: active.vendor || null, model: active.model || null, effort: active.effort || initialEffort || null, hasHistory: (active.history && active.history.length > 0), capabilities: _vendorCaps, mode: active.mode || "gui", terminalId: typeof active.terminalId === "number" ? active.terminalId : null, runtimeMode: active.runtimeMode || null, runtimeTerminalId: typeof active.runtimeTerminalId === "number" ? active.runtimeTerminalId : null, tuiSuspended: !!active.tuiSuspended });
216
229
  // Send per-session context sources
217
230
  var sessionSources = loadContextSources(slug, active.localId);
218
231
  sendTo(ws, { type: "context_sources_state", active: sessionSources });
@@ -257,7 +270,7 @@ function attachConnection(ctx) {
257
270
  }
258
271
  }
259
272
 
260
- if (active) {
273
+ if (active && !ws._clayPane) {
261
274
  userPresence.setPresence(slug, presenceKey, active.localId, storedPresence ? storedPresence.mateDm : null);
262
275
  // For auto-created sessions, apply project email defaults
263
276
  if (autoCreated) {
@@ -296,7 +309,7 @@ function attachConnection(ctx) {
296
309
  }
297
310
 
298
311
  function handleDisconnection(ws) {
299
- if (ws._clayActiveSession) {
312
+ if (ws._clayActiveSession && !ws._clayPane) {
300
313
  var dcPresKey = ws._clayUser ? ws._clayUser.id : "_default";
301
314
  var dcExisting = userPresence.getPresence(slug, dcPresKey);
302
315
  userPresence.setPresence(slug, dcPresKey, ws._clayActiveSession, dcExisting ? dcExisting.mateDm : null);
@@ -0,0 +1,312 @@
1
+ var pairMcp = require("./session-pair-mcp-server");
2
+ var yoke = require("./yoke");
3
+
4
+ var MAX_RESPONSE_CHARS = 30000;
5
+
6
+ function toolResult(value) {
7
+ return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
8
+ }
9
+
10
+ function toolError(err) {
11
+ return Promise.resolve({
12
+ content: [{ type: "text", text: "Error: " + (err.message || err) }],
13
+ isError: true,
14
+ });
15
+ }
16
+
17
+ function responseText(history, fromIndex) {
18
+ var text = "";
19
+ for (var i = Math.max(0, fromIndex || 0); i < history.length; i++) {
20
+ if (history[i] && history[i].type === "delta" && history[i].text) text += history[i].text;
21
+ }
22
+ if (text.length > MAX_RESPONSE_CHARS) text = text.slice(-MAX_RESPONSE_CHARS);
23
+ return text;
24
+ }
25
+
26
+ function errorSince(history, fromIndex) {
27
+ for (var i = history.length - 1; i >= Math.max(0, fromIndex || 0); i--) {
28
+ if (history[i] && history[i].type === "error") return history[i].text || "partner turn failed";
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function recentTurns(session, count) {
34
+ var history = session.history || [];
35
+ var starts = [];
36
+ for (var i = 0; i < history.length; i++) {
37
+ if (history[i] && history[i].type === "user_message") starts.push(i);
38
+ }
39
+ var from = starts.length > 0 ? starts[Math.max(0, starts.length - count)] : 0;
40
+ var turns = [];
41
+ var current = null;
42
+ for (var j = from; j < history.length; j++) {
43
+ var item = history[j];
44
+ if (!item) continue;
45
+ if (item.type === "user_message") {
46
+ current = { user: item.text || "", delegated: !!item.delegated, response: "" };
47
+ turns.push(current);
48
+ } else if (item.type === "delta" && item.text) {
49
+ if (!current) { current = { user: "", delegated: false, response: "" }; turns.push(current); }
50
+ current.response += item.text;
51
+ if (current.response.length > MAX_RESPONSE_CHARS) current.response = current.response.slice(-MAX_RESPONSE_CHARS);
52
+ }
53
+ }
54
+ return turns;
55
+ }
56
+
57
+ function attachSessionPair(ctx) {
58
+ var sm = ctx.sm;
59
+ var store = ctx.splitStore;
60
+
61
+ function groupAndPartner(caller) {
62
+ if (!caller) throw new Error("partner tools require a session-bound tool server");
63
+ var group = store.groupForMember(caller.localId);
64
+ if (!group) throw new Error("this session is not in a split group");
65
+ if (group.pair && group.pair.driverId !== caller.localId) {
66
+ throw new Error("only the configured Driver can direct this pair");
67
+ }
68
+ var partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
69
+ var partner = sm.sessions.get(partnerId);
70
+ if (!partner) throw new Error("split partner session was not found");
71
+ if (caller.ownerId !== partner.ownerId) throw new Error("split partner access denied");
72
+ return { group: group, partner: partner };
73
+ }
74
+
75
+ function broadcastDelegation(group, caller, partner, active) {
76
+ var message = {
77
+ type: "split_delegation",
78
+ groupId: group.id,
79
+ from: caller.localId,
80
+ to: partner.localId,
81
+ active: !!active,
82
+ };
83
+ if (typeof ctx.broadcastDelegation === "function") ctx.broadcastDelegation(group, message);
84
+ else ctx.send(message);
85
+ }
86
+
87
+ function finishDelegation(group, caller, partner, token) {
88
+ if (partner._pairDelegation !== token) return;
89
+ delete partner._pairDelegation;
90
+ delete partner._delegatedBy;
91
+ broadcastDelegation(group, caller, partner, false);
92
+ }
93
+
94
+ function waitForPartner(group, caller, partner, token, timeoutSeconds) {
95
+ return new Promise(function (resolve, reject) {
96
+ var deadline = Date.now() + timeoutSeconds * 1000;
97
+ var timer = setInterval(function () {
98
+ var currentGroup = store.groupForMember(caller.localId);
99
+ if (!currentGroup || currentGroup.id !== group.id || currentGroup.members.indexOf(partner.localId) === -1) {
100
+ clearInterval(timer);
101
+ finishDelegation(group, caller, partner, token);
102
+ reject(new Error("the split group was dissolved while waiting for the partner"));
103
+ return;
104
+ }
105
+ if (!partner.isProcessing && !partner._queryStarting) {
106
+ clearInterval(timer);
107
+ finishDelegation(group, caller, partner, token);
108
+ var failure = errorSince(partner.history || [], token.startIndex);
109
+ resolve({
110
+ status: failure ? "error" : "complete",
111
+ response: responseText(partner.history || [], token.startIndex),
112
+ error: failure || undefined,
113
+ });
114
+ return;
115
+ }
116
+ if (Date.now() >= deadline) {
117
+ clearInterval(timer);
118
+ monitorPartner(group, caller, partner, token);
119
+ resolve({ status: "running", response: responseText(partner.history || [], token.startIndex), hint: "Use read_partner to check again." });
120
+ }
121
+ }, 500);
122
+ });
123
+ }
124
+
125
+ function monitorPartner(group, caller, partner, token) {
126
+ var timer = setInterval(function () {
127
+ var currentGroup = store.groupForMember(caller.localId);
128
+ if (!currentGroup || currentGroup.id !== group.id || (!partner.isProcessing && !partner._queryStarting)) {
129
+ clearInterval(timer);
130
+ finishDelegation(group, caller, partner, token);
131
+ }
132
+ }, 500);
133
+ }
134
+
135
+ async function sendToPartner(args, caller) {
136
+ try {
137
+ if (caller && caller._delegatedBy) throw new Error("delegated turns cannot delegate to another session");
138
+ var resolved = groupAndPartner(caller);
139
+ var message = typeof args.message === "string" ? args.message.trim() : "";
140
+ if (!message) throw new Error("message is required");
141
+ var wait = args.wait !== false;
142
+ var timeout = Number.isFinite(args.timeoutSeconds) ? Math.floor(args.timeoutSeconds) : 300;
143
+ timeout = Math.max(1, Math.min(900, timeout));
144
+ var partner = resolved.partner;
145
+ if (partner._pairDelegation) throw new Error("the partner is already handling a delegated task");
146
+ var token = { from: caller.localId, groupId: resolved.group.id, startIndex: partner.history.length };
147
+ partner._pairDelegation = token;
148
+ partner._delegatedBy = caller.localId;
149
+ sm.sendAndRecord(partner, {
150
+ type: "user_message",
151
+ text: message,
152
+ delegated: true,
153
+ delegatedBy: caller.localId,
154
+ delegatedByTitle: caller.title || "Driver",
155
+ delegatedByVendor: caller.vendor || "claude",
156
+ });
157
+ partner.lastActivity = Date.now();
158
+ partner.sentToolResults = {};
159
+ broadcastDelegation(resolved.group, caller, partner, true);
160
+ var sdk = ctx.getSdk();
161
+ if (!sdk) throw new Error("SDK bridge is not ready");
162
+ if (!partner.isProcessing) {
163
+ partner.isProcessing = true;
164
+ ctx.onProcessingChanged();
165
+ sm.sendToSession(partner, { type: "status", status: "processing" });
166
+ }
167
+ if (!sdk.pushMessage(partner, message)) {
168
+ partner._queryStartTs = Date.now();
169
+ Promise.resolve(sdk.startQuery(partner, message, undefined, ctx.getLinuxUserForSession(partner))).catch(function (err) {
170
+ partner.isProcessing = false;
171
+ sm.sendAndRecord(partner, { type: "error", text: err.message || String(err) });
172
+ });
173
+ }
174
+ sm.broadcastSessionList();
175
+ if (!wait) {
176
+ monitorPartner(resolved.group, caller, partner, token);
177
+ return toolResult({ status: "running", partnerId: partner.localId, hint: "Use read_partner to check progress." });
178
+ }
179
+ return toolResult(await waitForPartner(resolved.group, caller, partner, token, timeout));
180
+ } catch (e) {
181
+ if (caller) {
182
+ var group = store.groupForMember(caller.localId);
183
+ if (group) {
184
+ var partnerId = group.members[0] === caller.localId ? group.members[1] : group.members[0];
185
+ var partner = sm.sessions.get(partnerId);
186
+ if (partner && partner._pairDelegation && partner._pairDelegation.from === caller.localId && !partner.isProcessing) {
187
+ finishDelegation(group, caller, partner, partner._pairDelegation);
188
+ }
189
+ }
190
+ }
191
+ return toolError(e);
192
+ }
193
+ }
194
+
195
+ function readPartner(args, caller) {
196
+ try {
197
+ var resolved = groupAndPartner(caller);
198
+ var count = Number.isFinite(args.lastTurns) ? Math.floor(args.lastTurns) : 1;
199
+ count = Math.max(1, Math.min(5, count));
200
+ return toolResult({
201
+ status: resolved.partner.isProcessing ? "running" : "idle",
202
+ partnerId: resolved.partner.localId,
203
+ title: resolved.partner.title || "New Session",
204
+ turns: recentTurns(resolved.partner, count),
205
+ });
206
+ } catch (e) {
207
+ return toolError(e);
208
+ }
209
+ }
210
+
211
+ function getToolDefs(boundSession) {
212
+ if (!boundSession) return pairMcp.getToolDefs({
213
+ send: function () { return toolError(new Error("send_to_partner requires a session-bound tool server")); },
214
+ read: function () { return toolError(new Error("read_partner requires a session-bound tool server")); },
215
+ });
216
+ // Mount whenever we have a bound session: MCP servers are fixed for the
217
+ // lifetime of a query, and Claude queries live across turns, so gating on
218
+ // group membership HERE would permanently hide the tools from a session
219
+ // whose split is created mid-conversation (the ad-hoc flow). Handlers
220
+ // re-resolve the group on every call, so an ungrouped session gets a
221
+ // clear error, and a session that gains a partner later just works.
222
+ // The one structural exclusion: a session that is ALREADY a configured
223
+ // pair worker at query start never sees the tools.
224
+ var group = store.groupForMember(boundSession.localId);
225
+ if (group && group.pair && group.pair.driverId !== boundSession.localId) return [];
226
+ return pairMcp.getToolDefs({
227
+ send: function (args) { return sendToPartner(args, boundSession); },
228
+ read: function (args) { return readPartner(args, boundSession); },
229
+ });
230
+ }
231
+
232
+ function validateVendor(vendor) {
233
+ var installed = sm.installedVendors || [];
234
+ if (!vendor || installed.indexOf(vendor) === -1) throw new Error("vendor is not installed: " + (vendor || "unknown"));
235
+ return vendor;
236
+ }
237
+
238
+ function createPair(ws, msg) {
239
+ try {
240
+ if (ctx.isMate) throw new Error("Pair sessions are only available in projects");
241
+ var driverSpec = msg.driver || {};
242
+ var workerSpec = msg.worker || {};
243
+ var workerVendor = validateVendor(workerSpec.vendor || sm.lastVendor || "codex");
244
+ var ownerId = ws._clayUser && ctx.usersModule.isMultiUser() ? ws._clayUser.id : null;
245
+ var driver;
246
+ var groupName;
247
+ if (Number.isInteger(driverSpec.sessionId)) {
248
+ // "Add Worker" on an existing session: the current session becomes
249
+ // the Driver with its full conversation context intact.
250
+ driver = sm.sessions.get(driverSpec.sessionId);
251
+ if (!driver) throw new Error("driver session not found");
252
+ if ((driver.ownerId || null) !== ownerId) throw new Error("driver session access denied");
253
+ if (store.groupForMember(driver.localId)) throw new Error("this session is already in a split group");
254
+ groupName = undefined; // auto name from member titles
255
+ } else {
256
+ var driverVendor = validateVendor(driverSpec.vendor || "claude");
257
+ var driverEffort = yoke.clampEffort(driverVendor, driverSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[driverVendor]) || sm.currentEffort || "medium") || null;
258
+ driver = sm.createSessionRaw({ ownerId: ownerId, vendor: driverVendor, model: driverSpec.model || null, effort: driverEffort });
259
+ driver.title = "Driver · " + ((yoke.getVendorInfo(driverVendor) || {}).displayName || driverVendor);
260
+ if (driverSpec.effort) driver.loopSettings = { effort: driverSpec.effort };
261
+ groupName = "Agent pair";
262
+ }
263
+ var workerEffort = yoke.clampEffort(workerVendor, workerSpec.effort || (sm.currentEffortByVendor && sm.currentEffortByVendor[workerVendor]) || sm.currentEffort || "medium") || null;
264
+ var worker = sm.createSessionRaw({ ownerId: ownerId, vendor: workerVendor, model: workerSpec.model || null, effort: workerEffort });
265
+ worker.title = "Worker · " + ((yoke.getVendorInfo(workerVendor) || {}).displayName || workerVendor);
266
+ if (workerSpec.effort) worker.loopSettings = { effort: workerSpec.effort };
267
+ var result = store.create(ws, {
268
+ members: [driver.localId, worker.localId],
269
+ pair: { driverId: driver.localId, workerId: worker.localId },
270
+ name: groupName,
271
+ });
272
+ if (!result.ok) throw new Error(result.error);
273
+ sm.broadcastSessionList();
274
+ ctx.sendTo(ws, { type: "pair_session_created", ok: true, group: result.group });
275
+ } catch (e) {
276
+ ctx.sendTo(ws, { type: "pair_session_created", ok: false, error: e.message || String(e) });
277
+ }
278
+ }
279
+
280
+ function handleMessage(ws, msg) {
281
+ if (msg.type === "pair_session_options") {
282
+ if (ctx.isMate) return false;
283
+ ctx.sendTo(ws, {
284
+ type: "pair_session_options",
285
+ installedVendors: sm.installedVendors || [],
286
+ modelsByVendor: sm.modelsByVendor || {},
287
+ capabilitiesByVendor: sm.capabilitiesByVendor || {},
288
+ lastVendor: sm.lastVendor || null,
289
+ });
290
+ return true;
291
+ }
292
+ if (msg.type === "pair_session_create") {
293
+ createPair(ws, msg);
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+
299
+ function getSystemPrompt(session) {
300
+ var group = store.groupForMember(session.localId);
301
+ if (!group || !group.pair || group.pair.driverId !== session.localId) return "";
302
+ return "You are the Driver in a two-agent pair. Plan the work, delegate concrete bounded tasks with send_to_partner, inspect results with read_partner, then integrate and verify the final outcome. Do not delegate work that must be performed sequentially in this same session.";
303
+ }
304
+
305
+ return { getToolDefs: getToolDefs, handleMessage: handleMessage, getSystemPrompt: getSystemPrompt };
306
+ }
307
+
308
+ module.exports = {
309
+ attachSessionPair: attachSessionPair,
310
+ responseText: responseText,
311
+ recentTurns: recentTurns,
312
+ };
@@ -336,13 +336,17 @@ function attachSessionSpawn(ctx) {
336
336
  // its handlers fail closed if anything ever routes a call to them.
337
337
  function createMcpServer(adapter, boundSession) {
338
338
  if (ctx.isMate || !adapter || typeof adapter.createToolServer !== "function") return null;
339
+ var tools = sessionSpawnMcp.getToolDefs({
340
+ spawn: function (args) { return spawn(args, boundSession || null); },
341
+ check: function (args) { return check(args, boundSession || null); },
342
+ });
343
+ if (typeof ctx.getPairToolDefs === "function") {
344
+ tools = tools.concat(ctx.getPairToolDefs(boundSession || null) || []);
345
+ }
339
346
  return adapter.createToolServer({
340
347
  name: "clay-sessions",
341
348
  version: "1.0.0",
342
- tools: sessionSpawnMcp.getToolDefs({
343
- spawn: function (args) { return spawn(args, boundSession || null); },
344
- check: function (args) { return check(args, boundSession || null); },
345
- }),
349
+ tools: tools,
346
350
  });
347
351
  }
348
352