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.
- package/lib/daemon.js +6 -27
- package/lib/project-connection.js +21 -8
- package/lib/project-session-pair.js +312 -0
- package/lib/project-session-spawn.js +8 -4
- package/lib/project-sessions.js +108 -10
- package/lib/project-user-message.js +18 -8
- package/lib/project.js +33 -0
- package/lib/public/app.js +20 -0
- package/lib/public/css/menus.css +83 -1
- package/lib/public/css/messages.css +14 -0
- package/lib/public/css/pane.css +420 -0
- package/lib/public/css/sidebar.css +40 -0
- package/lib/public/css/sticky-notes.css +2 -0
- package/lib/public/index.html +4 -0
- package/lib/public/modules/app-connection.js +6 -0
- package/lib/public/modules/app-favicon.js +5 -0
- package/lib/public/modules/app-header.js +101 -6
- package/lib/public/modules/app-messages.js +59 -6
- package/lib/public/modules/app-panels.js +49 -27
- package/lib/public/modules/app-rendering.js +11 -4
- package/lib/public/modules/pane-bridge.js +41 -0
- package/lib/public/modules/pane-session.js +18 -0
- package/lib/public/modules/sidebar-sessions.js +175 -1
- package/lib/public/modules/sidebar.js +2 -0
- package/lib/public/modules/split-group-helpers.js +18 -0
- package/lib/public/modules/split-pair-ui.js +324 -0
- package/lib/public/modules/split-view.js +499 -0
- package/lib/public/modules/sticky-notes.js +32 -91
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +111 -20
- package/lib/sdk-message-processor.js +2 -2
- package/lib/server.js +24 -4
- package/lib/session-hygiene.js +100 -0
- package/lib/session-pair-mcp-server.js +28 -0
- package/lib/session-split-groups.js +298 -0
- package/lib/sessions.js +163 -34
- package/lib/ws-request.js +14 -0
- package/lib/ws-schema.js +13 -0
- package/lib/yoke/adapters/claude.js +32 -0
- package/lib/yoke/adapters/codex.js +100 -77
- package/lib/yoke/adapters/kiro.js +34 -35
- package/lib/yoke/codex-app-server.js +56 -6
- package/lib/yoke/index.js +1 -0
- package/lib/yoke/skill-discovery.js +196 -0
- package/lib/yoke/vendor-registry.js +34 -0
- 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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
143
|
-
|
|
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:
|
|
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:
|
|
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
|
|